From 721aecf26c5e4130dac1c320b1ec3df180cff411 Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Wed, 4 Aug 2021 12:21:20 -0700 Subject: [PATCH 01/13] [ASTextNode2] Second try to merge Google's ASTextNode2 changes This time I included the accessibility changes, which required adding some of the new accessibility features in other parts of the code. Note, there are cases where not all of the accessibility was brought over because that was supposed to be covered in a separate PR made by someone from Google. --- Source/ASDisplayNode+Beta.h | 5 + Source/ASDisplayNode.h | 17 + Source/ASDisplayNode.mm | 16 + Source/ASExperimentalFeatures.h | 3 + Source/ASExperimentalFeatures.mm | 5 +- Source/ASTextNode2.h | 25 +- Source/ASTextNode2.mm | 692 +++++--- Source/Details/_ASDisplayView.mm | 1 + Source/Details/_ASDisplayViewAccessiblity.h | 52 +- Source/Details/_ASDisplayViewAccessiblity.mm | 489 ++++-- .../Private/ASDisplayNode+FrameworkPrivate.h | 35 + Source/Private/ASDisplayNodeInternal.h | 1 + Source/Private/ASInternalHelpers.h | 12 + Source/Private/ASInternalHelpers.mm | 32 + .../Component/ASTextDebugOption.mm | 2 +- .../TextExperiment/Component/ASTextLayout.h | 29 +- .../TextExperiment/Component/ASTextLayout.mm | 1440 +++++++---------- .../TextExperiment/String/ASTextAttribute.h | 11 +- .../TextExperiment/String/ASTextAttribute.mm | 6 + .../Utility/NSAttributedString+ASText.h | 23 +- .../Utility/NSAttributedString+ASText.mm | 97 +- .../Utility/NSParagraphStyle+ASText.mm | 2 +- 22 files changed, 1795 insertions(+), 1200 deletions(-) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 882042a97..051f8e1d8 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -166,6 +166,11 @@ AS_CATEGORY_IMPLEMENTABLE */ - (void)enableSubtreeRasterization; +/** + * @abstract Top, left, bottom, right padding values for the node. Only used in yoga + */ +@property (readonly) UIEdgeInsets paddings; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index eff930ccf..f5ee1e694 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -46,6 +46,11 @@ typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(void); */ typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(void); +/** + * Accessibility elements creation block. Used to specify accessibility elements of the node. + */ +typedef NSArray *_Nullable (^ASDisplayNodeAccessibilityElementsBlock)(void); + /** * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. */ @@ -817,6 +822,18 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; @end +@interface ASDisplayNode (CustomAccessibilityBehavior) + +/** + * Set the block that should be used to determining the accessibility elements of the node. + * When set, the accessibility-related logic (e.g. label aggregation) will not be triggered. + * + * @param block The block that returns the accessibility elements of the node. + */ +- (void)setAccessibilityElementsBlock:(ASDisplayNodeAccessibilityElementsBlock)block; + +@end + @interface ASDisplayNode (ASLayoutElement) /** diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index b96a77477..0d40098e7 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -929,6 +929,22 @@ - (void)__setNodeController:(ASNodeController *)controller } } +- (UIEdgeInsets)paddings { + MutexLocker l(__instanceLock__); +#if YOGA + if (_flags.yoga) { + YGNodeRef yogaNode = _style.yogaNode; + CGFloat top = YGNodeLayoutGetPadding(yogaNode, YGEdgeTop); + CGFloat left = YGNodeLayoutGetPadding(yogaNode, YGEdgeLeft); + CGFloat bottom = YGNodeLayoutGetPadding(yogaNode, YGEdgeBottom); + CGFloat right = YGNodeLayoutGetPadding(yogaNode, YGEdgeRight); + return UIEdgeInsetsMake(top, left, bottom, right); + } +#endif // YOGA + return UIEdgeInsetsZero; + +} + - (void)checkResponderCompatibility { #if ASDISPLAYNODE_ASSERTIONS_ENABLED diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index a8b655ce9..637d383b9 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -31,6 +31,9 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalDisableGlobalTextkitLock = 1 << 10, // exp_disable_global_textkit_lock ASExperimentalMainThreadOnlyDataController = 1 << 11, // exp_main_thread_only_data_controller ASExperimentalRangeUpdateOnChangesetUpdate = 1 << 12, // exp_range_update_on_changeset_update + ASExperimentalDoNotCacheAccessibilityElements = 1 << 23, // exp_do_not_cache_accessibility_elements + ASExperimentalEnableNodeIsHiddenFromAcessibility = 1 << 26, // exp_enable_node_is_hidden_from_accessibility + ASExperimentalEnableAcessibilityElementsReturnNil = 1 << 27, // exp_enable_accessibility_elements_return_nil ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 6113dc405..8f35fd3fa 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -24,7 +24,10 @@ @"exp_optimize_data_controller_pipeline", @"exp_disable_global_textkit_lock", @"exp_main_thread_only_data_controller", - @"exp_range_update_on_changeset_update"])); + @"exp_range_update_on_changeset_update", + @"exp_do_not_cache_accessibility_elements", + @"exp_enable_node_is_hidden_from_accessibility", + @"exp_enable_accessibility_elements_return_nil"])); if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/ASTextNode2.h b/Source/ASTextNode2.h index 5848adc37..86ad7b5ea 100644 --- a/Source/ASTextNode2.h +++ b/Source/ASTextNode2.h @@ -14,6 +14,20 @@ NS_ASSUME_NONNULL_BEGIN +/** + * Get and Set ASTextNode to use: + * a) Intrinsic size fix for NSAtrributedStrings with no paragraph styles and + * b) Yoga direction to determine alignment of NSTextAlignmentNatural text nodes. + */ +BOOL ASGetEnableTextNode2ImprovedRTL(void); +void ASSetEnableTextNode2ImprovedRTL(BOOL enable); + +/** + * Get and Set ASTextLayout and ASTextNode2 to enable to calculation of visible text range. + */ +BOOL ASGetEnableTextTruncationVisibleRange(void); +void ASSetEnableTextTruncationVisibleRange(BOOL enable); + /** @abstract Draws interactive rich text. @discussion Backed by the code in TextExperiment folder, on top of CoreText. @@ -171,7 +185,7 @@ NS_ASSUME_NONNULL_BEGIN @param point The point, in the receiver's coordinate system. @param attributeNameOut The name of the attribute at the point. Can be NULL. @param rangeOut The ultimate range of the found text. Can be NULL. - @result YES if an entity exists at `point`; NO otherwise. + @result The entity if it exists at `point`; nil otherwise. */ - (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString * _Nullable * _Nullable)attributeNameOut range:(out NSRange * _Nullable)rangeOut AS_WARN_UNUSED_RESULT; @@ -212,7 +226,14 @@ NS_ASSUME_NONNULL_BEGIN @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; +@property BOOL passthroughNonlinkTouches; + +/** + @abstract Whether additionalTruncationMessage is interactive. + @discussion This affects whether touches on additionalTruncationMessage will be intercepted when + passthroughNonlinkTouches is YES. + */ +@property BOOL additionalTruncationMessageIsInteractive; /** @abstract Always handle tap truncationAction, even the passthroughNonlinkTouches is YES. Default is NO. diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index e268eca2d..429e83384 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -12,18 +12,89 @@ #import #import -#import -#import #import #import -#import +#import #import +#import +#import +#import +#import #import #import +#import +#import #import +using namespace AS; + +typedef void (^TextAttachmentUpdateBlock)(ASImageNode *imageNode); +void UpdateTextAttachmentForText(NSAttributedString *attributedString, + TextAttachmentUpdateBlock updateBlock); + +BOOL kTextNode2ImprovedRTL = false; +BOOL ASGetEnableTextNode2ImprovedRTL(void) { return kTextNode2ImprovedRTL; } +void ASSetEnableTextNode2ImprovedRTL(BOOL enable) { kTextNode2ImprovedRTL = enable; } + +BOOL kTextTruncationVisibleRange = false; +BOOL ASGetEnableTextTruncationVisibleRange(void) { return kTextTruncationVisibleRange; } +void ASSetEnableTextTruncationVisibleRange(BOOL enable) { kTextTruncationVisibleRange = enable; } + +// Provide a way for an ASAccessibilityElement to dispatch to the ASTextNode for its +// accessibilityFrame +@interface ASTextNode2 () + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement; + +@end + +/** + * Calculates the accessibility frame for a given ASAccessibilityElement, ASTextLayout and + * ASDisplayNode in the screen cooordinates space. This can be used for setting or + * providing an accessibility frame for the given ASAccessibilityElement. + */ +static CGRect ASTextNodeAccessiblityElementFrame(ASAccessibilityElement *element, + ASTextLayout *layout, + ASDisplayNode *containerNode) { + // This needs to be in the first non layer nodes coordinates space + containerNode = + containerNode ?: ASFindClosestViewOfLayer(element.node.layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + CGRect textLayoutFrame = CGRectZero; + NSRange accessibilityRange = element.accessibilityRange; + if (accessibilityRange.location == NSNotFound) { + // If no accessibilityRange was specified (as is done for the text element), just use the + // label's range and clamp to the visible range otherwise the returned rect would be invalid. + NSRange range = NSMakeRange(0, element.accessibilityLabel.length); + range = NSIntersectionRange(range, layout.visibleRange); + textLayoutFrame = [layout rectForRange:[ASTextRange rangeWithRange:range]]; + } else { + textLayoutFrame = [layout rectForRange:[ASTextRange rangeWithRange:accessibilityRange]]; + } + CGRect accessibilityFrame = [element.node convertRect:textLayoutFrame toNode:containerNode]; + return UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, containerNode.view); +} + +@interface ASTextNodeFrameProvider : NSObject +@end + +@implementation ASTextNodeFrameProvider + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + ASTextNode2 *textNode = ASDynamicCast(accessibilityElement.node, ASTextNode2); + if (textNode == nil) { + NSCAssert(NO, @"Only accessibility elements from ASTextNode are allowed."); + return CGRectZero; + } + + // Ask the passed in text node for the accessibilityFrame + return [textNode accessibilityFrameForAccessibilityElement:accessibilityElement]; +} + +@end + @interface ASTextCacheValue : NSObject { @package AS::Mutex _m; @@ -43,7 +114,8 @@ @implementation ASTextCacheValue #define AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS 0 /** - * If it can't find a compatible layout, this method creates one. + * Wraps a cache around a call to [ASTextLayout layoutWithContainer:text:]. + * [ASTextLayout layoutWithContainer:text:] creates a layout if it was not found in the cache. * * NOTE: Be careful to copy `text` if needed. */ @@ -59,11 +131,16 @@ @implementation ASTextCacheValue layoutCacheLock->lock(); ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; + + // Disable the cache if the text has attachments, since caching attachment content is expensive. + BOOL shouldCacheLayout = YES; if (cacheValue == nil) { + shouldCacheLayout = ![text as_hasAttribute:ASTextAttachmentAttributeName]; cacheValue = [[ASTextCacheValue alloc] init]; - [textLayoutCache setObject:cacheValue forKey:[text copy]]; + if (shouldCacheLayout) { + [textLayoutCache setObject:cacheValue forKey:[text copy]]; + } } - // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. AS::MutexLocker lock(cacheValue->_m); layoutCacheLock->unlock(); @@ -116,15 +193,19 @@ @implementation ASTextCacheValue } // Cache Miss. Compute the text layout. + ASSignpostStart(MeasureText, cacheValue, "%@", [text.string substringToIndex:MIN(text.length, 10)]); ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; + ASSignpostEnd(MeasureText, cacheValue, ""); // Store the result in the cache. { - // This is a critical section. However we also must hold the lock until this point, in case - // another thread requests this cache item while a layout is being calculated, so they don't race. - cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); - if (cacheValue->_layouts.size() > 3) { - cacheValue->_layouts.pop_back(); + if (shouldCacheLayout) { + // This is a critical section. However we also must hold the lock until this point, in case + // another thread requests this cache item while a layout is being calculated, so they don't race. + cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); + if (cacheValue->_layouts.size() > 3) { + cacheValue->_layouts.pop_back(); + } } } @@ -171,7 +252,9 @@ @implementation AS_TN2_CLASSNAME { ASTextNodeHighlightStyle _highlightStyle; BOOL _longPressCancelsTouches; BOOL _passthroughNonlinkTouches; - BOOL _alwaysHandleTruncationTokenTap; + BOOL _additionalTruncationMessageIsInteractive; + + NSMutableDictionary *_drawParameters; } @dynamic placeholderEnabled; @@ -197,7 +280,8 @@ - (instancetype)init // Disable user interaction for text node by default. self.userInteractionEnabled = NO; self.needsDisplayOnBoundsChange = YES; - + + _truncationMode = NSLineBreakByTruncatingTail; _textContainer.truncationType = ASTextTruncationTypeEnd; // The common case is for a text node to be non-opaque and blended over some background. @@ -207,7 +291,7 @@ - (instancetype)init self.linkAttributeNames = DefaultLinkAttributeNames(); // Accessibility - self.isAccessibilityElement = YES; + self.isAccessibilityElement = NO; self.accessibilityTraits = self.defaultAccessibilityTraits; // Placeholders @@ -223,6 +307,14 @@ - (instancetype)init - (void)dealloc { CGColorRelease(_shadowColor); + if (!ASDisplayNodeThreadIsMain()) { + if ([_attributedText as_hasAttribute:ASTextAttachmentAttributeName]) { + ASPerformMainThreadDeallocation(&_attributedText); + } + if ([_truncationAttributedText as_hasAttribute:ASTextAttachmentAttributeName]) { + ASPerformMainThreadDeallocation(&_truncationAttributedText); + } + } } #pragma mark - Description @@ -279,7 +371,7 @@ - (BOOL)supportsLayerBacking return NO; } - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // If the text contains any links, return NO. NSAttributedString *attributedText = _attributedText; NSRange range = NSMakeRange(0, attributedText.length); @@ -301,7 +393,7 @@ - (BOOL)supportsLayerBacking - (NSString *)defaultAccessibilityLabel { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _attributedText.string; } @@ -310,11 +402,123 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits return UIAccessibilityTraitStaticText; } +- (BOOL)isAccessibilityElement +{ + // If the ASTextNode2 should act as an UIAccessibilityContainer it has to return + // NO for isAccessibilityElement + return NO; +} + +- (NSInteger)accessibilityElementCount +{ + return self.accessibilityElements.count; +} + +// Returns the default ASTextNodeFrameProvider to be used as frame provider of text node's +// accessibility elements. +static ASTextNodeFrameProvider *ASTextNode2ASTextNodeFrameProviderDefault() { + static ASTextNodeFrameProvider *frameProvider = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + frameProvider = [[ASTextNodeFrameProvider alloc] init]; + }); + return frameProvider; +} + +- (NSArray *)accessibilityElements +{ + NSInteger attributedTextLength = _attributedText.length; + if (attributedTextLength == 0) { + return @[]; + } + + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; + + // Search the first node that is not layer backed + ASDisplayNode *containerNode = ASFindClosestViewOfLayer(self.layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + + // Create an accessibility element to represent the label's text. It's not necessary to specify + // a accessibilityRange here, as the entirety of the text is being represented. + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerNode.view]; + accessibilityElement.node = self; + accessibilityElement.accessibilityRange = NSMakeRange(NSNotFound, 0); + accessibilityElement.accessibilityIdentifier = self.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = self.accessibilityLabel; + accessibilityElement.accessibilityValue = self.accessibilityValue; + accessibilityElement.accessibilityTraits = self.accessibilityTraits; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = self.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = self.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = self.accessibilityAttributedValue; + } + accessibilityElement.frameProvider = ASTextNode2ASTextNodeFrameProviderDefault(); + [accessibilityElements addObject:accessibilityElement]; + + // Collect all links as accessiblity items + for (NSString *linkAttributeName in _linkAttributeNames) { + [_attributedText enumerateAttribute:linkAttributeName inRange:NSMakeRange(0, attributedTextLength) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + if (value == nil) { + return; + } + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:self]; + accessibilityElement.node = self; + accessibilityElement.accessibilityTraits = UIAccessibilityTraitLink; + accessibilityElement.accessibilityLabel = [_attributedText.string substringWithRange:range]; + accessibilityElement.accessibilityRange = range; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = + [_attributedText attributedSubstringFromRange:range]; + } + accessibilityElement.frameProvider = ASTextNode2ASTextNodeFrameProviderDefault(); + [accessibilityElements addObject:accessibilityElement]; + }]; + } + return accessibilityElements; +} + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + // Go up the tree to the top container node that contains the ASTextNode + // this is especially necessary if the text node is layer backed + ASDisplayNode *containerNode = ASFindClosestViewOfLayer(_layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + ASTextLayout *layout = + ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, _attributedText); + return ASTextNodeAccessiblityElementFrame(accessibilityElement, layout, containerNode); +} + +- (BOOL)performAccessibilityCustomActionLink:(UIAccessibilityCustomAction *)action +{ + NSCAssert(0 != (action.accessibilityTraits & UIAccessibilityTraitLink), @"Action needs to have UIAccessibilityTraitLink trait set"); + NSCAssert([action isKindOfClass:[ASAccessibilityCustomAction class]], @"Action needs to be of kind ASAccessibilityCustomAction"); + ASAccessibilityCustomAction *customAction = (ASAccessibilityCustomAction *)action; + + // In TextNode2 forward the link custom action to textNode:tappedLinkAttribute:value:atPoint:textRange: + // the default method that is available for link handling within ASTextNodeDelegate + if ([self.delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + // Convert from screen coordinates to the node coordinate space + CGPoint centerAccessibilityFrame = CGPointMake(CGRectGetMidX(customAction.accessibilityFrame), CGRectGetMidY(customAction.accessibilityFrame)); + CGPoint center = [self.supernode convertPoint:centerAccessibilityFrame fromNode:nil]; + [self.delegate textNode:(ASTextNode *)self tappedLinkAttribute:NSLinkAttributeName value:customAction.value atPoint:center textRange:customAction.textRange]; + return YES; + } + return NO; +} + #pragma mark - Layout and Sizing +- (void)layoutDidFinish { + [super layoutDidFinish]; + if (CGRectIsEmpty(self.bounds) && self.layer.needsDisplay) { + self.layer.contents = nil; + } +} + - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCustom(_textContainer.insets, textContainerInset, UIEdgeInsetsEqualToEdgeInsets)) { [self setNeedsLayout]; } @@ -333,7 +537,7 @@ - (void)setTextContainerLinePositionModifier:(id)mod - (id)textContainerLinePositionModifier { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _textContainer.linePositionModifier; } @@ -342,16 +546,25 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _textContainer.size = constrainedSize; [self _ensureTruncationText]; - + BOOL isCalculatingIntrinsicSize = NO; +#if YOGA + // In Yoga nodes, we cannot count on constrainedSize being infinite as we make AT_MOST measurements which receive fixed at-most values. + // However note the effect of this setting is to always left-align the text and report its actual used-width instead of the + // position of the far edge relative to the left: Which is behavior we always want under Yoga anyway. + if (_flags.yoga) { + isCalculatingIntrinsicSize = YES; + } +#endif // If the constrained size has a max/inf value on the text's forward direction, the text node is calculating its intrinsic size. // Need to consider both width and height when determining if it is calculating instrinsic size. Even the constrained width is provided, the height can be inf // it may provide a text that is longer than the width and require a wordWrapping line break mode and looking for the height to be calculated. - BOOL isCalculatingIntrinsicSize = (_textContainer.size.width >= ASTextContainerMaxSize.width) || (_textContainer.size.height >= ASTextContainerMaxSize.height); - + if (!isCalculatingIntrinsicSize) { + isCalculatingIntrinsicSize = (_textContainer.size.width >= ASTextContainerMaxSize.width) || (_textContainer.size.height >= ASTextContainerMaxSize.height); + } NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; [self prepareAttributedString:mutableText isForIntrinsicSize:isCalculatingIntrinsicSize]; ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, mutableText); @@ -362,6 +575,12 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize return layout.textBoundingSize; } +#if YOGA +- (float)yogaBaselineWithSize:(CGSize)size { + return ASTextGetBaseline(size.height, self.yogaParent, self.attributedText); +} +#endif + #pragma mark - Modifying User Text // Returns the ascender of the first character in attributedString by also including the line height if specified in paragraph style. @@ -381,19 +600,20 @@ + (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString - (NSAttributedString *)attributedText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _attributedText; } - (void)setAttributedText:(NSAttributedString *)attributedText { - if (attributedText == nil) { - attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + // Avoid copy / create for nil or zero-length arg. Treat them both as singleton zero. + if (attributedText.length == 0) { + attributedText = ASGetZeroAttributedString(); } // Many accessors in this method will acquire the lock (including ASDisplayNode methods). // Holding it for the duration of the method is more efficient in this case. - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); NSAttributedString *oldAttributedText = _attributedText; if (!ASCompareAssignCopy(_attributedText, attributedText)) { @@ -404,11 +624,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText [self _locked_invalidateTruncationText]; NSUInteger length = attributedText.length; +#if !YOGA if (length > 0) { ASLayoutElementStyle *style = [self _locked_style]; style.ascender = [[self class] ascenderWithAttributedString:attributedText]; style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender]; } +#endif // Tell the display node superclasses that the cached layout is incorrect now [self setNeedsLayout]; @@ -419,22 +641,23 @@ - (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); - } - #if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS [ASTextNode _registerAttributedText:_attributedText]; #endif + + if (self.isNodeLoaded) { + // Invalidate the accessibility elements for self as well as for the first accessibility + // container to requery the accessibility items for it + [self invalidateAccessibilityElements]; + [self invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } #pragma mark - Text Layout - (void)setExclusionPaths:(NSArray *)exclusionPaths { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _textContainer.exclusionPaths = exclusionPaths; [self setNeedsLayout]; @@ -443,14 +666,18 @@ - (void)setExclusionPaths:(NSArray *)exclusionPaths - (NSArray *)exclusionPaths { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _textContainer.exclusionPaths; } - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString isForIntrinsicSize:(BOOL)isForIntrinsicSize { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); NSLineBreakMode innerMode; + // `innerMode` is the truncation mode used by CoreText, within ASTextLayout. Since we are + // supplying our own truncation logic, we replace any actually-truncating modes with + // `NSLineBreakByWordWrapping` which just breaks down the lines for us. We still have the original + // truncationMode available to us from the ASTextContainer when we need it. switch (_truncationMode) { case NSLineBreakByWordWrapping: case NSLineBreakByCharWrapping: @@ -461,38 +688,75 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString is innerMode = NSLineBreakByWordWrapping; } - // Apply/Fix paragraph style if needed - [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { - - BOOL applyTruncationMode = YES; - NSMutableParagraphStyle *paragraphStyle = nil; - // Only "left" and "justified" alignments are supported while calculating intrinsic size. - // Other alignments like "right", "center" and "natural" cause the size to be bigger than needed and thus should be ignored/overridden. - const BOOL forceLeftAlignment = (style != nil - && isForIntrinsicSize - && style.alignment != NSTextAlignmentLeft - && style.alignment != NSTextAlignmentJustified); - if (style != nil) { - if (innerMode == style.lineBreakMode) { - applyTruncationMode = NO; - } - paragraphStyle = [style mutableCopy]; + // NOTE if there are no attributes, we still get called once with a `nil` style! + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + const NSLineBreakMode previousMode = style ? style.lineBreakMode : NSLineBreakByWordWrapping; + const BOOL applyTruncationMode = innerMode != previousMode; + const BOOL useNaturalAlignment = (!style || style.alignment == NSTextAlignmentNatural); + BOOL forceLeftAlignment = NO; + // This experiment launched so long ago but relies on semanticContentAttribute + if (kTextNode2ImprovedRTL) { + // To calculate intrinsic size, we generally always use NSTextAlignmentLeft (perhaps NSTextAlignmentJustified). + forceLeftAlignment = (isForIntrinsicSize + && (!style + || (style.alignment != NSTextAlignmentLeft + && style.alignment != NSTextAlignmentJustified))); } else { - if (innerMode == NSLineBreakByWordWrapping) { - applyTruncationMode = NO; - } - paragraphStyle = [NSMutableParagraphStyle new]; + forceLeftAlignment = (style != nil + && isForIntrinsicSize + && style.alignment != NSTextAlignmentLeft + && style.alignment != NSTextAlignmentJustified); } - if (!applyTruncationMode && !forceLeftAlignment) { + + if (!applyTruncationMode && !forceLeftAlignment && !useNaturalAlignment) { return; } + NSMutableParagraphStyle *paragraphStyle = + [style mutableCopy] ?: [[NSMutableParagraphStyle alloc] init]; paragraphStyle.lineBreakMode = innerMode; - if (applyTruncationMode) { - paragraphStyle.lineBreakMode = _truncationMode; - } if (forceLeftAlignment) { paragraphStyle.alignment = NSTextAlignmentLeft; + } else if (useNaturalAlignment) { +#if YOGA + if (!kTextNode2ImprovedRTL) { +#endif + if (AS_AVAILABLE_IOS(10)) { + switch (self.primitiveTraitCollection.layoutDirection) { + case UITraitEnvironmentLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UITraitEnvironmentLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + case UITraitEnvironmentLayoutDirectionUnspecified: + break; + } + } else { + NSNumber *layoutDirection = ASApplicationUserInterfaceLayoutDirection(); + if (layoutDirection) { + switch (static_cast([layoutDirection integerValue])) { + case UIUserInterfaceLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UIUserInterfaceLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + } + } + } +#if YOGA + } else { + switch ([self yogaLayoutDirection]) { + case UIUserInterfaceLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UIUserInterfaceLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + } + } +#endif } [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; }]; @@ -517,34 +781,28 @@ - (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]; + MutexLocker l(__instanceLock__); + [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]; + // Unlike layout, here we must copy the container since drawing is asynchronous. + ASTextContainer *copiedContainer = [_textContainer copy]; - [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; - needsTintColor = self.textColorFollowsTintColor && mutableText.length > 0; - bgColor = self.backgroundColor ?: [NSNull null]; + // Some unit tests set insets directly, so don't override them with zero padding + if (!UIEdgeInsetsEqualToEdgeInsets(self.paddings, UIEdgeInsetsZero)) { + copiedContainer.insets = self.paddings; } - + copiedContainer.size = self.bounds.size; + [copiedContainer makeImmutable]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + // After all other attributes are set, apply tint color if needed and foreground color is not already specified - if (needsTintColor) { + if (self.textColorFollowsTintColor && mutableText.length > 0) { // 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 @@ -552,11 +810,16 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer } } - return @{ - @"container": copiedContainer, - @"text": mutableText, - @"bgColor": bgColor - }; + ASTextLayout *layout = + ASTextNodeCompatibleLayoutWithContainerAndText(copiedContainer, mutableText); + if (!_drawParameters) { + _drawParameters = [[NSMutableDictionary alloc] init]; + } + _drawParameters[@"container"] = copiedContainer; + _drawParameters[@"text"] = mutableText; + _drawParameters[@"bgColor"] = self.backgroundColor ?: [NSNull null]; + _drawParameters[@"layout"] = layout ?: [NSNull null]; + return [_drawParameters copy]; } + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing @@ -639,14 +902,19 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // 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 *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, mutableText); + // Start by checking whether there is any "direct" hit of additionalTruncationMessage. This + // ensures that additionalTruncationMessage still receives the touch if any link's expanded touch + // area overlaps it. if ([self _locked_pointInsideAdditionalTruncationMessage:point withLayout:layout]) { if (inAdditionalTruncationMessageOut != NULL) { *inAdditionalTruncationMessageOut = YES; @@ -655,28 +923,60 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point } NSRange visibleRange = layout.visibleRange; + BOOL enableImprovedTextTruncationVisibleRange = ASGetEnableImprovedTextTruncationVisibleRange(); + BOOL enableTextTruncationVisibleRange = ASGetEnableTextTruncationVisibleRange(); + + if (_truncationAttributedText != nil && [self isTruncated]) { + // Make sure the touch area doesn't include the area of the truncated tokens when the text + // is truncated. + if (enableImprovedTextTruncationVisibleRange) { + // The link attribute should only be fetched if the point is inside the uncovered part of + // the text. This is to make sure tapping on the truncation token will not fire any link + // attributes. + if (layout.truncatedLineBeforeTruncationToken && + CGRectContainsPoint(layout.truncatedLine.bounds, point) && + !CGRectContainsPoint(layout.truncatedLineBeforeTruncationToken.bounds, point)) { + return nil; + } + if (layout.truncatedLineBeforeTruncationTokenRange.location != NSNotFound) { + // The index before the truncated tokens. + NSUInteger truncatedLineBeforeTruncationTokenEnd = + layout.truncatedLineBeforeTruncationTokenRange.location + + layout.truncatedLineBeforeTruncationTokenRange.length; + visibleRange = NSMakeRange(visibleRange.location, + truncatedLineBeforeTruncationTokenEnd - visibleRange.location); + } + } else if (enableTextTruncationVisibleRange) { + visibleRange.length -= _truncationAttributedText.length; + } + } NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); - // Search the 9 points of a 44x44 square around the touch until we find a link. + // Search the 17 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] = { + static constexpr CGSize kRectOffsets[] = { { 0, 0 }, + { -11, 0 }, { 11, 0 }, { -22, 0 }, { 22, 0 }, + { 0, -11 }, { 0, 11 }, { 0, -22 }, { 0, 22 }, + { -11, -11 }, { -11, 11 }, { -22, -22 }, { -22, 22 }, + { 11, -11 }, { 11, 11 }, { 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 textRangeAtPoint:testPoint]; + if (!range || !NSLocationInRange(range.start.offset, clampedRange)) { continue; } + for (NSString *attributeName in _linkAttributeNames) { NSRange effectiveRange = NSMakeRange(0, 0); - id value = [_attributedText attribute:attributeName atIndex:pos.offset + id value = [_attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; if (value == nil) { // Didn't find any links specified with this attribute. @@ -691,7 +991,9 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point continue; } - *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); + if (rangeOut != NULL) { + *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); + } if (attributeNameOut != NULL) { *attributeNameOut = attributeName; @@ -701,6 +1003,20 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point } } + // If there is no link, we can safely check whether the touch lands in the expanded touch + // area of additionalTruncationMessage. + if (_additionalTruncationMessage) { + for (const CGSize &offset : kRectOffsets) { + const CGPoint testPoint = CGPointMake(point.x + offset.width, point.y + offset.height); + if ([self _locked_pointInsideAdditionalTruncationMessage:testPoint withLayout:layout]) { + if (inAdditionalTruncationMessageOut != NULL) { + *inAdditionalTruncationMessageOut = YES; + } + return nil; + } + } + } + return nil; } @@ -710,21 +1026,22 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout BOOL inAdditionalTruncationMessage = NO; CTLineRef truncatedCTLine = layout.truncatedLine.CTLine; - if (truncatedCTLine != NULL && _additionalTruncationMessage != nil) { + if (truncatedCTLine != NULL && _additionalTruncationMessage != nil && + CGRectContainsPoint(layout.truncatedLine.bounds, point)) { CFIndex stringIndexForPosition = CTLineGetStringIndexForPosition(truncatedCTLine, point); if (stringIndexForPosition != kCFNotFound) { CFIndex truncatedCTLineGlyphCount = CTLineGetGlyphCount(truncatedCTLine); 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; - + if (additionalTruncationTokenLine) { CFRelease(additionalTruncationTokenLine); } @@ -744,14 +1061,14 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout if ((firstTruncatedTokenIndex + truncationTokenLineGlyphCount) < stringIndexForPosition && stringIndexForPosition < (firstTruncatedTokenIndex + composedTruncationTextLineGlyphCount)) { inAdditionalTruncationMessage = YES; - } + } break; } case ASTextTruncationTypeEnd: { if (stringIndexForPosition > (truncatedCTLineGlyphCount - additionalTruncationTokenLineGlyphCount)) { inAdditionalTruncationMessage = YES; } - break; + break; } default: // For now, assume that a tap inside this text, but outside the text range is a tap on the @@ -763,7 +1080,7 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout } } } - + return inAdditionalTruncationMessage; } @@ -772,7 +1089,7 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. if (gestureRecognizer == _longPressGestureRecognizer) { // Don't allow long press on truncation message @@ -806,21 +1123,21 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer - (ASTextNodeHighlightStyle)highlightStyle { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _highlightStyle; } - (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _highlightStyle = highlightStyle; } - (NSRange)highlightRange { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _highlightRange; } @@ -838,7 +1155,7 @@ - (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. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. // Set these so that link tapping works. _highlightedLinkAttributeName = highlightedAttributeName; @@ -906,7 +1223,7 @@ - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *) ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(textContainerCopy, _attributedText); NSArray *highlightRects = [layout selectionRectsWithoutStartAndEndForRange:[ASTextRange rangeWithRange:highlightRange]]; - NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; + NSMutableArray *converted = [[NSMutableArray alloc] initWithCapacity:highlightRects.count]; CALayer *layer = self.layer; UIEdgeInsets shadowPadding = self.shadowPadding; @@ -924,7 +1241,10 @@ - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *) ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; - overlayLayer.frame = highlightTargetLayer.bounds; + CGRect frame = highlightTargetLayer.bounds; + frame.origin.x += self.paddings.left; + frame.origin.y += self.paddings.top; + overlayLayer.frame = frame; overlayLayer.masksToBounds = NO; overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; [highlightTargetLayer addSublayer:overlayLayer]; @@ -1007,7 +1327,7 @@ - (UIColor *)placeholderColor - (void)setPlaceholderColor:(UIColor *)placeholderColor { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { self.placeholderEnabled = CGColorGetAlpha(placeholderColor.CGColor) > 0; } @@ -1024,15 +1344,11 @@ - (UIImage *)placeholderImage - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _passthroughNonlinkTouches and _alwaysHandleTruncationTokenTap ivars. + MutexLocker l(__instanceLock__); // Protect usage of ivars. if (!_passthroughNonlinkTouches) { return [super pointInside:point withEvent:event]; } - - if (_alwaysHandleTruncationTokenTap) { - return YES; - } NSRange range = NSMakeRange(0, 0); NSString *linkAttributeName = nil; @@ -1043,7 +1359,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event range:&range inAdditionalTruncationMessage:&inAdditionalTruncationMessage forHighlighting:YES]; - + if (_additionalTruncationMessageIsInteractive && inAdditionalTruncationMessage) { + return YES; + } + NSUInteger lastCharIndex = NSIntegerMax; BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); @@ -1078,7 +1397,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event if (inAdditionalTruncationMessage) { NSRange visibleRange = NSMakeRange(0, 0); { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // 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 *containerCopy = [_textContainer copy]; @@ -1109,7 +1428,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event ASDisplayNodeAssertMainThread(); [super touchesEnded:touches withEvent:event]; - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. id delegate = self.delegate; if ([self _pendingLinkTap] && [delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { CGPoint point = [[touches anyObject] locationInView:self.view]; @@ -1130,7 +1449,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event ASDisplayNodeAssertMainThread(); [super touchesMoved:touches withEvent:event]; - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. UITouch *touch = [touches anyObject]; CGPoint locationInView = [touch locationInView:self.view]; // on 3D Touch enabled phones, this gets fired with changes in force, and usually will get fired immediately after touchesBegan:withEvent: @@ -1160,7 +1479,7 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { id delegate = self.delegate; if ([delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; [delegate textNode:(ASTextNode *)self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; } @@ -1169,7 +1488,7 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer - (BOOL)_pendingLinkTap { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && self.delegate != nil; } @@ -1179,18 +1498,26 @@ - (BOOL)_pendingTruncationTap return [ASLockedSelf(_highlightedLinkAttributeName) isEqualToString:ASTextNodeTruncationTokenAttributeName]; } -- (BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - return _alwaysHandleTruncationTokenTap; +- (BOOL)passthroughNonlinkTouches { + MutexLocker l(__instanceLock__); + return _passthroughNonlinkTouches; } -- (void)setAlwaysHandleTruncationTokenTap:(BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - _alwaysHandleTruncationTokenTap = alwaysHandleTruncationTokenTap; +- (void)setPassthroughNonlinkTouches:(BOOL)passthroughNonlinkTouches { + MutexLocker l(__instanceLock__); + _passthroughNonlinkTouches = passthroughNonlinkTouches; } - + +- (BOOL)additionalTruncationMessageIsInteractive { + MutexLocker l(__instanceLock__); + return _additionalTruncationMessageIsInteractive; +} + +- (void)setAdditionalTruncationMessageIsInteractive:(BOOL)additionalTruncationMessageIsInteractive { + MutexLocker l(__instanceLock__); + _additionalTruncationMessageIsInteractive = additionalTruncationMessageIsInteractive; +} + #pragma mark - Shadow Properties /** @@ -1206,7 +1533,7 @@ - (CGColorRef)shadowColor - (void)setShadowColor:(CGColorRef)shadowColor { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); @@ -1221,7 +1548,7 @@ - (CGSize)shadowOffset - (void)setShadowOffset:(CGSize)shadowOffset { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCustom(_shadowOffset, shadowOffset, CGSizeEqualToSize)) { [self setNeedsDisplay]; } @@ -1234,7 +1561,7 @@ - (CGFloat)shadowOpacity - (void)setShadowOpacity:(CGFloat)shadowOpacity { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_shadowOpacity, shadowOpacity)) { [self setNeedsDisplay]; } @@ -1247,7 +1574,7 @@ - (CGFloat)shadowRadius - (void)setShadowRadius:(CGFloat)shadowRadius { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_shadowRadius, shadowRadius)) { [self setNeedsDisplay]; } @@ -1262,7 +1589,7 @@ - (UIEdgeInsets)shadowPadding - (void)setPointSizeScaleFactors:(NSArray *)scaleFactors { AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_pointSizeScaleFactors, scaleFactors)) { [self setNeedsLayout]; } @@ -1275,19 +1602,9 @@ - (void)setPointSizeScaleFactors:(NSArray *)scaleFactors #pragma mark - Truncation Message -static NSAttributedString *DefaultTruncationAttributedString() -{ - static NSAttributedString *defaultTruncationAttributedString; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - defaultTruncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; - }); - return defaultTruncationAttributedString; -} - - (void)_ensureTruncationText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (_textContainer.truncationToken == nil) { _textContainer.truncationToken = [self _locked_composedTruncationText]; } @@ -1300,7 +1617,7 @@ - (NSAttributedString *)truncationAttributedText - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { [self _invalidateTruncationText]; } @@ -1313,7 +1630,7 @@ - (NSAttributedString *)additionalTruncationMessage - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { [self _invalidateTruncationText]; } @@ -1326,7 +1643,7 @@ - (NSLineBreakMode)truncationMode - (void)setTruncationMode:(NSLineBreakMode)truncationMode { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_truncationMode, truncationMode)) { ASTextTruncationType truncationType; switch (truncationMode) { @@ -1351,19 +1668,31 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode - (BOOL)isTruncated { - return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine != nil); + AS::MutexLocker l(__instanceLock__); + ASTextLayout *layout = [self locked_textLayoutForSize:[self _locked_threadSafeBounds].size]; + return !NSEqualRanges(layout.visibleRange, layout.range); } - (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize { - return ASLockedSelf([self locked_textLayoutForSize:constrainedSize.max].truncatedLine != nil); + AS::MutexLocker l(__instanceLock__); + ASTextLayout *layout = [self locked_textLayoutForSize:constrainedSize.max]; + return !NSEqualRanges(layout.visibleRange, layout.range); } - (ASTextLayout *)locked_textLayoutForSize:(CGSize)size { - ASTextContainer *container = [_textContainer copy]; - container.size = size; - return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText); + ASTextContainer *container; + if (!CGSizeEqualToSize(_textContainer.size, size)) { + container = [_textContainer copy]; + container.size = size; + [container makeImmutable]; + } else { + container = _textContainer; + } + NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + return ASTextNodeCompatibleLayoutWithContainerAndText(container, mutableText); } - (NSUInteger)maximumNumberOfLines @@ -1374,7 +1703,7 @@ - (NSUInteger)maximumNumberOfLines - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_textContainer.maximumNumberOfRows, maximumNumberOfLines)) { [self setNeedsDisplay]; } @@ -1382,16 +1711,15 @@ - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines - (NSUInteger)lineCount { - ASLockScopeSelf(); - AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - return 0; + MutexLocker l(__instanceLock__); + return ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, _attributedText).rowCount; } #pragma mark - Truncation Message - (void)_invalidateTruncationText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); [self _locked_invalidateTruncationText]; [self setNeedsDisplay]; } @@ -1407,7 +1735,7 @@ - (void)_locked_invalidateTruncationText */ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // Check if we even have an additional truncation message. if (!_additionalTruncationMessage) { @@ -1441,36 +1769,8 @@ - (NSAttributedString *)_locked_composedTruncationText composedTruncationText = _truncationAttributedText; } else if (_additionalTruncationMessage != nil) { composedTruncationText = _additionalTruncationMessage; - } else { - composedTruncationText = DefaultTruncationAttributedString(); } - return [self _locked_prepareTruncationStringForDrawing:composedTruncationText]; -} - -/** - * - cleanses it of core text attributes so TextKit doesn't crash - * - Adds whole-string attributes so the truncation message matches the styling - * of the body text - */ -- (NSAttributedString *)_locked_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString -{ - DISABLED_ASAssertLocked(__instanceLock__); - NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; - // Grab the attributes from the full string - if (_attributedText.length > 0) { - NSAttributedString *originalString = _attributedText; - NSInteger originalStringLength = _attributedText.length; - // Add any of the original string's attributes to the truncation string, - // but don't overwrite any of the truncation string's attributes - NSDictionary *originalStringAttributes = [originalString attributesAtIndex:originalStringLength-1 effectiveRange:NULL]; - [truncationString enumerateAttributesInRange:NSMakeRange(0, truncationString.length) options:0 usingBlock: - ^(NSDictionary *attributes, NSRange range, BOOL *stop) { - NSMutableDictionary *futureTruncationAttributes = [originalStringAttributes mutableCopy]; - [futureTruncationAttributes addEntriesFromDictionary:attributes]; - [truncationMutableString setAttributes:futureTruncationAttributes range:range]; - }]; - } - return truncationMutableString; + return composedTruncationText; } #if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS @@ -1509,4 +1809,26 @@ - (BOOL)usingExperiment return YES; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { + UpdateTextAttachmentForText(self.attributedText, ^(ASImageNode *imageNode) { + [imageNode exitInterfaceState:oldState]; + [imageNode enterInterfaceState:newState]; + }); + [super interfaceStateDidChange:newState fromState:oldState]; +} + @end + +void UpdateTextAttachmentForText(NSAttributedString *attributedString, + TextAttachmentUpdateBlock updateBlock) { + [attributedString + enumerateAttribute:ASTextAttachmentAttributeName + inRange:NSMakeRange(0, attributedString.length) + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(ASTextAttachment *asTextAttachment, NSRange range, BOOL *_Nonnull stop) { + if (ASImageNode *node = ASDynamicCast(asTextAttachment.content, ASImageNode)) { + updateBlock(node); + } + }]; +} + diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 7741e4447..6a8237f76 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -41,6 +41,7 @@ @implementation _ASDisplayView NSArray *_accessibilityElements; CGRect _lastAccessibilityElementsFrame; + BOOL _inIsAccessibilityElement; } #pragma mark - Class diff --git a/Source/Details/_ASDisplayViewAccessiblity.h b/Source/Details/_ASDisplayViewAccessiblity.h index 7f159d1d0..1b1e3155b 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.h +++ b/Source/Details/_ASDisplayViewAccessiblity.h @@ -7,7 +7,10 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import + +NS_ASSUME_NONNULL_BEGIN + // WARNING: When dealing with accessibility elements, please use the `accessibilityElements` // property instead of the older methods e.g. `accessibilityElementCount()`. While the older methods @@ -15,6 +18,51 @@ // their correctness. For details, see // https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements +@class ASDisplayNode; +@class ASAccessibilityElement; + +/** + * The methods adopted by the object to provide frame information for a given + * ASAccessibilityElement + */ +@protocol ASAccessibilityElementFrameProviding + +/** + * Returns the accessibilityFrame for the given ASAccessibilityElement + */ +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement; + +@end + +/** + * Encapsulates Texture related information about an item that should be + * accessible to users with disabilities, but that isn’t accessible by default. + */ +@interface ASAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) NSRange accessibilityRange; + +/** + * If a frameProvider is set on the ASAccessibilityElement it will be asked to + * return the frame for the corresponding UIAccessibilityElement within + * accessibilityElement. + * + * @note: If a frameProvider is set any accessibilityFrame set on the + * UIAccessibilityElement explicitly will be ignored + */ +@property (nonatomic) id frameProvider; + +@end + +@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction + +@property (nonatomic, readonly) ASDisplayNode *node; +@property (nonatomic, nullable, readonly) id value; +@property (nonatomic, readonly) NSRange textRange; + +@end + // After recusively collecting all of the accessibility elements of a node, they get sorted. This sort determines // the order that a screen reader will traverse the elements. By default, we sort these elements based on their // origin: lower y origin comes first, then lower x origin. If 2 nodes have an equal origin, the node with the smaller @@ -28,3 +76,5 @@ typedef NSComparisonResult (^ASSortAccessibilityElementsComparator)(NSObject *, // Use this method to supply your own custom sort comparator used to determine the order of the accessibility elements void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator); + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 8f3e12991..fad6712dc 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -9,19 +9,76 @@ #ifndef ASDK_ACCESSIBILITY_DISABLE -#import #import +#import #import #import #import #import #import #import +#import +#import #import +/// Returns if the passed in node is considered a leaf node +NS_INLINE BOOL ASIsLeafNode(__unsafe_unretained ASDisplayNode *node) { + return node.subnodes.count == 0; +} + +/// Returns an NSString trimmed of whitespaces and newlines at the beginning the end. +static NSString *ASTrimmedAccessibilityLabel(NSString *accessibilityLabel) { + return [accessibilityLabel + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; +} + +/// Returns a NSAttributedString trimmed of whitespaces and newlines at the beginning and the end. +static NSAttributedString *ASTrimmedAttributedAccessibilityLabel( + NSAttributedString *attributedString) { + // Create a cached inverted character set from whitespaceAndNewlineCharacterSet + // [NSCharacterSet whitespaceAndNewlineCharacterSet] is cached, but the invertedSet is not. + static NSCharacterSet *invertedWhiteSpaceAndNewLineCharacterSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + invertedWhiteSpaceAndNewLineCharacterSet = + [NSCharacterSet whitespaceAndNewlineCharacterSet].invertedSet; + }); + NSString *string = attributedString.string; + + NSRange range = [string rangeOfCharacterFromSet:invertedWhiteSpaceAndNewLineCharacterSet]; + NSUInteger location = (range.length > 0) ? range.location : 0; + + range = [string rangeOfCharacterFromSet:invertedWhiteSpaceAndNewLineCharacterSet + options:NSBackwardsSearch]; + NSUInteger length = (range.length > 0) ? NSMaxRange(range) - location : string.length - location; + + if (location == 0 && length == string.length) { + return attributedString; + } + + return [attributedString attributedSubstringFromRange:NSMakeRange(location, length)]; +} + +/// Returns NO when implicit custom action synthesis should not be enabled for the node. Returns YES +/// when implicit custom action synthesis is OK for the node, assuming it contains an non-empty +/// accessibility label. +static BOOL ASMayImplicitlySynthesizeAccessibilityCustomAction(ASDisplayNode *node, + ASDisplayNode *rootContainerNode) { + if (node == rootContainerNode) { + return NO; + } + return node.accessibilityTraits & ASInteractiveAccessibilityTraitsMask(); +} + #pragma mark - UIAccessibilityElement +@protocol ASAccessibilityElementPositioning + +@property (nonatomic, readonly) CGRect accessibilityFrame; + +@end + static ASSortAccessibilityElementsComparator currentAccessibilityComparator = nil; static ASSortAccessibilityElementsComparator defaultAccessibilityComparator = nil; @@ -70,14 +127,56 @@ static CGRect ASAccessibilityFrameForNode(ASDisplayNode *node) { return [layer convertRect:node.bounds toLayer:ASFindWindowOfLayer(layer).layer]; } -@interface ASAccessibilityElement : UIAccessibilityElement +@interface _ASDisplayViewAccessibilityFrameProvider : NSObject +@end -@property (nonatomic) ASDisplayNode *node; +@implementation _ASDisplayViewAccessibilityFrameProvider + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + return ASAccessibilityFrameForNode(accessibilityElement.node); +} + +@end + +@interface ASAccessibilityElement () + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node; @end +// Returns the default _ASDisplayViewAccessibilityFrameProvider to be used as frame provider +// of accessibility elements within ASDisplayViewAccessibility. +static _ASDisplayViewAccessibilityFrameProvider *_ASDisplayViewAccessibilityFrameProviderDefault() { + static _ASDisplayViewAccessibilityFrameProvider *frameProvider = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + frameProvider = [[_ASDisplayViewAccessibilityFrameProvider alloc] init]; + }); + return frameProvider; +} + +// Create an ASAccessibilityElement for a given UIView and ASDisplayNode for usage +// within _ASDisplayViewAccessibility +static ASAccessibilityElement *_ASDisplayViewAccessibilityCreateASAccessibilityElement( + UIView *containerView, ASDisplayNode *node) { + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerView]; + accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = node.accessibilityLabel; + accessibilityElement.accessibilityHint = node.accessibilityHint; + accessibilityElement.accessibilityValue = node.accessibilityValue; + accessibilityElement.accessibilityTraits = node.accessibilityTraits; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; + } + accessibilityElement.node = node; + accessibilityElement.frameProvider = _ASDisplayViewAccessibilityFrameProviderDefault(); + + return accessibilityElement; +} + @implementation ASAccessibilityElement + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node @@ -100,19 +199,34 @@ + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)containe - (CGRect)accessibilityFrame { - return ASAccessibilityFrameForNode(self.node); + if (_frameProvider) { + return [_frameProvider accessibilityFrameForAccessibilityElement:self]; + } + + return [super accessibilityFrame]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p, %@, %@>", NSStringFromClass([self class]), self, + self.accessibilityLabel, + NSStringFromCGRect(self.accessibilityFrame)]; } @end #pragma mark - _ASDisplayView / UIAccessibilityContainer -@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction +@interface ASAccessibilityCustomAction () @property (nonatomic) ASDisplayNode *node; +@property (nonatomic, nullable) id value; +@property (nonatomic) NSRange textRange; @end +@interface ASAccessibilityCustomAction() +@end + @implementation ASAccessibilityCustomAction - (CGRect)accessibilityFrame @@ -122,141 +236,221 @@ - (CGRect)accessibilityFrame @end -/// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container -static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) +#pragma mark - Collecting Accessibility with ASTextNode Links Handling + +/// Collect all subnodes for the given node by walking down the subnode tree and calculates the +/// screen coordinates based on the containerNode and container. This is necessary for layer backed +/// nodes or rasterrized subtrees as no UIView instance for this node exists. +static void CollectAccessibilityElementsForLayerBackedOrRasterizedNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); - + + // Iterate any node in the tree and either collect nodes that are accessibility elements + // or leaf nodes that are accessibility containers ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) { - // For every subnode that is layer backed or it's supernode has subtree rasterization enabled - // we have to create a UIAccessibilityElement as no view for this node exists - if (currentNode != containerNode && currentNode.isAccessibilityElement) { - UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode]; - [elements addObject:accessibilityElement]; + if (currentNode != containerNode) { + if (currentNode.isAccessibilityElement) { + // For every subnode that is an accessibility element and is layer backed + // or an ancestor has subtree rasterization enabled, create a + // UIAccessibilityElement as no view for this node exists + UIAccessibilityElement *accessibilityElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(container, currentNode); + [elements addObject:accessibilityElement]; + } else if (ASIsLeafNode(currentNode) && currentNode.accessibilityElementCount > 0) { + // In leaf nodes that are layer backed and acting as UIAccessibilityContainer + // (isAccessibilityElement == NO we call through to the + // accessibilityElements to collect all accessibility elements of this node + [elements addObjectsFromArray:currentNode.accessibilityElements]; + } } }); } -static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, - NSMutableArray *elements) { - ASDisplayNodeCAssertNotNil(view, @"Passed in view should not be nil"); - if (view == nil) { +/// Called from CollectAccessibilityElements for nodes that are returning YES for +/// isAccessibilityContainer to collect all subnodes accessibility labels as well as custom actions +/// for nodes that have interactive accessibility traits enabled. Furthermore for ASTextNode's it +/// also aggregates all links within the attributedString as custom action +static void AggregateSubtreeAccessibilityLabelsAndCustomActions(ASDisplayNode *rootContainer, + ASDisplayNode *containerNode, + UIView *containerView, + NSMutableArray *elements) { + ASDisplayNodeCAssertNotNil(containerView, @"Passed in view should not be nil"); + if (containerView == nil) { return; } UIAccessibilityElement *accessiblityElement = - [ASAccessibilityElement accessibilityElementWithContainer:view - node:container]; - + _ASDisplayViewAccessibilityCreateASAccessibilityElement(containerView, containerNode); + NSMutableArray *labeledNodes = [[NSMutableArray alloc] init]; NSMutableArray *actions = [[NSMutableArray alloc] init]; std::queue queue; - queue.push(container); - + queue.push(containerNode); + // If the container does not have an accessibility label set, or if the label is meant for custom - // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden + // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overridden // value and do not perform the aggregation. BOOL shouldAggregateSubnodeLabels = - (container.accessibilityLabel.length == 0) || - (container.accessibilityTraits & ASInteractiveAccessibilityTraitsMask()); - + (ASTrimmedAccessibilityLabel(containerNode.accessibilityLabel).length == 0) || + ASMayImplicitlySynthesizeAccessibilityCustomAction(containerNode, rootContainer); + + // Iterate through the whole subnode tree and aggregate ASDisplayNode *node = nil; while (!queue.empty()) { node = queue.front(); queue.pop(); - - if (node != container && node.isAccessibilityContainer) { - UIView *containerView = node.isLayerBacked ? view : node.view; - CollectAccessibilityElementsForContainer(node, containerView, elements); + + // If the node is an accessibility container go further down for collecting all the nodes information. + if (node != containerNode && node.isAccessibilityContainer) { + UIView *view = containerNode.isLayerBacked ? containerView : containerNode.view; + AggregateSubtreeAccessibilityLabelsAndCustomActions(node, node, view, elements); continue; } - - if (node.accessibilityLabel.length > 0) { - if (node.accessibilityTraits & ASInteractiveAccessibilityTraitsMask()) { - ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; + + + // Aggregate either custom actions for specific accessibility traits or the accessibility labels + // of the node. + NSString *trimmedNodeAccessibilityLabel = ASTrimmedAccessibilityLabel(node.accessibilityLabel); + if (trimmedNodeAccessibilityLabel.length > 0) { + if (ASMayImplicitlySynthesizeAccessibilityCustomAction(node, rootContainer)) { + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] + initWithName:trimmedNodeAccessibilityLabel + target:node + selector:@selector(performAccessibilityCustomAction:)]; action.node = node; [actions addObject:action]; - + + // Connect the node with the custom action which representing it. node.accessibilityCustomAction = action; - } else if (node == container || shouldAggregateSubnodeLabels) { - ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node]; + } else if (node == containerNode || shouldAggregateSubnodeLabels) { + ASAccessibilityElement *nonInteractiveElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(containerView, node); [labeledNodes addObject:nonInteractiveElement]; + + // For ASTextNode accessibility container besides aggregating all of the of the subnodes + // we are also collecting all of the link as custom actions. + NSAttributedString *attributedText = nil; + if ([node respondsToSelector:@selector(attributedText)]) { + attributedText = ((ASTextNode *)node).attributedText; + } + NSArray *linkAttributeNames = nil; + if ([node respondsToSelector:@selector(linkAttributeNames)]) { + linkAttributeNames = ((ASTextNode *)node).linkAttributeNames; + } + linkAttributeNames = linkAttributeNames ?: @[]; + + for (NSString *linkAttributeName in linkAttributeNames) { + [attributedText enumerateAttribute:linkAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + if (value == nil) { + return; + } + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:[attributedText.string substringWithRange:range] target:node selector:@selector(performAccessibilityCustomActionLink:)]; + action.accessibilityTraits = UIAccessibilityTraitLink; + action.node = node; + action.value = value; + action.textRange = range; + [actions addObject:action]; + }]; + } } } - + for (ASDisplayNode *subnode in node.subnodes) { queue.push(subnode); } } - + SortAccessibilityElements(labeledNodes); - + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { - NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; - NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; - [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if (idx != 0) { - [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + NSAttributedString *attributedAccessbilityLabelsDivider = + [[NSAttributedString alloc] initWithString:@", "]; + NSMutableAttributedString *attributedAccessibilityLabel = + [[NSMutableAttributedString alloc] init]; + [labeledNodes enumerateObjectsUsingBlock:^(ASAccessibilityElement *_Nonnull element, + NSUInteger idx, BOOL *_Nonnull stop) { + NSAttributedString *trimmedAttributedLabel = + ASTrimmedAttributedAccessibilityLabel(element.accessibilityAttributedLabel); + if (trimmedAttributedLabel.length == 0) { + return; + } + if (idx != 0 && attributedAccessibilityLabel.length != 0) { + [attributedAccessibilityLabel appendAttributedString:attributedAccessbilityLabelsDivider]; } - [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + [attributedAccessibilityLabel appendAttributedString:trimmedAttributedLabel]; }]; - accessiblityElement.accessibilityAttributedLabel = attributedLabel; + accessiblityElement.accessibilityAttributedLabel = attributedAccessibilityLabel; } else { - NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; - accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + NSMutableString *accessibilityLabel = [[NSMutableString alloc] init]; + [labeledNodes enumerateObjectsUsingBlock:^(ASAccessibilityElement *_Nonnull element, + NSUInteger idx, BOOL *_Nonnull stop) { + NSString *trimmedAccessibilityLabel = ASTrimmedAccessibilityLabel(element.accessibilityLabel); + if (trimmedAccessibilityLabel.length == 0) { + return; + } + if (idx != 0 && accessibilityLabel.length != 0) { + [accessibilityLabel appendString:@", "]; + } + [accessibilityLabel appendString:trimmedAccessibilityLabel]; + }]; + accessiblityElement.accessibilityLabel = accessibilityLabel; } - + SortAccessibilityElements(actions); accessiblityElement.accessibilityCustomActions = actions; - + [elements addObject:accessiblityElement]; } /// Check if a view is a subviews of an UIScrollView. This is used to determine whether to enforce that /// accessibility elements must be on screen static BOOL recusivelyCheckSuperviewsForScrollView(UIView *view) { - if (!view) { - return NO; - } else if ([view isKindOfClass:[UIScrollView class]]) { - return YES; - } - return recusivelyCheckSuperviewsForScrollView(view.superview); + if (!view) { + return NO; + } else if ([view isKindOfClass:[UIScrollView class]]) { + return YES; + } + return recusivelyCheckSuperviewsForScrollView(view.superview); } /// returns YES if this node should be considered "hidden" from the screen reader. static BOOL nodeIsHiddenFromAcessibility(ASDisplayNode *node) { - return node.isHidden || node.alpha == 0.0 || node.accessibilityElementsHidden; + if (ASActivateExperimentalFeature(ASExperimentalEnableNodeIsHiddenFromAcessibility)) { + return node.isHidden || node.alpha == 0.0 || node.accessibilityElementsHidden; + } + return NO; } /// Collect all accessibliity elements for a given view and view node -static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements) +static void CollectAccessibilityElementsWithTextNodeLinkHandling(ASDisplayNode *node, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); ASDisplayNodeCAssertFalse(node.isLayerBacked); if (node.isLayerBacked) { return; } - + BOOL anySubNodeIsCollection = (nil != ASDisplayNodeFindFirstNode(node, - ^BOOL(ASDisplayNode *nodeToCheck) { + ^BOOL(ASDisplayNode *nodeToCheck) { return ASDynamicCast(nodeToCheck, ASCollectionNode) != nil || - ASDynamicCast(nodeToCheck, ASTableNode) != nil; + ASDynamicCast(nodeToCheck, ASTableNode) != nil; })); - + UIView *view = node.view; // If we don't have a window, let's just bail out if (!view.window) { return; } - + + // Handle an accessibility container (collects accessibility labels or custom actions) if (node.isAccessibilityContainer && !anySubNodeIsCollection) { - CollectAccessibilityElementsForContainer(node, view, elements); + AggregateSubtreeAccessibilityLabelsAndCustomActions(node, node, node.view, elements); return; } - - // Handle rasterize case + + // Handle a node which tree is rasterized to collect all accessibility elements if (node.rasterizesSubtree) { - CollectUIAccessibilityElementsForNode(node, node, view, elements); + CollectAccessibilityElementsForLayerBackedOrRasterizedNode(node, node, node.view, elements); return; } @@ -287,82 +481,193 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el // If a subnode is outside of the view's window, exclude it UNLESS it is a subview of an UIScrollView. // In this case UIKit will return the element even if it is outside of the window or the scrollView's visible rect (contentOffset + contentSize) CGRect nodeInWindowCoords = [node convertRect:subnode.frame toNode:nil]; - if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords) && !recusivelyCheckSuperviewsForScrollView(view)) { + if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords) && !recusivelyCheckSuperviewsForScrollView(view) && ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { continue; } if (subnode.isAccessibilityElement) { // An accessiblityElement can either be a UIView or a UIAccessibilityElement if (subnode.isLayerBacked) { - // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node - UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode]; + // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement + // that represents this node + UIAccessibilityElement *accessiblityElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(node.view, subnode); [elements addObject:accessiblityElement]; } else { - // Accessiblity element is not layer backed just add the view as accessibility element + // Accessiblity element is not layer backed, add the view to the elements as _ASDisplayView + // is itself a UIAccessibilityContainer [elements addObject:subnode.view]; } } else if (subnode.isLayerBacked) { - // Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement - CollectUIAccessibilityElementsForNode(subnode, node, view, elements); + // Go down the hierarchy for layer backed subnodes which are also UIAccessibilityContainer's + // and collect all of the UIAccessibilityElement + CollectAccessibilityElementsForLayerBackedOrRasterizedNode(subnode, node, node.view, elements); } else if (subnode.accessibilityElementCount > 0) { - // UIView is itself a UIAccessibilityContainer just add it + // _ASDisplayView is itself a UIAccessibilityContainer just add it, UIKit will call the + // accessiblity methods of the nodes _ASDisplayView [elements addObject:subnode.view]; } } } +#pragma mark - _ASDisplayView + +@interface _ASDisplayView () { + NSArray *_accessibilityElements; + BOOL _inIsAccessibilityElement; +} + +@end + @implementation _ASDisplayView (UIAccessibilityContainer) -#pragma mark - UIAccessibility +#pragma mark UIAccessibility + +- (BOOL)isAccessibilityElement +{ + ASDisplayNodeAssertMainThread(); + if (_inIsAccessibilityElement) { + return [super isAccessibilityElement]; + } + _inIsAccessibilityElement = YES; + BOOL isAccessibilityElement = [self.asyncdisplaykit_node isAccessibilityElement]; + _inIsAccessibilityElement = NO; + return isAccessibilityElement; +} - (void)setAccessibilityElements:(nullable NSArray *)accessibilityElements { - // this is a no-op. You should not be setting accessibilityElements directly on _ASDisplayView. - // if you wish to set accessibilityElements, do so in your node. UIKit will call _ASDisplayView's - // accessibilityElements which will in turn ask its node for its elements. + ASDisplayNodeAssertMainThread(); + _accessibilityElements = accessibilityElements; } - (nullable NSArray *)accessibilityElements { ASDisplayNodeAssertMainThread(); - + ASDisplayNode *viewNode = self.asyncdisplaykit_node; if (viewNode == nil) { return @[]; } - + // we no longer cache accessibilityElements. When caching, in order to provide correct element when items become hidden/visible // we had to manually clear _accessibilityElements. This seemed like a heavy burden to place on a user, and one that is also // not immediately obvious. While recomputing accessibilityElements may be expensive, this will only affect users that have // voice over enabled (we checked to ensure performance did not suffer by not caching for an overall user base). For those // users with voice over on, being correct is almost certainly more important than being performant. - return [viewNode accessibilityElements]; + if (_accessibilityElements == nil || ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements)) { + _accessibilityElements = [viewNode accessibilityElements]; + } + return _accessibilityElements; +} + +@end + +@implementation ASDisplayNode (CustomAccessibilityBehavior) + +- (void)setAccessibilityElementsBlock:(ASDisplayNodeAccessibilityElementsBlock)block { + AS::MutexLocker l(__instanceLock__); + _accessibilityElementsBlock = block; } @end @implementation ASDisplayNode (AccessibilityInternal) -- (nullable NSArray *)accessibilityElements +- (BOOL)isAccessibilityElement +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access isAccessibilityElement since node is not loaded"); + return [super isAccessibilityElement]; + } + + return [_view isAccessibilityElement]; +} + +- (NSInteger)accessibilityElementCount +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementCount since node is not loaded"); + return 0; + } + + // Please Note! + // If accessibility is not enabled on a device or the Accessibility Inspector was not started + // once yet on a Mac this method will always return 0! UIKit will dynamically link in + // specific accessibility implementation methods in this cases. + return [_view accessibilityElementCount]; +} + +- (NSArray *)accessibilityElements { - // NSObject implements the informal accessibility protocol. This means that all ASDisplayNodes already have an accessibilityElements - // property. If an ASDisplayNode subclass has explicitly set the property, let's use that instead of traversing the node tree to try - // to create the elements automatically - NSArray *elements = [super accessibilityElements]; - if (elements.count) { - return elements; + if (ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { + // NSObject implements the informal accessibility protocol. This means that all ASDisplayNodes already have an accessibilityElements + // property. If an ASDisplayNode subclass has explicitly set the property, let's use that instead of traversing the node tree to try + // to create the elements automatically + NSArray *elements = [super accessibilityElements]; + if (elements.count) { + return elements; + } } if (!self.isNodeLoaded) { ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded"); - return nil; + return ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil) ? nil : @[]; + } + if (_accessibilityElementsBlock) { + return _accessibilityElementsBlock(); } + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; - CollectAccessibilityElements(self, accessibilityElements); + CollectAccessibilityElementsWithTextNodeLinkHandling(self, accessibilityElements); + SortAccessibilityElements(accessibilityElements); // If we did not find any accessibility elements, return nil instead of empty array. This allows a WKWebView within the node // to participate in accessibility. - return accessibilityElements.count == 0 ? nil : accessibilityElements; + if (ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { + return accessibilityElements.count == 0 ? nil : accessibilityElements; + } else { + return accessibilityElements; + } +} + +- (void)invalidateFirstAccessibilityContainerOrNonLayerBackedNode { + if (!ASAccessibilityIsEnabled()) { + return; + } + ASDisplayNode *firstNonLayerbackedNode = nil; + BOOL containerInvalidated = [self invalidateUpToContainer:&firstNonLayerbackedNode]; + if (!self.isLayerBacked) { + return; + } + if (!containerInvalidated) { + [firstNonLayerbackedNode invalidateAccessibilityElements]; + } +} + +// Walks up the tree and until the first node that returns YES for isAccessibilityContainer is found +// and invalidates it's accessibility elements and YES will be returned. +// In case no node that returns YES for isAccessibilityContainer the first non layer backed node +// will be returned with the firstNonLayerbackedNode pointer and NO will be returned. +- (BOOL)invalidateUpToContainer:(ASDisplayNode **)firstNonLayerbackedNode { + ASDisplayNode *supernode = self.supernode; + if (supernode.isAccessibilityContainer) { + if (supernode.isNodeLoaded) { + [supernode invalidateAccessibilityElements]; + return YES; + } + } + if (*firstNonLayerbackedNode == nil && !self.isLayerBacked) { + *firstNonLayerbackedNode = self; + } + if (!supernode) { + return NO; + } + return [self.supernode invalidateUpToContainer:firstNonLayerbackedNode]; +} + +- (void)invalidateAccessibilityElements { + self.accessibilityElements = nil; } @end diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index b7e8b1538..53432f023 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -320,8 +320,43 @@ NS_INLINE UIAccessibilityTraits ASInteractiveAccessibilityTraitsMask() { return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; } +NS_INLINE BOOL ASAccessibilityIsEnabled() { +#if DEBUG + return true; +#else + if (UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning()) { + return true; + } + // In some ui test environment where DEBUG is not defined. + dispatch_once(&kShouldEnableAccessibilityForTestingOnceToken, ^{ + shouldEnableAccessibilityForTesting = [[[NSProcessInfo processInfo] arguments] + containsObject:@"AS_FORCE_ACCESSIBILITY_FOR_TESTING"]; + }); + return shouldEnableAccessibilityForTesting; +#endif +} + @interface ASDisplayNode (AccessibilityInternal) + +/** + * @discussion An array of the accessibility elements from the node. + */ - (nullable NSArray *)accessibilityElements; + +/** + * @discussion Invalidates the cached accessibility elements for the node + */ +- (void)invalidateAccessibilityElements; + +/** + * @discussion Invalidates the accessibility elements for the first accessibility container or + * the first non layer backed node by walking up the tree starting by self. + * + * @note Call this when a layer backed node changed (added/removed/updated) or + * a view in an accessibility container changed. + */ +- (void)invalidateFirstAccessibilityContainerOrNonLayerBackedNode; + @end; @interface UIView (ASDisplayNodeInternal) diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index b9f5f40d4..9675ddd72 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -253,6 +253,7 @@ static constexpr CACornerMask kASCACornerAllCorners = CGPoint _accessibilityActivationPoint; UIBezierPath *_accessibilityPath; + ASDisplayNodeAccessibilityElementsBlock _accessibilityElementsBlock; // Safe Area support // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 29fa26897..2f5918c98 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -17,11 +17,21 @@ NS_ASSUME_NONNULL_BEGIN +@interface NSAttributedString (ASTextAttachment) + +- (BOOL)as_hasAttribute:(NSAttributedStringKey)attributeKey; + +@end + ASDK_EXTERN void ASInitializeFrameworkMainThread(void); ASDK_EXTERN BOOL ASDefaultAllowsGroupOpacity(void); ASDK_EXTERN BOOL ASDefaultAllowsEdgeAntialiasing(void); +/// ASTraitCollection is probably a better place to look on iOS >= 10 +/// This _may not be set_ if AS_INITIALIZE_FRAMEWORK_MANUALLY is not set or we are used by an extension +ASDK_EXTERN NSNumber *ASApplicationUserInterfaceLayoutDirection(void); + ASDK_EXTERN BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); ASDK_EXTERN BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector); @@ -119,6 +129,8 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASImageDownloaderPriority ASImageDown */ ASDK_EXTERN NSMutableSet *ASCreatePointerBasedMutableSet(void); +ASDK_EXTERN NSAttributedString *ASGetZeroAttributedString(void); + NS_ASSUME_NONNULL_END #ifndef AS_INITIALIZE_FRAMEWORK_MANUALLY diff --git a/Source/Private/ASInternalHelpers.mm b/Source/Private/ASInternalHelpers.mm index 4d6e1fde8..e648ea992 100644 --- a/Source/Private/ASInternalHelpers.mm +++ b/Source/Private/ASInternalHelpers.mm @@ -16,6 +16,24 @@ static NSNumber *allowsGroupOpacityFromUIKitOrNil; static NSNumber *allowsEdgeAntialiasingFromUIKitOrNil; +static NSNumber *applicationUserInterfaceLayoutDirection = nil; + +@implementation NSAttributedString (ASTextAttachment) + +- (BOOL)as_hasAttribute:(NSAttributedStringKey)attributeKey { + NSUInteger length = self.length; + if (length == 0) { + return NO; + } + NSRange range; + id result = [self attribute:attributeKey + atIndex:0 + longestEffectiveRange:&range + inRange:NSMakeRange(0, length)]; + return result || range.length != length; +} + +@end BOOL ASDefaultAllowsGroupOpacity() { @@ -39,6 +57,10 @@ BOOL ASDefaultAllowsEdgeAntialiasing() return edgeAntialiasing; } +NSNumber *ASApplicationUserInterfaceLayoutDirection() { + return applicationUserInterfaceLayoutDirection; +} + #if AS_SIGNPOST_ENABLE void _ASInitializeSignpostObservers(void) { @@ -66,6 +88,7 @@ void ASInitializeFrameworkMainThread(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ASDisplayNodeCAssertMainThread(); + applicationUserInterfaceLayoutDirection = @([UIApplication sharedApplication].userInterfaceLayoutDirection); // Ensure these values are cached on the main thread before needed in the background. if (ASActivateExperimentalFeature(ASExperimentalLayerDefaults)) { // Nop. We will gather default values on-demand in ASDefaultAllowsGroupOpacity and ASDefaultAllowsEdgeAntialiasing @@ -259,3 +282,12 @@ - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath }); return (__bridge_transfer NSMutableSet *)CFSetCreateMutable(NULL, 0, &callbacks); } + +NSAttributedString *ASGetZeroAttributedString(void) { + static NSAttributedString *str; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + str = [[NSAttributedString alloc] init]; + }); + return str; +} diff --git a/Source/TextExperiment/Component/ASTextDebugOption.mm b/Source/TextExperiment/Component/ASTextDebugOption.mm index 2565b903c..9887e1ad7 100644 --- a/Source/TextExperiment/Component/ASTextDebugOption.mm +++ b/Source/TextExperiment/Component/ASTextDebugOption.mm @@ -6,7 +6,7 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASTextDebugOption.h" +#import #import static pthread_mutex_t _sharedDebugLock; diff --git a/Source/TextExperiment/Component/ASTextLayout.h b/Source/TextExperiment/Component/ASTextLayout.h index 55ac417f1..58d70c632 100644 --- a/Source/TextExperiment/Component/ASTextLayout.h +++ b/Source/TextExperiment/Component/ASTextLayout.h @@ -10,9 +10,9 @@ #import #import -#import "ASTextDebugOption.h" -#import "ASTextLine.h" -#import "ASTextInput.h" +#import +#import +#import @protocol ASTextLinePositionModifier; @@ -23,6 +23,19 @@ NS_ASSUME_NONNULL_BEGIN */ ASDK_EXTERN const CGSize ASTextContainerMaxSize; +/** + * Get and Set ASTextNode2 to enable the better calculation of visible text range. + */ +BOOL ASGetEnableImprovedTextTruncationVisibleRange(void); +void ASSetEnableImprovedTextTruncationVisibleRange(BOOL enable); + +/** + * Get and Set ASTextLayout to fix the clickable area on truncation token when the last visible line + * is untruncated (e.g. the last visible line is an empty line). + */ +BOOL ASGetEnableImprovedTextTruncationVisibleRangeLastLineFix(void); +void ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(BOOL enable); + /** The ASTextContainer class defines a region in which text is laid out. ASTextLayout class uses one or more ASTextContainer objects to generate layouts. @@ -82,9 +95,6 @@ ASDK_EXTERN const CGSize ASTextContainerMaxSize; /// Default is YES; @property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd; -/// Whether the text is vertical form (may used for CJK text layout). Default is NO. -@property (getter=isVerticalForm) BOOL verticalForm; - /// Maximum number of rows, 0 means no limit. Default is 0. @property NSUInteger maximumNumberOfRows; @@ -224,8 +234,13 @@ ASDK_EXTERN const CGSize ASTextContainerMaxSize; @property (nonatomic, readonly) CTFrameRef frame; ///< Array of `ASTextLine`, no truncated @property (nonatomic, readonly) NSArray *lines; -///< ASTextLine with truncated token, or nil +///< ASTextLine with truncated token, or nil. Note that this may be nil if no truncation token was specified. +///< To check if the entire string was drawn, use NSEqualRanges(visibleRange, range). @property (nullable, nonatomic, readonly) ASTextLine *truncatedLine; +///< Range of the truncated line before the truncation tokens +@property(nonatomic, readonly) NSRange truncatedLineBeforeTruncationTokenRange; +///< The part of truncatedLine before the truncation token. +@property(nullable, nonatomic, readonly) ASTextLine *truncatedLineBeforeTruncationToken; ///< Array of `ASTextAttachment` @property (nullable, nonatomic, readonly) NSArray *attachments; ///< Array of NSRange(wrapped by NSValue) in text diff --git a/Source/TextExperiment/Component/ASTextLayout.mm b/Source/TextExperiment/Component/ASTextLayout.mm index f01c25908..e3bed8d28 100644 --- a/Source/TextExperiment/Component/ASTextLayout.mm +++ b/Source/TextExperiment/Component/ASTextLayout.mm @@ -9,15 +9,21 @@ #import -#import #import -#import +#import +#import +#import +#import +#import +#import #import +#import #import -#import const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; +NSAttributedString *fillBaseAttributes(NSAttributedString *str, NSDictionary *attrs); + typedef struct { CGFloat head; CGFloat foot; @@ -55,22 +61,29 @@ static CGColorRef ASTextGetCGColor(CGColorRef color) { return color; } +BOOL kTextNode2ImprovedTextTruncationVisibleRange = false; +BOOL ASGetEnableImprovedTextTruncationVisibleRange(void) { + return kTextNode2ImprovedTextTruncationVisibleRange; +} +void ASSetEnableImprovedTextTruncationVisibleRange(BOOL enable) { + kTextNode2ImprovedTextTruncationVisibleRange = enable; +} + +BOOL kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix = false; +BOOL ASGetEnableImprovedTextTruncationVisibleRangeLastLineFix(void) { + return kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix; +} +void ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(BOOL enable) { + kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix = enable; +} + @implementation ASTextLinePositionSimpleModifier - (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(ASTextContainer *)container { - if (container.verticalForm) { - for (NSUInteger i = 0, max = lines.count; i < max; i++) { - ASTextLine *line = lines[i]; - CGPoint pos = line.position; - pos.x = container.size.width - container.insets.right - line.row * _fixedLineHeight - _fixedLineHeight * 0.9; - line.position = pos; - } - } else { - for (NSUInteger i = 0, max = lines.count; i < max; i++) { - ASTextLine *line = lines[i]; - CGPoint pos = line.position; - pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top; - line.position = pos; - } + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGPoint pos = line.position; + pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top; + line.position = pos; } } @@ -286,14 +299,6 @@ - (void)setPathLineWidth:(CGFloat)pathLineWidth { Setter(_pathLineWidth = pathLineWidth); } -- (BOOL)isVerticalForm { - Getter(BOOL v = _verticalForm) return v; -} - -- (void)setVerticalForm:(BOOL)verticalForm { - Setter(_verticalForm = verticalForm); -} - - (NSUInteger)maximumNumberOfRows { Getter(NSUInteger num = _maximumNumberOfRows) return num; } @@ -342,6 +347,8 @@ @interface ASTextLayout () @property (nonatomic) CTFrameRef frame; @property (nonatomic) NSArray *lines; @property (nonatomic) ASTextLine *truncatedLine; +@property(nonatomic) NSRange truncatedLineBeforeTruncationTokenRange; +@property(nonatomic) ASTextLine *truncatedLineBeforeTruncationToken; @property (nonatomic) NSArray *attachments; @property (nonatomic) NSArray *attachmentRanges; @property (nonatomic) NSArray *attachmentRects; @@ -395,13 +402,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; } -+ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container + text:(NSAttributedString *)text range:(NSRange)range { ASTextLayout *layout = NULL; CGPathRef cgPath = nil; CGRect cgPathBox = {0}; - BOOL isVerticalForm = NO; BOOL rowMaySeparated = NO; - NSMutableDictionary *frameAttrs = nil; CTFramesetterRef ctSetter = NULL; CTFrameRef ctFrame = NULL; CFArrayRef ctLines = nil; @@ -414,7 +420,10 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri NSMutableSet *attachmentContentsSet = nil; BOOL needTruncation = NO; NSAttributedString *truncationToken = nil; + NSMutableAttributedString *lastLineText = nil; ASTextLine *truncatedLine = nil; + NSRange truncatedLineBeforeTruncationTokenRange; + ASTextLine *truncatedLineBeforeTruncationToken; ASRowEdge *lineRowsEdge = NULL; NSUInteger *lineRowsIndex = NULL; NSRange visibleRange; @@ -435,30 +444,32 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (range.location + range.length > text.length) return nil; [container makeImmutable]; maximumNumberOfRows = container.maximumNumberOfRows; - - // It may use larger constraint size when create CTFrame with - // CTFramesetterCreateFrame in iOS 10. + + // Amazingly, nobody ever figured out _what_ the supposed layoutSizeBug on iOS 10 was, or as + // follows from that, whether it actually ever got fixed in, say, iOS 11. + // However, there is a lot of functionality now that assumes this is YES (and presumably is broken + // on iOS 9-). Thus, this flag should probably just be removed to simplify this code. BOOL needFixLayoutSizeBug = AS_AT_LEAST_IOS10; layout = [[ASTextLayout alloc] _init]; layout.text = text; layout.container = container; layout.range = range; - isVerticalForm = container.verticalForm; - - // set cgPath and cgPathBox + + // `exclusionPaths` are like `verticalForm` in that their behavior hasn't been verified in a long + // time, though I think they have a much better chance of still working. + // TODO Verify or remove ASTextLayout exclusionPaths if (container.path == nil && container.exclusionPaths.count == 0) { if (container.size.width <= 0 || container.size.height <= 0) FAIL_AND_RETURN CGRect rect = (CGRect) {CGPointZero, container.size }; + // Note from above, `needFixLayoutSizeBug` should really always be YES if (needFixLayoutSizeBug) { + // Thus, `contraintSizeIsExtended` is always YES. And thus we always set the main-axis + // constraint to "infinity"/maxSize: constraintSizeIsExtended = YES; constraintRectBeforeExtended = UIEdgeInsetsInsetRect(rect, container.insets); constraintRectBeforeExtended = CGRectStandardize(constraintRectBeforeExtended); - if (container.isVerticalForm) { - rect.size.width = ASTextContainerMaxSize.width; - } else { - rect.size.height = ASTextContainerMaxSize.height; - } + rect.size.height = ASTextContainerMaxSize.height; } rect = UIEdgeInsetsInsetRect(rect, container.insets); rect = CGRectStandardize(rect); @@ -486,7 +497,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [layout.container.exclusionPaths enumerateObjectsUsingBlock: ^(UIBezierPath *onePath, NSUInteger idx, BOOL *stop) { CGPathAddPath(path, NULL, onePath.CGPath); }]; - + cgPathBox = CGPathGetPathBoundingBox(path); CGAffineTransform trans = CGAffineTransformMakeScale(1, -1); CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans); @@ -496,19 +507,31 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri cgPath = path; } if (!cgPath) FAIL_AND_RETURN - + + // With no exclusion or container paths, we got a rectangular "path" `cgPath` from above. As + // noted, we are probably about to do a whole bunch of wrong things if we did not. + + // BEGIN CTFramesetter cache code. There are two KEY LINEs in the below section, the rest of it is + // code to support caching of CTFramesetters, which are very expensive to create. + // frame setter config - frameAttrs = [[NSMutableDictionary alloc] init]; - if (container.isPathFillEvenOdd == NO) { - frameAttrs[(id)kCTFramePathFillRuleAttributeName] = @(kCTFramePathFillWindingNumber); - } - if (container.pathLineWidth > 0) { - frameAttrs[(id)kCTFramePathWidthAttributeName] = @(container.pathLineWidth); - } - if (container.isVerticalForm == YES) { - frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft); - } - + NSDictionary *frameAttrs = ({ + static constexpr NSUInteger kMaxAttrCount = 3; + NSUInteger count = 0; + id keys[kMaxAttrCount]; + id objects[kMaxAttrCount]; + if (container.isPathFillEvenOdd == NO) { + keys[count] = (id)kCTFramePathFillRuleAttributeName; + objects[count++] = @(kCTFramePathFillWindingNumber); + } + if (container.pathLineWidth > 0) { + keys[count] = (id)kCTFramePathWidthAttributeName; + objects[count++] = @(container.pathLineWidth); + } + // If you add new attributes, make sure to update kMaxAttrCount above. + count ? [[NSDictionary alloc] initWithObjects:objects forKeys:keys count:count] : nil; + }); + /* * Framesetter cache. * Framesetters can only be used by one thread at a time. @@ -555,10 +578,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri // Create a framesetter if needed. if (!ctSetter) { + // KEY LINE #1 (See above) ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); } if (!ctSetter) FAIL_AND_RETURN + // KEY LINE #2 (See above) ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFDictionaryRef)frameAttrs); // Return to cache. @@ -575,6 +600,11 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } if (!ctFrame) FAIL_AND_RETURN + + // END CTFramesetterCache code + + // Now (Step 2?) we figure out where the individual lines are: CoreText already did this, we are + // just extracting them from the ctFrame reference. lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); lineCount = CFArrayGetCount(ctLines); @@ -583,17 +613,13 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (lineOrigins == NULL) FAIL_AND_RETURN CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins); } - + CGRect textBoundingRect = CGRectZero; CGSize textBoundingSize = CGSizeZero; NSInteger rowIdx = -1; NSUInteger rowCount = 0; CGRect lastRect = CGRectMake(0, -FLT_MAX, 0, 0); CGPoint lastPosition = CGPointMake(0, -FLT_MAX); - if (isVerticalForm) { - lastRect = CGRectMake(FLT_MAX, 0, 0, 0); - lastPosition = CGPointMake(FLT_MAX, 0); - } // calculate line frame NSUInteger lineCurrentIdx = 0; @@ -602,7 +628,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CTLineRef ctLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, i); CFArrayRef ctRuns = CTLineGetGlyphRuns(ctLine); if (!ctRuns || CFArrayGetCount(ctRuns) == 0) continue; - + // CoreText coordinate system CGPoint ctLineOrigin = lineOrigins[i]; @@ -610,8 +636,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CGPoint position; position.x = cgPathBox.origin.x + ctLineOrigin.x; position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y; - - ASTextLine *line = [ASTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm]; + + ASTextLine *line = [ASTextLine lineWithCTLine:ctLine position:position vertical:NO]; [lines addObject:line]; } @@ -619,46 +645,35 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri // Give user a chance to modify the line's position. [container.linePositionModifier modifyLines:lines fromText:text inContainer:container]; + // We treat the first line differently: BOOL first = YES; for (ASTextLine *line in lines) { - CGPoint position = line.position; - CGRect rect = line.bounds; + CGPoint linePosition = line.position; + CGRect lineBounds = line.bounds; if (constraintSizeIsExtended) { - if (isVerticalForm) { - if (rect.origin.x + rect.size.width > - constraintRectBeforeExtended.origin.x + - constraintRectBeforeExtended.size.width) { - measuringBeyondConstraints = YES; - } - } else { - if (rect.origin.y + rect.size.height > - constraintRectBeforeExtended.origin.y + - constraintRectBeforeExtended.size.height) { - measuringBeyondConstraints = YES; - } + if (lineBounds.origin.y + lineBounds.size.height > + constraintRectBeforeExtended.origin.y + + constraintRectBeforeExtended.size.height) { + // To support truncation, we will sometimes keep laying out rows even though we have gone + // past the (vertical) constraints. + measuringBeyondConstraints = YES; } } + // rowIdx (and rowCount) ONLY ADVANCE for rows that are within constraints. BOOL newRow = !measuringBeyondConstraints; - if (newRow && rowMaySeparated && position.x != lastPosition.x) { - if (isVerticalForm) { - if (rect.size.width > lastRect.size.width) { - if (rect.origin.x > lastPosition.x && lastPosition.x > rect.origin.x - rect.size.width) newRow = NO; - } else { - if (lastRect.origin.x > position.x && position.x > lastRect.origin.x - lastRect.size.width) newRow = NO; - } + if (newRow && rowMaySeparated && linePosition.x != lastPosition.x) { + if (lineBounds.size.height > lastRect.size.height) { + // Are this line's bounds entirely above the last line's? If so, it's not a new row + if (lineBounds.origin.y < lastPosition.y && lastPosition.y < lineBounds.origin.y + lineBounds.size.height) newRow = NO; } else { - if (rect.size.height > lastRect.size.height) { - if (rect.origin.y < lastPosition.y && lastPosition.y < rect.origin.y + rect.size.height) newRow = NO; - } else { - if (lastRect.origin.y < position.y && position.y < lastRect.origin.y + lastRect.size.height) newRow = NO; - } + if (lastRect.origin.y < linePosition.y && linePosition.y < lastRect.origin.y + lastRect.size.height) newRow = NO; } } if (newRow) rowIdx++; - lastRect = rect; - lastPosition = position; + lastRect = lineBounds; + lastPosition = linePosition; line.index = lineCurrentIdx; line.row = rowIdx; @@ -668,16 +683,26 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (first) { first = NO; - textBoundingRect = rect; - } else if (!measuringBeyondConstraints) { + textBoundingRect = lineBounds; + } + // We do a couple of checks: If we are within our given constraints and maximumNumberOfRows, + // we will expand the overall `textBoundingRect`. + else if (!measuringBeyondConstraints) { if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) { - textBoundingRect = CGRectUnion(textBoundingRect, rect); + textBoundingRect = CGRectUnion(textBoundingRect, lineBounds); } } } + // BEGIN Truncation code. + { + // We expect to remove lines if we truncate, but we will want them later. NSMutableArray *removedLines = [NSMutableArray new]; + + // There are two main reasons to truncate: We exceed the given bounds, or we exceed the given + // `maximumNumberOfRows`. The first one we mostly figured out above. Here we now check for the + // `maximumNumberOfRows` limit: if (rowCount > 0) { if (maximumNumberOfRows > 0) { if (rowCount > maximumNumberOfRows) { @@ -692,8 +717,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } while (1); } } - ASTextLine *lastLine = rowCount < lines.count ? lines[rowCount - 1] : lines.lastObject; + + // `rowCount` is currently set to how many rows fit our constraints. Our last line might be + // either the last line, or just the last one that fit (< rowCount). + ASTextLine *lastLine = rowCount < lines.count ? lines[rowCount - 1] : lines.lastObject; if (!needTruncation && lastLine.range.location + lastLine.range.length < text.length) { + // lastLine doesn't go to the end of the text: ie it has been truncated. needTruncation = YES; while (lines.count > rowCount) { ASTextLine *line = lines.lastObject; @@ -718,21 +747,11 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } lastRowIdx = line.row; lineRowsIndex[lastRowIdx] = i; - if (isVerticalForm) { - lastHead = rect.origin.x + rect.size.width; - lastFoot = lastHead - rect.size.width; - } else { - lastHead = rect.origin.y; - lastFoot = lastHead + rect.size.height; - } + lastHead = rect.origin.y; + lastFoot = lastHead + rect.size.height; } else { - if (isVerticalForm) { - lastHead = MAX(lastHead, rect.origin.x + rect.size.width); - lastFoot = MIN(lastFoot, rect.origin.x); - } else { - lastHead = MIN(lastHead, rect.origin.y); - lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height); - } + lastHead = MIN(lastHead, rect.origin.y); + lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height); } } lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot}; @@ -744,94 +763,76 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } } - { // calculate bounding size - CGRect rect = textBoundingRect; - if (container.path) { - if (container.pathLineWidth > 0) { - CGFloat inset = container.pathLineWidth / 2; - rect = CGRectInset(rect, -inset, -inset); - } - } else { - rect = UIEdgeInsetsInsetRect(rect, ASTextUIEdgeInsetsInvert(container.insets)); - } - rect = CGRectStandardize(rect); - CGSize size = rect.size; - if (container.verticalForm) { - size.width += container.size.width - (rect.origin.x + rect.size.width); - } else { - size.width += rect.origin.x; - } - size.height += rect.origin.y; - if (size.width < 0) size.width = 0; - if (size.height < 0) size.height = 0; - size.width = ceil(size.width); - size.height = ceil(size.height); - textBoundingSize = size; - } - visibleRange = ASTextNSRangeFromCFRange(CTFrameGetVisibleStringRange(ctFrame)); if (needTruncation) { ASTextLine *lastLine = lines.lastObject; NSRange lastRange = lastLine.range; - visibleRange.length = lastRange.location + lastRange.length - visibleRange.location; + visibleRange.length = NSMaxRange(lastRange) - visibleRange.location; // create truncated line if (container.truncationType != ASTextTruncationTypeNone) { + CTLineTruncationType type = kCTLineTruncationEnd; + if (container.truncationType == ASTextTruncationTypeStart) { + type = kCTLineTruncationStart; + } else if (container.truncationType == ASTextTruncationTypeMiddle) { + type = kCTLineTruncationMiddle; + } + CTLineRef truncationTokenLine = NULL; - if (container.truncationToken) { - truncationToken = container.truncationToken; - truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + truncationToken = container.truncationToken; + // For Tail truncation, if the truncation token is empty or only whitespace, we simply take + // whatever the Framesetter already generated and go with that (e.g. last word is omitted.) + static dispatch_once_t nonWhitespaceCharacterSetOnce; + static NSCharacterSet *nonWhitespaceCharacterSet; + dispatch_once(&nonWhitespaceCharacterSetOnce, ^{ + nonWhitespaceCharacterSet = NSCharacterSet.whitespaceCharacterSet.invertedSet; + }); + // Truncation token is all whitespace, or just an empty string. + if (truncationToken && type == kCTLineTruncationEnd && NSNotFound == [truncationToken.string rangeOfCharacterFromSet:nonWhitespaceCharacterSet].location) { + // Don't do anything. Reset truncationToken to nil, leave truncationTokenLine as NULL. + truncationToken = nil; } else { + // Create a CTLine from truncationToken. By default, apply the attributes from the end of + // the last line to the new truncation token. CFArrayRef runs = CTLineGetGlyphRuns(lastLine.CTLine); NSUInteger runCount = CFArrayGetCount(runs); + NSMutableAttributedString *string = + [[NSMutableAttributedString alloc] initWithString:ASTextTruncationToken]; NSMutableDictionary *attrs = nil; if (runCount > 0) { - CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex(runs, runCount - 1); - attrs = (id) CTRunGetAttributes(run); - attrs = attrs ? attrs.mutableCopy : [NSMutableArray new]; + + // Get last line run + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, runCount - 1); + + // Attributes from last run + attrs = (id)CTRunGetAttributes(run); + attrs = attrs ? [attrs mutableCopy] : [[NSMutableDictionary alloc] init]; [attrs removeObjectsForKeys:[NSMutableAttributedString as_allDiscontinuousAttributeKeys]]; - CTFontRef font = (__bridge CTFontRef) attrs[(id) kCTFontAttributeName]; - CGFloat fontSize = font ? CTFontGetSize(font) : 12.0; - UIFont *uiFont = [UIFont systemFontOfSize:fontSize * 0.9]; - if (uiFont) { - font = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL); - } else { - font = NULL; - } - if (font) { - attrs[(id) kCTFontAttributeName] = (__bridge id) (font); - uiFont = nil; - CFRelease(font); + + if (container.truncationToken) { + string = [container.truncationToken mutableCopy]; } - CGColorRef color = (__bridge CGColorRef) (attrs[(id) kCTForegroundColorAttributeName]); + + // Ignore clear color + CGColorRef color = (__bridge CGColorRef)attrs[(id)kCTForegroundColorAttributeName]; if (color && CFGetTypeID(color) == CGColorGetTypeID() && CGColorGetAlpha(color) == 0) { - // ignore clear color - [attrs removeObjectForKey:(id) kCTForegroundColorAttributeName]; + [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName]; } - if (!attrs) attrs = [NSMutableDictionary new]; } - truncationToken = [[NSAttributedString alloc] initWithString:ASTextTruncationToken attributes:attrs]; - truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + truncationToken = fillBaseAttributes(string, attrs); + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken); } if (truncationTokenLine) { - CTLineTruncationType type = kCTLineTruncationEnd; - if (container.truncationType == ASTextTruncationTypeStart) { - type = kCTLineTruncationStart; - } else if (container.truncationType == ASTextTruncationTypeMiddle) { - type = kCTLineTruncationMiddle; - } - NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy; + // TODO: Avoid creating this unless we actually modify the last line text. + lastLineText = [[text attributedSubstringFromRange:lastLine.range] mutableCopy]; CGFloat truncatedWidth = lastLine.width; CGFloat atLeastOneLine = lastLine.width; CGRect cgPathRect = CGRectZero; if (CGPathIsRect(cgPath, &cgPathRect)) { - if (isVerticalForm) { - truncatedWidth = cgPathRect.size.height; - } else { - truncatedWidth = cgPathRect.size.width; - } + truncatedWidth = cgPathRect.size.width; } int i = 0; + int limit = (int) removedLines.count; 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". i = (int)removedLines.count - 1; @@ -843,13 +844,19 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [lastLineText appendAttributedString:[text attributedSubstringFromRange:removedLines[i].range]]; atLeastOneLine += removedLines[i--].width; } - [lastLineText appendAttributedString:truncationToken]; + if (type == kCTLineTruncationEnd) { + [lastLineText appendAttributedString:truncationToken]; + } + if (type == kCTLineTruncationMiddle) { // If we are truncating Middle, we do not want + // to collect the same text into the truncated line more than once. + limit = i+1; + } } if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants to collect some // text following the truncated content. i = 0; atLeastOneLine = removedLines[i].width; - while (atLeastOneLine < truncatedWidth && i < removedLines.count) { + while (atLeastOneLine < truncatedWidth && i < limit) { atLeastOneLine += removedLines[i++].width; } for (i--; i >= 0; i--) { @@ -867,91 +874,134 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef) lastLineText); if (ctLastLineExtend) { - CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, type, truncationTokenLine); + // CTLineCreateTruncatedLine only reorders its CTRuns and doesn't change the range. + // After the reorder, some CTRuns in ctTruncatedLine are visible to the users, but we + // don't know which ones since this is completely handled by Core Text. + CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, + type, truncationTokenLine); + if (kTextNode2ImprovedTextTruncationVisibleRange) { + CFArrayRef truncatedLineRuns = CTLineGetGlyphRuns(ctTruncatedLine); + // Calculate the range of the truncated line before the truncation tokens + if (truncatedLineRuns) { + CFIndex truncatedLineRunCount = CFArrayGetCount(truncatedLineRuns); + + CGFloat truncationTokenWidth = + CTLineGetTypographicBounds(truncationTokenLine, 0, 0, 0); + CGFloat ctLastLineExtendWidth = + CTLineGetTypographicBounds(ctLastLineExtend, 0, 0, 0); + + // If the last line is not truncated, the truncation token is appended directly to + // the text without deletion. + if (kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix && + (truncatedWidth > ctLastLineExtendWidth)) { + NSAttributedString *lastLineSubString = [lastLineText + attributedSubstringFromRange:NSMakeRange(0, + MAX(0, lastLineText.length - + truncationToken.length))]; + truncatedLineBeforeTruncationToken = + [ASTextLine lineWithCTLine:CTLineCreateWithAttributedString( + (CFAttributedStringRef)lastLineSubString) + position:lastLine.position + vertical:NO]; + + } else { + // Since Core Text hides the information that which CTRun is the last visible one, + // iterate through the runs to estimate the last visible run. + for (int i = 0; i < truncatedLineRunCount; i++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(truncatedLineRuns, + truncatedLineRunCount - 1 - i); + CFRange runRange = CTRunGetStringRange(run); + // Make sure that lastLineSubString has a valid range. + NSUInteger truncatedLineBeforeTruncationTokenRangeEnd = + MAX(MIN(lastLineText.length, runRange.location + runRange.length), 0); + truncatedLineBeforeTruncationTokenRange = NSMakeRange( + lastLine.range.location, truncatedLineBeforeTruncationTokenRangeEnd); + + NSAttributedString *lastLineSubString = [lastLineText + attributedSubstringFromRange: + NSMakeRange(0, truncatedLineBeforeTruncationTokenRangeEnd)]; + CGFloat lastLineSubStringWidth = CTLineGetTypographicBounds( + CTLineCreateWithAttributedString((CFAttributedStringRef)lastLineSubString), + 0, 0, 0); + // If lastLineSubString and truncationToken can "almost" fit in truncatedWidth, + // assume the current run is the last visible CTRun, and lastLineSubString is + // the visible part in the last line. Adding 2 here for error tolerance since + // truncationTokenWidth + lastLineSubStringWidth might be slightly longer than + // truncatedWidth. + if (truncationTokenWidth + lastLineSubStringWidth < truncatedWidth + 2) { + truncatedLineBeforeTruncationToken = + [ASTextLine lineWithCTLine:CTLineCreateWithAttributedString( + (CFAttributedStringRef)lastLineSubString) + position:lastLine.position + vertical:NO]; + break; + } + } + } + } + } + CFRelease(ctLastLineExtend); if (ctTruncatedLine) { - truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm]; - truncatedLine.index = lastLine.index; - truncatedLine.row = lastLine.row; + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:NO]; + // 1) If truncation mode is middle or start, and the end of the string contains taller text (or taller attachments), then truncating the line may make it taller + // (By pulling up the tall text that was previously in a later, clipped line, into the truncation line). + // 1b) There are edge cases where truncating the line makes it taller, thus it exceeds the bounds, and we in fact needed to truncate at an earlier line. + // Accommodating these cases in a robust manner would require multiple passes. (TODO_NOTREALLY) + // 2) In all cases, truncating the line may make it shorter. (Of course) + // 3) If text is not left-aligned, and truncating changed the width of the last line, it also needs to change its position. + BOOL adjusted = NO; + CGPoint adjustedPosition = truncatedLine.position; + if (truncatedLine.bounds.size.height > lastLine.bounds.size.height) { + adjusted = YES; + adjustedPosition = {adjustedPosition.x, lastLine.position.y + (truncatedLine.bounds.size.height - lastLine.bounds.size.height)/2}; + } + if ([lastLineText as_alignment] == NSTextAlignmentRight) { // (TODO: same for center-aligned) + adjusted = YES; + adjustedPosition = {lastLine.position.x - (truncatedLine.bounds.size.width - lastLine.bounds.size.width), adjustedPosition.y}; + } + if (adjusted) { + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:adjustedPosition vertical:NO]; + } CFRelease(ctTruncatedLine); } + textBoundingRect = CGRectUnion(textBoundingRect, truncatedLine.bounds); + truncatedLine.index = lastLine.index; + truncatedLine.row = lastLine.row; } CFRelease(truncationTokenLine); } } } - } - - if (isVerticalForm) { - NSCharacterSet *rotateCharset = ASTextVerticalFormRotateCharacterSet(); - NSCharacterSet *rotateMoveCharset = ASTextVerticalFormRotateAndMoveCharacterSet(); - void (^lineBlock)(ASTextLine *) = ^(ASTextLine *line){ - CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); - if (!runs) return; - NSUInteger runCount = CFArrayGetCount(runs); - if (runCount == 0) return; - NSMutableArray *lineRunRanges = [NSMutableArray new]; - line.verticalRotateRange = lineRunRanges; - for (NSUInteger r = 0; r < runCount; r++) { - CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); - NSMutableArray *runRanges = [NSMutableArray new]; - [lineRunRanges addObject:runRanges]; - NSUInteger glyphCount = CTRunGetGlyphCount(run); - if (glyphCount == 0) continue; - - CFIndex runStrIdx[glyphCount + 1]; - CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); - CFRange runStrRange = CTRunGetStringRange(run); - runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; - CFDictionaryRef runAttrs = CTRunGetAttributes(run); - CTFontRef font = (CTFontRef)CFDictionaryGetValue(runAttrs, kCTFontAttributeName); - BOOL isColorGlyph = ASTextCTFontContainsColorBitmapGlyphs(font); - - NSUInteger prevIdx = 0; - ASTextRunGlyphDrawMode prevMode = ASTextRunGlyphDrawModeHorizontal; - NSString *layoutStr = layout.text.string; - for (NSUInteger g = 0; g < glyphCount; g++) { - BOOL glyphRotate = 0, glyphRotateMove = NO; - CFIndex runStrLen = runStrIdx[g + 1] - runStrIdx[g]; - if (isColorGlyph) { - glyphRotate = YES; - } else if (runStrLen == 1) { - unichar c = [layoutStr characterAtIndex:runStrIdx[g]]; - glyphRotate = [rotateCharset characterIsMember:c]; - if (glyphRotate) glyphRotateMove = [rotateMoveCharset characterIsMember:c]; - } else if (runStrLen > 1){ - NSString *glyphStr = [layoutStr substringWithRange:NSMakeRange(runStrIdx[g], runStrLen)]; - BOOL glyphRotate = [glyphStr rangeOfCharacterFromSet:rotateCharset].location != NSNotFound; - if (glyphRotate) glyphRotateMove = [glyphStr rangeOfCharacterFromSet:rotateMoveCharset].location != NSNotFound; - } - - ASTextRunGlyphDrawMode mode = glyphRotateMove ? ASTextRunGlyphDrawModeVerticalRotateMove : (glyphRotate ? ASTextRunGlyphDrawModeVerticalRotate : ASTextRunGlyphDrawModeHorizontal); - if (g == 0) { - prevMode = mode; - } else if (mode != prevMode) { - ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, g - prevIdx) drawMode:prevMode]; - [runRanges addObject:aRange]; - prevIdx = g; - prevMode = mode; - } - } - if (prevIdx < glyphCount) { - ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, glyphCount - prevIdx) drawMode:prevMode]; - [runRanges addObject:aRange]; + { // calculate bounding size + CGRect rect = textBoundingRect; + if (container.path) { + if (container.pathLineWidth > 0) { + CGFloat inset = container.pathLineWidth / 2; + rect = CGRectInset(rect, -inset, -inset); } - + } else { + rect = UIEdgeInsetsInsetRect(rect, ASTextUIEdgeInsetsInvert(container.insets)); } - }; - for (ASTextLine *line in lines) { - lineBlock(line); + rect = CGRectStandardize(rect); + CGSize size = rect.size; + size.width += rect.origin.x; + size.height += rect.origin.y; + if (size.width < 0) size.width = 0; + if (size.height < 0) size.height = 0; + size.width = ceil(size.width); + size.height = ceil(size.height); + textBoundingSize = size; } - if (truncatedLine) lineBlock(truncatedLine); } - + + // We store some hints we will look up when its time to draw. First thing, do we need to draw at all? if (visibleRange.length > 0) { layout.needDrawText = YES; - + + // ... and some finer details: We go through the NSAttrbutedString attributes looking for things + // that would require the following drawing behaviors, and set flags for them if so: void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) { if (attrs[ASTextHighlightAttributeName]) layout.containsHighlight = YES; if (attrs[ASTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES; @@ -963,12 +1013,18 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (attrs[ASTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES; if (attrs[ASTextBorderAttributeName]) layout.needDrawBorder = YES; }; - + [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; if (truncatedLine) { + if (container.truncationType == ASTextTruncationTypeStart || container.truncationType == ASTextTruncationTypeMiddle) { + // If truncation mode is middle or start, there is another visible range not expressed in visibleRange. + [lastLineText enumerateAttributesInRange:NSMakeRange(0, lastLineText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + } [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; } } + + // Also, set up attachments to use for rendering later. for (NSUInteger i = 0, max = lines.count; i < max; i++) { ASTextLine *line = lines[i]; if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; @@ -993,6 +1049,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri layout.frame = ctFrame; layout.lines = lines; layout.truncatedLine = truncatedLine; + layout.truncatedLineBeforeTruncationTokenRange = truncatedLineBeforeTruncationTokenRange; + layout.truncatedLineBeforeTruncationToken = truncatedLineBeforeTruncationToken; layout.attachments = attachments; layout.attachmentRanges = attachmentRanges; layout.attachmentRects = attachmentRects; @@ -1067,19 +1125,16 @@ - (id)copyWithZone:(NSZone *)zone { */ - (NSUInteger)_rowIndexForEdge:(CGFloat)edge { if (_rowCount == 0) return NSNotFound; - BOOL isVertical = _container.verticalForm; NSUInteger lo = 0, hi = _rowCount - 1, mid = 0; NSUInteger rowIdx = NSNotFound; while (lo <= hi) { mid = (lo + hi) / 2; ASRowEdge oneEdge = _lineRowsEdge[mid]; - if (isVertical ? - (oneEdge.foot <= edge && edge <= oneEdge.head) : - (oneEdge.head <= edge && edge <= oneEdge.foot)) { + if (oneEdge.head <= edge && edge <= oneEdge.foot) { rowIdx = mid; break; } - if ((isVertical ? (edge > oneEdge.head) : (edge < oneEdge.head))) { + if (edge < oneEdge.head) { if (mid == 0) break; hi = mid - 1; } else { @@ -1101,18 +1156,10 @@ - (NSUInteger)_closestRowIndexForEdge:(CGFloat)edge { if (_rowCount == 0) return NSNotFound; NSUInteger rowIdx = [self _rowIndexForEdge:edge]; if (rowIdx == NSNotFound) { - if (_container.verticalForm) { - if (edge > _lineRowsEdge[0].head) { - rowIdx = 0; - } else if (edge < _lineRowsEdge[_rowCount - 1].foot) { - rowIdx = _rowCount - 1; - } - } else { - if (edge < _lineRowsEdge[0].head) { - rowIdx = 0; - } else if (edge > _lineRowsEdge[_rowCount - 1].foot) { - rowIdx = _rowCount - 1; - } + if (edge < _lineRowsEdge[0].head) { + rowIdx = 0; + } else if (edge > _lineRowsEdge[_rowCount - 1].foot) { + rowIdx = _rowCount - 1; } } return rowIdx; @@ -1247,22 +1294,12 @@ - (BOOL)_isRightToLeftInLine:(ASTextLine *)line atPoint:(CGPoint)point { CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); CGPoint glyphPosition; CTRunGetPositions(run, CFRangeMake(0, 1), &glyphPosition); - if (_container.verticalForm) { - CGFloat runX = glyphPosition.x; - runX += line.position.y; - CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - if (runX <= point.y && point.y <= runX + runWidth) { - if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; - break; - } - } else { - CGFloat runX = glyphPosition.x; - runX += line.position.x; - CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - if (runX <= point.x && point.x <= runX + runWidth) { - if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; - break; - } + CGFloat runX = glyphPosition.x; + runX += line.position.x; + CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (runX <= point.x && point.x <= runX + runWidth) { + if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; + break; } } return RTL; @@ -1311,7 +1348,7 @@ - (NSUInteger)rowIndexForLine:(NSUInteger)line { - (NSUInteger)lineIndexForPoint:(CGPoint)point { if (_lines.count == 0 || _rowCount == 0) return NSNotFound; - NSUInteger rowIdx = [self _rowIndexForEdge:_container.verticalForm ? point.x : point.y]; + NSUInteger rowIdx = [self _rowIndexForEdge: point.y]; if (rowIdx == NSNotFound) return NSNotFound; NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; @@ -1325,9 +1362,8 @@ - (NSUInteger)lineIndexForPoint:(CGPoint)point { } - (NSUInteger)closestLineIndexForPoint:(CGPoint)point { - BOOL isVertical = _container.verticalForm; if (_lines.count == 0 || _rowCount == 0) return NSNotFound; - NSUInteger rowIdx = [self _closestRowIndexForEdge:isVertical ? point.x : point.y]; + NSUInteger rowIdx = [self _closestRowIndexForEdge: point.y]; if (rowIdx == NSNotFound) return NSNotFound; NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; @@ -1338,30 +1374,16 @@ - (NSUInteger)closestLineIndexForPoint:(CGPoint)point { NSUInteger minIndex = lineIdx0; for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) { CGRect bounds = ((ASTextLine *)_lines[i]).bounds; - if (isVertical) { - if (bounds.origin.y <= point.y && point.y <= bounds.origin.y + bounds.size.height) return i; - CGFloat distance; - if (point.y < bounds.origin.y) { - distance = bounds.origin.y - point.y; - } else { - distance = point.y - (bounds.origin.y + bounds.size.height); - } - if (distance < minDistance) { - minDistance = distance; - minIndex = i; - } + if (bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width) return i; + CGFloat distance; + if (point.x < bounds.origin.x) { + distance = bounds.origin.x - point.x; } else { - if (bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width) return i; - CGFloat distance; - if (point.x < bounds.origin.x) { - distance = bounds.origin.x - point.x; - } else { - distance = point.x - (bounds.origin.x + bounds.size.width); - } - if (distance < minDistance) { - minDistance = distance; - minIndex = i; - } + distance = point.x - (bounds.origin.x + bounds.size.width); + } + if (distance < minDistance) { + minDistance = distance; + minIndex = i; } } return minIndex; @@ -1374,19 +1396,14 @@ - (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)line if (position < range.location || position > range.location + range.length) return CGFLOAT_MAX; CGFloat offset = CTLineGetOffsetForStringIndex(line.CTLine, position, NULL); - return _container.verticalForm ? (offset + line.position.y) : (offset + line.position.x); + return offset + line.position.x; } - (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex { if (lineIndex >= _lines.count) return NSNotFound; ASTextLine *line = _lines[lineIndex]; - if (_container.verticalForm) { - point.x = point.y - line.position.y; - point.y = 0; - } else { - point.x -= line.position.x; - point.y = 0; - } + point.x -= line.position.x; + point.y = 0; CFIndex idx = CTLineGetStringIndexForPosition(line.CTLine, point); if (idx == kCFNotFound) return NSNotFound; @@ -1442,12 +1459,10 @@ If the emoji contains one or more variant form (such as ☔️ "\u2614\uFE0F") } - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { - BOOL isVertical = _container.verticalForm; // When call CTLineGetStringIndexForPosition() on ligature such as 'fi', // and the point `hit` the glyph's left edge, it may get the ligature inside offset. // I don't know why, maybe it's a bug of CoreText. Try to avoid it. - if (isVertical) point.y += 0.00001234; - else point.x += 0.00001234; + point.x += 0.00001234; NSUInteger lineIndex = [self closestLineIndexForPoint:point]; if (lineIndex == NSNotFound) return nil; @@ -1473,22 +1488,12 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex]; if (left != CGFLOAT_MAX && right != CGFLOAT_MAX) { - if (_container.isVerticalForm) { - if (fabs(point.y - left) < fabs(point.y - right)) { - position = bindingRange.location; - finalAffinity = ASTextAffinityForward; - } else { - position = bindingRange.location + bindingRange.length; - finalAffinity = ASTextAffinityBackward; - } + if (fabs(point.x - left) < fabs(point.x - right)) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; } else { - if (fabs(point.x - left) < fabs(point.x - right)) { - position = bindingRange.location; - finalAffinity = ASTextAffinityForward; - } else { - position = bindingRange.location + bindingRange.length; - finalAffinity = ASTextAffinityBackward; - } + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; } } else if (left != CGFLOAT_MAX) { position = left; @@ -1560,13 +1565,13 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } // above whole text frame - if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (point.y < line.top))) { + if (lineIndex == 0 && (point.y < line.top)) { position = 0; finalAffinity = ASTextAffinityForward; finalAffinityDetected = YES; } // below whole text frame - if (lineIndex == _lines.count - 1 && (isVertical ? (point.x < line.left) : (point.y > line.bottom))) { + if (lineIndex == _lines.count - 1 && (point.y > line.bottom)) { position = line.range.location + line.range.length; finalAffinity = ASTextAffinityBackward; finalAffinityDetected = YES; @@ -1596,19 +1601,11 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } [self _insideComposedCharacterSequences:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { - if (isVertical) { - position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); - } else { - position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); - } + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); }]; [self _insideEmoji:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { - if (isVertical) { - position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); - } else { - position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); - } + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); }]; if (position < _visibleRange.location) position = _visibleRange.location; @@ -1623,7 +1620,7 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } else if (position <= line.range.location) { finalAffinity = RTL ? ASTextAffinityBackward : ASTextAffinityForward; } else { - finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; + finalAffinity = (ofs < point.x && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; } } } @@ -1647,35 +1644,19 @@ - (ASTextPosition *)positionForPoint:(CGPoint)point if (lineIndex == NSNotFound) return oldPosition; ASTextLine *line = _lines[lineIndex]; ASRowEdge vertical = _lineRowsEdge[line.row]; - if (_container.verticalForm) { - point.x = (vertical.head + vertical.foot) * 0.5; - } else { - point.y = (vertical.head + vertical.foot) * 0.5; - } + point.y = (vertical.head + vertical.foot) * 0.5; newPos = [self closestPositionToPoint:point]; if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] && newPos.offset != otherPosition.offset) { return newPos; } - - if (_container.isVerticalForm) { - if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionUp offset:1]; - if (range) return range.start; - } else { // search forward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionDown offset:1]; - if (range) return range.end; - } - } else { - if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; - if (range) return range.start; - } else { // search forward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; - if (range) return range.end; - } + if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; + if (range) return range.start; + } else { // search forward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; + if (range) return range.end; } - return oldPosition; } @@ -1691,14 +1672,8 @@ - (ASTextRange *)textRangeAtPoint:(CGPoint)point { BOOL RTL = [self _isRightToLeftInLine:_lines[lineIndex] atPoint:point]; CGRect rect = [self caretRectForPosition:pos]; if (CGRectIsNull(rect)) return nil; - - if (_container.verticalForm) { - ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown offset:1]; - return range; - } else { - ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; - return range; - } + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; + return range; } - (ASTextRange *)closestTextRangeAtPoint:(CGPoint)point { @@ -1714,22 +1689,18 @@ - (ASTextRange *)closestTextRangeAtPoint:(CGPoint)point { UITextLayoutDirection direction = UITextLayoutDirectionRight; if (pos.offset >= line.range.location + line.range.length) { if (direction != RTL) { - direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + direction = UITextLayoutDirectionLeft; } else { - direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + direction = UITextLayoutDirectionRight; } } else if (pos.offset <= line.range.location) { if (direction != RTL) { - direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + direction = UITextLayoutDirectionRight; } else { - direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + direction = UITextLayoutDirectionLeft; } } else { - if (_container.verticalForm) { - direction = (rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown; - } else { - direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; - } + direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; } ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1]; @@ -1807,17 +1778,9 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position if (!position) return nil; if (position.offset < visibleStart || position.offset > visibleEnd) return nil; if (offset == 0) return [self textRangeByExtendingPosition:position]; - - BOOL isVerticalForm = _container.verticalForm; - BOOL verticalMove, forwardMove; - - if (isVerticalForm) { - verticalMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionRight; - forwardMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown; - } else { - verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown; - forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; - } + + BOOL verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown; + BOOL forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; if (offset < 0) { forwardMove = !forwardMove; @@ -1858,32 +1821,17 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position for (NSUInteger i = 0; i < moveToLineCount; i++) { NSUInteger lineIndex = moveToLineFirstIndex + i; ASTextLine *line = _lines[lineIndex]; - if (isVerticalForm) { - if (line.top <= ofs && ofs <= line.bottom) { - insideIndex = line.index; - break; - } - if (line.top < mostLeft) { - mostLeft = line.top; - mostLeftLine = line; - } - if (line.bottom > mostRight) { - mostRight = line.bottom; - mostRightLine = line; - } - } else { - if (line.left <= ofs && ofs <= line.right) { - insideIndex = line.index; - break; - } - if (line.left < mostLeft) { - mostLeft = line.left; - mostLeftLine = line; - } - if (line.right > mostRight) { - mostRight = line.right; - mostRightLine = line; - } + if (line.left <= ofs && ofs <= line.right) { + insideIndex = line.index; + break; + } + if (line.left < mostLeft) { + mostLeft = line.left; + mostLeftLine = line; + } + if (line.right > mostRight) { + mostRight = line.right; + mostRightLine = line; } } BOOL afinityEdge = NO; @@ -1896,12 +1844,7 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position afinityEdge = YES; } ASTextLine *insideLine = _lines[insideIndex]; - NSUInteger pos; - if (isVerticalForm) { - pos = [self textPositionForPoint:CGPointMake(insideLine.position.x, ofs) lineIndex:insideIndex]; - } else { - pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex]; - } + NSUInteger pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex]; if (pos == NSNotFound) return nil; ASTextPosition *extPos; if (afinityEdge) { @@ -1980,11 +1923,7 @@ - (CGPoint)linePositionForPosition:(ASTextPosition *)position { ASTextLine *line = _lines[lineIndex]; CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; if (offset == CGFLOAT_MAX) return CGPointZero; - if (_container.verticalForm) { - return CGPointMake(line.position.x, offset); - } else { - return CGPointMake(offset, line.position.y); - } + return CGPointMake(offset, line.position.y); } - (CGRect)caretRectForPosition:(ASTextPosition *)position { @@ -1993,11 +1932,7 @@ - (CGRect)caretRectForPosition:(ASTextPosition *)position { ASTextLine *line = _lines[lineIndex]; CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; if (offset == CGFLOAT_MAX) return CGRectNull; - if (_container.verticalForm) { - return CGRectMake(line.bounds.origin.x, offset, line.bounds.size.width, 0); - } else { - return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height); - } + return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height); } - (CGRect)firstRectForRange:(ASTextRange *)range { @@ -2015,54 +1950,28 @@ - (CGRect)firstRectForRange:(ASTextRange *)range { if (line.row != startLine.row) break; [lines addObject:line]; } - if (_container.verticalForm) { - if (lines.count == 1) { - CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat bottom; - if (startLine == endLine) { - bottom = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; - } else { - bottom = startLine.bottom; - } - if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; - if (top > bottom) ASTEXT_SWAP(top, bottom); - return CGRectMake(startLine.left, top, startLine.width, bottom - top); + if (lines.count == 1) { + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right; + if (startLine == endLine) { + right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; } else { - CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat bottom = startLine.bottom; - if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; - if (top > bottom) ASTEXT_SWAP(top, bottom); - CGRect rect = CGRectMake(startLine.left, top, startLine.width, bottom - top); - for (NSUInteger i = 1; i < lines.count; i++) { - ASTextLine *line = lines[i]; - rect = CGRectUnion(rect, line.bounds); - } - return rect; + right = startLine.right; } + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + return CGRectMake(left, startLine.top, right - left, startLine.height); } else { - if (lines.count == 1) { - CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat right; - if (startLine == endLine) { - right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; - } else { - right = startLine.right; - } - if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; - if (left > right) ASTEXT_SWAP(left, right); - return CGRectMake(left, startLine.top, right - left, startLine.height); - } else { - CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat right = startLine.right; - if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; - if (left > right) ASTEXT_SWAP(left, right); - CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); - for (NSUInteger i = 1; i < lines.count; i++) { - ASTextLine *line = lines[i]; - rect = CGRectUnion(rect, line.bounds); - } - return rect; + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right = startLine.right; + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); + for (NSUInteger i = 1; i < lines.count; i++) { + ASTextLine *line = lines[i]; + rect = CGRectUnion(rect, line.bounds); } + return rect; } } @@ -2080,7 +1989,6 @@ - (CGRect)rectForRange:(ASTextRange *)range { - (NSArray *)selectionRectsForRange:(ASTextRange *)range { range = [self _correctedRangeWithEdge:range]; - BOOL isVertical = _container.verticalForm; NSMutableArray *rects = [[NSMutableArray alloc] init]; if (!range) return rects; @@ -2094,81 +2002,52 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { CGFloat offsetEnd = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; ASTextSelectionRect *start = [ASTextSelectionRect new]; - if (isVertical) { - start.rect = CGRectMake(startLine.left, offsetStart, startLine.width, 0); - } else { - start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height); - } + start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height); start.containsStart = YES; - start.isVertical = isVertical; + start.isVertical = NO; [rects addObject:start]; ASTextSelectionRect *end = [ASTextSelectionRect new]; - if (isVertical) { - end.rect = CGRectMake(endLine.left, offsetEnd, endLine.width, 0); - } else { - end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height); - } + end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height); end.containsEnd = YES; - end.isVertical = isVertical; + end.isVertical = NO; [rects addObject:end]; if (startLine.row == endLine.row) { // same row if (offsetStart > offsetEnd) ASTEXT_SWAP(offsetStart, offsetEnd); ASTextSelectionRect *rect = [ASTextSelectionRect new]; - if (isVertical) { - rect.rect = CGRectMake(startLine.bounds.origin.x, offsetStart, MAX(startLine.width, endLine.width), offsetEnd - offsetStart); - } else { - rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height)); - } - rect.isVertical = isVertical; + rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height)); + rect.isVertical = NO; [rects addObject:rect]; } else { // more than one row // start line select rect ASTextSelectionRect *topRect = [ASTextSelectionRect new]; - topRect.isVertical = isVertical; + topRect.isVertical = NO; CGFloat topOffset = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; CTRunRef topRun = [self _runForLine:startLine position:range.start]; if (topRun && (CTRunGetStatus(topRun) & kCTRunStatusRightToLeft)) { - if (isVertical) { - topRect.rect = CGRectMake(startLine.left, _container.path ? startLine.top : _container.insets.top, startLine.width, topOffset - startLine.top); - } else { - topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height); - } + topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height); topRect.writingDirection = UITextWritingDirectionRightToLeft; } else { - 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); - } + // 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); } [rects addObject:topRect]; // end line select rect ASTextSelectionRect *bottomRect = [ASTextSelectionRect new]; - bottomRect.isVertical = isVertical; + bottomRect.isVertical = NO; CGFloat bottomOffset = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; CTRunRef bottomRun = [self _runForLine:endLine position:range.end]; if (bottomRun && (CTRunGetStatus(bottomRun) & kCTRunStatusRightToLeft)) { - if (isVertical) { - bottomRect.rect = CGRectMake(endLine.left, bottomOffset, endLine.width, (_container.path ? endLine.bottom : _container.size.height - _container.insets.bottom) - bottomOffset); - } else { - bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height); - } + bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height); bottomRect.writingDirection = UITextWritingDirectionRightToLeft; } else { - if (isVertical) { - CGFloat top = _container.path ? endLine.top : _container.insets.top; - bottomRect.rect = CGRectMake(endLine.left, top, endLine.width, bottomOffset - top); - } else { - CGFloat left = _container.path ? endLine.left : _container.insets.left; - bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height); - } + CGFloat left = _container.path ? endLine.left : _container.insets.left; + bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height); } [rects addObject:bottomRect]; @@ -2186,49 +2065,28 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { } } if (startLineDetected) { - if (isVertical) { - if (!_container.path) { - r.origin.y = _container.insets.top; - r.size.height = _container.size.height - _container.insets.bottom - _container.insets.top; - } - r.size.width = CGRectGetMinX(topRect.rect) - CGRectGetMaxX(bottomRect.rect); - r.origin.x = CGRectGetMaxX(bottomRect.rect); - } else { - if (!_container.path) { - r.origin.x = _container.insets.left; - r.size.width = _container.size.width - _container.insets.right - _container.insets.left; - } - r.origin.y = CGRectGetMaxY(topRect.rect); - r.size.height = bottomRect.rect.origin.y - r.origin.y; + if (!_container.path) { + r.origin.x = _container.insets.left; + r.size.width = _container.size.width - _container.insets.right - _container.insets.left; } + r.origin.y = CGRectGetMaxY(topRect.rect); + r.size.height = bottomRect.rect.origin.y - r.origin.y; ASTextSelectionRect *rect = [ASTextSelectionRect new]; rect.rect = r; - rect.isVertical = isVertical; + rect.isVertical = NO; [rects addObject:rect]; } } else { - if (isVertical) { - CGRect r0 = bottomRect.rect; - CGRect r1 = topRect.rect; - CGFloat mid = (CGRectGetMaxX(r0) + CGRectGetMinX(r1)) * 0.5; - r0.size.width = mid - r0.origin.x; - CGFloat r1ofs = r1.origin.x - mid; - r1.origin.x -= r1ofs; - r1.size.width += r1ofs; - topRect.rect = r1; - bottomRect.rect = r0; - } else { - CGRect r0 = topRect.rect; - CGRect r1 = bottomRect.rect; - CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5; - r0.size.height = mid - r0.origin.y; - CGFloat r1ofs = r1.origin.y - mid; - r1.origin.y -= r1ofs; - r1.size.height += r1ofs; - topRect.rect = r0; - bottomRect.rect = r1; - } + CGRect r0 = topRect.rect; + CGRect r1 = bottomRect.rect; + CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5; + r0.size.height = mid - r0.origin.y; + CGFloat r1ofs = r1.origin.y - mid; + r1.origin.y -= r1ofs; + r1.size.height += r1ofs; + topRect.rect = r0; + bottomRect.rect = r1; } } return rects; @@ -2274,18 +2132,11 @@ typedef NS_OPTIONS(NSUInteger, ASTextBorderType) { ASTextBorderTypeNormal = 1 << 1, }; -static CGRect ASTextMergeRectInSameLine(CGRect rect1, CGRect rect2, BOOL isVertical) { - if (isVertical) { - CGFloat top = MIN(rect1.origin.y, rect2.origin.y); - CGFloat bottom = MAX(rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height); - CGFloat width = MAX(rect1.size.width, rect2.size.width); - return CGRectMake(rect1.origin.x, top, width, bottom - top); - } else { - CGFloat left = MIN(rect1.origin.x, rect2.origin.x); - CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); - CGFloat height = MAX(rect1.size.height, rect2.size.height); - return CGRectMake(left, rect1.origin.y, right - left, height); - } +static CGRect ASTextMergeRectInSameLine(CGRect rect1, CGRect rect2) { + CGFloat left = MIN(rect1.origin.x, rect2.origin.x); + CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); + CGFloat height = MAX(rect1.size.height, rect2.size.height); + return CGRectMake(left, rect1.origin.y, right - left, height); } static void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *underlinePosition, CGFloat *lineThickness) { @@ -2312,13 +2163,13 @@ static void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *u if (lineThickness) *lineThickness = maxLineThickness; } -static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, BOOL isVertical, NSArray *runRanges, CGFloat verticalOffset) { +static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, NSArray *runRanges, CGFloat verticalOffset) { CGAffineTransform runTextMatrix = CTRunGetTextMatrix(run); BOOL runTextMatrixIsID = CGAffineTransformIsIdentity(runTextMatrix); CFDictionaryRef runAttrs = CTRunGetAttributes(run); NSValue *glyphTransformValue = (NSValue *)CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); - if (!isVertical && !glyphTransformValue) { // draw run + if (!glyphTransformValue) { // draw run if (!runTextMatrixIsID) { CGContextSaveGState(context); CGAffineTransform trans = CGContextGetTextMatrix(context); @@ -2358,102 +2209,46 @@ static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGContextSetTextDrawingMode(context, kCGTextFillStroke); } } - - if (isVertical) { + if (glyphTransformValue) { CFIndex runStrIdx[glyphCount + 1]; CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); CFRange runStrRange = CTRunGetStringRange(run); runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; CGSize glyphAdvances[glyphCount]; CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); - CGFloat ascent = CTFontGetAscent(runFont); - CGFloat descent = CTFontGetDescent(runFont); CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; CGPoint zeroPoint = CGPointZero; - - for (ASTextRunGlyphRange *oneRange in runRanges) { - NSRange range = oneRange.glyphRangeInRun; - NSUInteger rangeMax = range.location + range.length; - ASTextRunGlyphDrawMode mode = oneRange.drawMode; - - for (NSUInteger g = range.location; g < rangeMax; g++) { - CGContextSaveGState(context); { - CGContextSetTextMatrix(context, CGAffineTransformIdentity); - if (glyphTransformValue) { - CGContextSetTextMatrix(context, glyphTransform); - } - if (mode) { // CJK glyph, need rotated - CGFloat ofs = (ascent - descent) * 0.5; - CGFloat w = glyphAdvances[g].width * 0.5; - CGFloat x = line.position.x + verticalOffset + glyphPositions[g].y + (ofs - w); - CGFloat y = -line.position.y + size.height - glyphPositions[g].x - (ofs + w); - if (mode == ASTextRunGlyphDrawModeVerticalRotateMove) { - x += w; - y += w; - } - CGContextSetTextPosition(context, x, y); - } else { - CGContextRotateCTM(context, -M_PI_2); - CGContextSetTextPosition(context, - line.position.y - size.height + glyphPositions[g].x, - line.position.x + verticalOffset + glyphPositions[g].y); - } - - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); - CGFontRelease(cgFont); - } - } CGContextRestoreGState(context); - } + + for (NSUInteger g = 0; g < glyphCount; g++) { + CGContextSaveGState(context); { + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextMatrix(context, glyphTransform); + CGContextSetTextPosition(context, + line.position.x + glyphPositions[g].x, + size.height - (line.position.y + glyphPositions[g].y)); + + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); + } else { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); + CGFontRelease(cgFont); + } + } CGContextRestoreGState(context); } - } else { // not vertical - if (glyphTransformValue) { - CFIndex runStrIdx[glyphCount + 1]; - CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); - CFRange runStrRange = CTRunGetStringRange(run); - runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; - CGSize glyphAdvances[glyphCount]; - CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); - CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; - CGPoint zeroPoint = CGPointZero; - - for (NSUInteger g = 0; g < glyphCount; g++) { - CGContextSaveGState(context); { - CGContextSetTextMatrix(context, CGAffineTransformIdentity); - CGContextSetTextMatrix(context, glyphTransform); - CGContextSetTextPosition(context, - line.position.x + glyphPositions[g].x, - size.height - (line.position.y + glyphPositions[g].y)); - - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); - CGFontRelease(cgFont); - } - } CGContextRestoreGState(context); - } + } else { + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context); } else { - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); - CGFontRelease(cgFont); - } + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); + CGFontRelease(cgFont); } } - } CGContextRestoreGState(context); } } @@ -2488,7 +2283,7 @@ static void ASTextSetLinePatternInContext(ASTextLineStyle style, CGFloat width, } -static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects, BOOL isVertical) { +static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects) { if (rects.count == 0) return; ASTextShadow *shadow = border.shadow; @@ -2501,11 +2296,7 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde NSMutableArray *paths = [NSMutableArray new]; for (NSValue *value in rects) { CGRect rect = value.CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, border.insets); - } + rect = UIEdgeInsetsInsetRect(rect, border.insets); rect = ASTextCGRectPixelRound(rect); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius]; [path closePath]; @@ -2547,11 +2338,7 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde CGContextSetLineJoin(context, border.lineJoin); for (NSValue *value in rects) { CGRect rect = value.CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, border.insets); - } + rect = UIEdgeInsetsInsetRect(rect, border.insets); rect = CGRectInset(rect, inset, inset); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; [path closePath]; @@ -2604,85 +2391,46 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde } } -static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color, BOOL isVertical) { +static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color) { NSUInteger styleBase = style & 0xFF; if (styleBase == 0) return; CGContextSaveGState(context); { - if (isVertical) { - CGFloat x, y1, y2, w; - y1 = ASRoundPixelValue(position.y); - y2 = ASRoundPixelValue(position.y + length); - w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); - - CGFloat linePixel = ASTextCGFloatToPixel(w); - if (fabs(linePixel - floor(linePixel)) < 0.1) { - int iPixel = linePixel; - if (iPixel == 0 || (iPixel % 2)) { // odd line pixel - x = ASTextCGFloatPixelHalf(position.x); - } else { - x = ASFloorPixelValue(position.x); - } + CGFloat x1, x2, y, w; + x1 = ASRoundPixelValue(position.x); + x2 = ASRoundPixelValue(position.x + length); + w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = ASTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + y = ASTextCGFloatPixelHalf(position.y); } else { - x = position.x; - } - - CGContextSetStrokeColorWithColor(context, color); - ASTextSetLinePatternInContext(style, lineWidth, position.y, context); - CGContextSetLineWidth(context, w); - if (styleBase == ASTextLineStyleSingle) { - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleThick) { - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleDouble) { - CGContextMoveToPoint(context, x - w, y1); - CGContextAddLineToPoint(context, x - w, y2); - CGContextStrokePath(context); - CGContextMoveToPoint(context, x + w, y1); - CGContextAddLineToPoint(context, x + w, y2); - CGContextStrokePath(context); + y = ASFloorPixelValue(position.y); } } else { - CGFloat x1, x2, y, w; - x1 = ASRoundPixelValue(position.x); - x2 = ASRoundPixelValue(position.x + length); - w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); - - CGFloat linePixel = ASTextCGFloatToPixel(w); - if (fabs(linePixel - floor(linePixel)) < 0.1) { - int iPixel = linePixel; - if (iPixel == 0 || (iPixel % 2)) { // odd line pixel - y = ASTextCGFloatPixelHalf(position.y); - } else { - y = ASFloorPixelValue(position.y); - } - } else { - y = position.y; - } - - CGContextSetStrokeColorWithColor(context, color); - ASTextSetLinePatternInContext(style, lineWidth, position.x, context); - CGContextSetLineWidth(context, w); - if (styleBase == ASTextLineStyleSingle) { - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleThick) { - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleDouble) { - CGContextMoveToPoint(context, x1, y - w); - CGContextAddLineToPoint(context, x2, y - w); - CGContextStrokePath(context); - CGContextMoveToPoint(context, x1, y + w); - CGContextAddLineToPoint(context, x2, y + w); - CGContextStrokePath(context); - } + y = position.y; + } + + CGContextSetStrokeColorWithColor(context, color); + ASTextSetLinePatternInContext(style, lineWidth, position.x, context); + CGContextSetLineWidth(context, w); + if (styleBase == ASTextLineStyleSingle) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleThick) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleDouble) { + CGContextMoveToPoint(context, x1, y - w); + CGContextAddLineToPoint(context, x2, y - w); + CGContextStrokePath(context); + CGContextMoveToPoint(context, x1, y + w); + CGContextAddLineToPoint(context, x2, y + w); + CGContextStrokePath(context); } } CGContextRestoreGState(context); } @@ -2694,8 +2442,7 @@ static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize si CGContextTranslateCTM(context, 0, size.height); CGContextScaleCTM(context, 1, -1); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -2709,7 +2456,7 @@ static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize si CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextSetTextPosition(context, posX, posY); - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } if (cancel && cancel()) break; } @@ -2725,8 +2472,7 @@ static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CG CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -2772,18 +2518,13 @@ static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CG } lineContinueIndex++; } while (true); - - if (isVertical) { - UIEdgeInsets insets = layout.container.insets; - unionRect.origin.y = insets.top; - unionRect.size.height = layout.container.size.height -insets.top - insets.bottom; - } else { - UIEdgeInsets insets = layout.container.insets; - unionRect.origin.x = insets.left; - unionRect.size.width = layout.container.size.width -insets.left - insets.right; - } + + UIEdgeInsets insets = layout.container.insets; + unionRect.origin.x = insets.left; + unionRect.size.width = layout.container.size.width -insets.left - insets.right; + unionRect.origin.x += verticalOffset; - ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical); + ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]]); l = lineContinueIndex; break; @@ -2798,8 +2539,7 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; NSString *borderKey = (type == ASTextBorderTypeNormal ? ASTextBorderAttributeName : ASTextBackgroundBorderAttributeName); @@ -2857,25 +2597,15 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CTRunGetPositions(iRun, CFRangeMake(0, 1), &iRunPosition); CGFloat ascent, descent; CGFloat iRunWidth = CTRunGetTypographicBounds(iRun, CFRangeMake(0, 0), &ascent, &descent, NULL); - - if (isVertical) { - ASTEXT_SWAP(iRunPosition.x, iRunPosition.y); - iRunPosition.y += iLine.position.y; - CGRect iRect = CGRectMake(verticalOffset + line.position.x - descent, iRunPosition.y, ascent + descent, iRunWidth); - if (CGRectIsNull(extLineRect)) { - extLineRect = iRect; - } else { - extLineRect = CGRectUnion(extLineRect, iRect); - } + + iRunPosition.x += iLine.position.x; + CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent); + if (CGRectIsNull(extLineRect)) { + extLineRect = iRect; } else { - iRunPosition.x += iLine.position.x; - CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent); - if (CGRectIsNull(extLineRect)) { - extLineRect = iRect; - } else { - extLineRect = CGRectUnion(extLineRect, iRect); - } + extLineRect = CGRectUnion(extLineRect, iRect); } + } if (!CGRectIsNull(extLineRect)) { @@ -2887,27 +2617,18 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CGRect curRect= ((NSValue *)[runRects firstObject]).CGRectValue; for (NSInteger re = 0, reMax = runRects.count; re < reMax; re++) { CGRect rect = ((NSValue *)runRects[re]).CGRectValue; - if (isVertical) { - if (fabs(rect.origin.x - curRect.origin.x) < 1) { - curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); - } else { - [drawRects addObject:[NSValue valueWithCGRect:curRect]]; - curRect = rect; - } + if (fabs(rect.origin.y - curRect.origin.y) < 1) { + curRect = ASTextMergeRectInSameLine(rect, curRect); } else { - if (fabs(rect.origin.y - curRect.origin.y) < 1) { - curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); - } else { - [drawRects addObject:[NSValue valueWithCGRect:curRect]]; - curRect = rect; - } + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; } } if (!CGRectEqualToRect(curRect, CGRectZero)) { [drawRects addObject:[NSValue valueWithCGRect:curRect]]; } - ASTextDrawBorderRects(context, size, border, drawRects, isVertical); + ASTextDrawBorderRects(context, size, border, drawRects); if (l == endLineIndex) { r = endRunIndex; @@ -2930,8 +2651,7 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextTranslateCTM(context, verticalOffset, 0); for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { @@ -2969,26 +2689,15 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGPoint underlineStart, strikethroughStart; CGFloat length; - - if (isVertical) { - underlineStart.x = line.position.x + underlinePosition; - strikethroughStart.x = line.position.x + xHeight / 2; - - CGPoint runPosition = CGPointZero; - CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); - underlineStart.y = strikethroughStart.y = runPosition.x + line.position.y; - length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - - } else { - underlineStart.y = line.position.y - underlinePosition; - strikethroughStart.y = line.position.y - xHeight / 2; - - CGPoint runPosition = CGPointZero; - CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); - underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x; - length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - } - + + underlineStart.y = line.position.y - underlinePosition; + strikethroughStart.y = line.position.y - xHeight / 2; + + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x; + length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (needDrawUnderline) { CGColorRef color = underline.color.CGColor; if (!color) { @@ -3010,12 +2719,12 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } CGContextRestoreGState(context); } CGContextRestoreGState(context); shadow = shadow.subShadow; } - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } if (needDrawStrikethrough) { @@ -3039,12 +2748,12 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } CGContextRestoreGState(context); } CGContextRestoreGState(context); shadow = shadow.subShadow; } - ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color); } } } @@ -3053,8 +2762,7 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS static void ASTextDrawAttachment(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) { - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) { ASTextAttachment *a = layout.attachments[i]; @@ -3063,26 +2771,34 @@ static void ASTextDrawAttachment(ASTextLayout *layout, CGContextRef context, CGS UIImage *image = nil; UIView *view = nil; CALayer *layer = nil; + ASDisplayNode *node = nil; if ([a.content isKindOfClass:[UIImage class]]) { image = a.content; } else if ([a.content isKindOfClass:[UIView class]]) { view = a.content; } else if ([a.content isKindOfClass:[CALayer class]]) { layer = a.content; + } else if ([a.content isKindOfClass:[ASDisplayNode class]]) { + node = a.content; } - if (!image && !view && !layer) continue; - if (image && !context) continue; + if (!image && !view && !layer && !node) continue; + if ((image || node) && !context) continue; if (view && !targetView) continue; if (layer && !targetLayer) continue; if (cancel && cancel()) break; - + if (!image && node) { + ASNetworkImageNode *networkImage = ASDynamicCast(node, ASNetworkImageNode); + if ([networkImage animatedImage]) { + // Need to check first because [networkImage defaultImage] can return coverImage for + // animated image. + image = [[UIImage alloc] initWithCGImage:(CGImageRef)node.contents]; + } else { + image = [networkImage image] ?: [networkImage defaultImage]; + } + } CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size; CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); - } + rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); rect = ASTextCGRectFitWithContentMode(rect, asize, a.contentMode); rect = ASTextCGRectPixelRound(rect); rect = CGRectStandardize(rect); @@ -3111,8 +2827,7 @@ static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize //move out of context. (0xFFFF is just a random large number) CGFloat offsetAlterX = size.width + 0xFFFF; - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextSaveGState(context); { CGContextTranslateCTM(context, point.x, point.y); @@ -3149,7 +2864,7 @@ static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } CGContextRestoreGState(context); shadow = shadow.subShadow; } @@ -3165,8 +2880,7 @@ static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CG CGContextScaleCTM(context, 1, -1); CGContextSetTextMatrix(context, CGAffineTransformIdentity); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -3217,7 +2931,7 @@ static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CG CGContextFillRect(context, runImageBounds); CGContextSetBlendMode(context, kCGBlendModeDestinationIn); CGContextBeginTransparencyLayer(context, NULL); { - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } CGContextEndTransparencyLayer(context); } CGContextEndTransparencyLayer(context); } CGContextEndTransparencyLayer(context); @@ -3239,8 +2953,7 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CGContextSetLineJoin(context, kCGLineJoinMiter); CGContextSetLineCap(context, kCGLineCapButt); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextTranslateCTM(context, verticalOffset, 0); if (op.CTFrameBorderColor || op.CTFrameFillColor) { @@ -3316,28 +3029,19 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s } if (op.baselineColor) { [op.baselineColor setStroke]; - if (isVertical) { - CGFloat x = ASTextCGFloatPixelHalf(line.position.x); - CGFloat y1 = ASTextCGFloatPixelHalf(line.top); - CGFloat y2 = ASTextCGFloatPixelHalf(line.bottom); - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else { - CGFloat x1 = ASTextCGFloatPixelHalf(lineBounds.origin.x); - CGFloat x2 = ASTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); - CGFloat y = ASTextCGFloatPixelHalf(line.position.y); - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } + CGFloat x1 = ASTextCGFloatPixelHalf(lineBounds.origin.x); + CGFloat x2 = ASTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); + CGFloat y = ASTextCGFloatPixelHalf(line.position.y); + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); } if (op.CTLineNumberColor) { [op.CTLineNumberColor set]; NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(l).description]; num.as_color = op.CTLineNumberColor; num.as_font = [UIFont systemFontOfSize:6]; - [num drawAtPoint:CGPointMake(line.position.x, line.position.y - (isVertical ? 1 : 6))]; + [num drawAtPoint:CGPointMake(line.position.x, line.position.y - 6)]; } if (op.CTRunFillColor || op.CTRunBorderColor || op.CTRunNumberColor || op.CGGlyphFillColor || op.CGGlyphBorderColor) { CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); @@ -3353,24 +3057,14 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CTRunGetAdvances(run, CFRangeMake(0, glyphCount), glyphAdvances); CGPoint runPosition = glyphPositions[0]; - if (isVertical) { - ASTEXT_SWAP(runPosition.x, runPosition.y); - runPosition.x = line.position.x; - runPosition.y += line.position.y; - } else { - runPosition.x += line.position.x; - runPosition.y = line.position.y - runPosition.y; - } + runPosition.x += line.position.x; + runPosition.y = line.position.y - runPosition.y; CGFloat ascent, descent, leading; CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); CGRect runTypoBounds; - if (isVertical) { - runTypoBounds = CGRectMake(runPosition.x - descent, runPosition.y, ascent + descent, width); - } else { - runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent); - } - + runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent); + if (op.CTRunFillColor) { [op.CTRunFillColor setFill]; CGContextAddRect(context, ASTextCGRectPixelRound(runTypoBounds)); @@ -3393,16 +3087,10 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CGPoint pos = glyphPositions[g]; CGSize adv = glyphAdvances[g]; CGRect rect; - if (isVertical) { - ASTEXT_SWAP(pos.x, pos.y); - pos.x = runPosition.x; - pos.y += line.position.y; - rect = CGRectMake(pos.x - descent, pos.y, runTypoBounds.size.width, adv.width); - } else { - pos.x += line.position.x; - pos.y = runPosition.y; - rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height); - } + pos.x += line.position.x; + pos.y = runPosition.y; + rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height); + if (op.CGGlyphFillColor) { [op.CGGlyphFillColor setFill]; CGContextAddRect(context, ASTextCGRectPixelRound(rect)); @@ -3481,3 +3169,15 @@ - (void)drawInContext:(CGContextRef)context } @end + +NSAttributedString *fillBaseAttributes(NSAttributedString *str, NSDictionary *attrs) { + NSUInteger len = str.length; + if (!len) return str; + NSMutableAttributedString *m_result; // Do not create unless needed. + for (NSString *name in attrs) { + if ([str as_hasAttribute:name]) continue; + if (!m_result) m_result = [str mutableCopy]; + [m_result addAttribute:name value:attrs[name] range:NSMakeRange(0, len)]; + } + return m_result ?: str; +} diff --git a/Source/TextExperiment/String/ASTextAttribute.h b/Source/TextExperiment/String/ASTextAttribute.h index 571334ed3..e142bee00 100644 --- a/Source/TextExperiment/String/ASTextAttribute.h +++ b/Source/TextExperiment/String/ASTextAttribute.h @@ -48,9 +48,10 @@ typedef NS_OPTIONS (NSInteger, ASTextLineStyle) { Text vertical alignment. */ typedef NS_ENUM(NSInteger, ASTextVerticalAlignment) { - ASTextVerticalAlignmentTop = 0, ///< Top alignment. - ASTextVerticalAlignmentCenter = 1, ///< Center alignment. - ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. + ASTextVerticalAlignmentTop = 0, ///< Top alignment. + ASTextVerticalAlignmentCenter = 1, ///< Center alignment. + ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. + ASTextVerticalAlignmentBaseline = 3, ///< Baseline alignment. }; /** @@ -270,7 +271,9 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR */ @interface ASTextAttachment : NSObject + (instancetype)attachmentWithContent:(nullable id)content NS_RETURNS_RETAINED; -@property (nullable, nonatomic) id content; ///< Supported type: UIImage, UIView, CALayer + +@property(nullable, nonatomic) + id content; ///< Supported type: UIImage, UIView, CALayer, ASDisplayNode @property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. @property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. @property (nullable, nonatomic) NSDictionary *userInfo; ///< The user information dictionary. diff --git a/Source/TextExperiment/String/ASTextAttribute.mm b/Source/TextExperiment/String/ASTextAttribute.mm index 8af432271..516bff9d6 100644 --- a/Source/TextExperiment/String/ASTextAttribute.mm +++ b/Source/TextExperiment/String/ASTextAttribute.mm @@ -9,6 +9,8 @@ #import "ASTextAttribute.h" #import +#import +#import #import NSString *const ASTextBackedStringAttributeName = @"ASTextBackedString"; @@ -365,6 +367,10 @@ - (id)copyWithZone:(NSZone *)zone { return one; } +- (void)dealloc { + ASPerformMainThreadDeallocation(&_userInfo); +} + @end diff --git a/Source/TextExperiment/Utility/NSAttributedString+ASText.h b/Source/TextExperiment/Utility/NSAttributedString+ASText.h index ef44fb4f3..f0d2adddc 100644 --- a/Source/TextExperiment/Utility/NSAttributedString+ASText.h +++ b/Source/TextExperiment/Utility/NSAttributedString+ASText.h @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, copy, readonly) NSDictionary *as_attributes; +/** + Returns the attributes at the first character as Core Text attributes if NS attributes. + */ +@property (nullable, nonatomic, copy, readonly) NSDictionary *as_ctAttributes; + /** Returns the attributes for the character at a given index. @@ -597,10 +602,10 @@ NS_ASSUME_NONNULL_BEGIN /** Creates and returns an attachment. - - + + Example: ContentMode:bottom Alignment:Top. - + The text The attachment holder ↓ ↓ ─────────┌──────────────────────┐─────── @@ -613,21 +618,25 @@ NS_ASSUME_NONNULL_BEGIN │ ██████████████ ←───────────────── The attachment content │ ██████████████ │ └──────────────────────┘ - + @param content The attachment (UIImage/UIView/CALayer). @param contentMode The attachment's content mode in attachment holder @param attachmentSize The attachment holder's size in text layout. @param font The attachment will align to this font. @param alignment The attachment holder's alignment to text line. - + @param contentInset The attachment's contentInset. + @param userInfo The infomation associated with the attachment. + @return An attributed string, or nil if an error occurs. @since ASText:6.0 */ -+ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content contentMode:(UIViewContentMode)contentMode attachmentSize:(CGSize)attachmentSize alignToFont:(UIFont *)font - alignment:(ASTextVerticalAlignment)alignment; + alignment:(ASTextVerticalAlignment)alignment + contentInsets:(UIEdgeInsets)contentInsets + userInfo:(NSDictionary *)userInfo; /** Creates and returns an attahment from a fourquare image as if it was an emoji. diff --git a/Source/TextExperiment/Utility/NSAttributedString+ASText.mm b/Source/TextExperiment/Utility/NSAttributedString+ASText.mm index 39f628151..01f8b4699 100644 --- a/Source/TextExperiment/Utility/NSAttributedString+ASText.mm +++ b/Source/TextExperiment/Utility/NSAttributedString+ASText.mm @@ -37,6 +37,39 @@ - (NSDictionary *)as_attributes { return [self as_attributesAtIndex:0]; } +- (NSDictionary *)as_ctAttributes { + NSDictionary *attributes = self.as_attributes; + if (attributes == nil) { + return nil; + } + + NSMutableDictionary *mutableCTAttributes = [[NSMutableDictionary alloc] initWithCapacity:attributes.count]; + + // Map for NS attributes that are not mapping cleanly to CT attributes + static NSDictionary *NSToCTAttributeNamesMap = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSToCTAttributeNamesMap = @{ + NSFontAttributeName: (NSString *)kCTFontAttributeName, + NSBackgroundColorAttributeName: (NSString *)kCTBackgroundColorAttributeName, + NSForegroundColorAttributeName: (NSString *)kCTForegroundColorAttributeName, + NSUnderlineColorAttributeName: (NSString *)kCTUnderlineColorAttributeName, + NSUnderlineStyleAttributeName: (NSString *)kCTUnderlineStyleAttributeName, + NSStrokeWidthAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSStrokeColorAttributeName: (NSString *)kCTStrokeColorAttributeName, + NSKernAttributeName: (NSString *)kCTKernAttributeName, + NSLigatureAttributeName: (NSString *)kCTLigatureAttributeName + }; + }); + + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + key = NSToCTAttributeNamesMap[key] ?: key; + [mutableCTAttributes setObject:value forKey:key]; + }]; + + return [mutableCTAttributes copy]; +} + - (UIFont *)as_font { return [self as_fontAtIndex:0]; } @@ -470,14 +503,18 @@ + (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content contentMode:(UIViewContentMode)contentMode attachmentSize:(CGSize)attachmentSize alignToFont:(UIFont *)font - alignment:(ASTextVerticalAlignment)alignment { + alignment:(ASTextVerticalAlignment)alignment + contentInsets:(UIEdgeInsets)contentInsets + userInfo:(NSDictionary *)userInfo { NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; - + ASTextAttachment *attach = [ASTextAttachment new]; attach.content = content; attach.contentMode = contentMode; + attach.userInfo = userInfo; + attach.contentInsets = contentInsets; [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; - + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; delegate.width = attachmentSize.width; switch (alignment) { @@ -507,16 +544,17 @@ + (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content delegate.descent = attachmentSize.height; } } break; + case ASTextVerticalAlignmentBaseline: default: { delegate.ascent = attachmentSize.height; delegate.descent = 0; - } break; + } } - + CTRunDelegateRef delegateRef = delegate.CTRunDelegate; [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)]; if (delegate) CFRelease(delegateRef); - + return atr; } @@ -953,29 +991,30 @@ - (void)as_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)r [self as_setAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; } -#define ParagraphStyleSet(_attr_) \ -[self enumerateAttribute:NSParagraphStyleAttributeName \ -inRange:range \ -options:kNilOptions \ -usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \ -NSMutableParagraphStyle *style = nil; \ -if (value) { \ -if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \ -value = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \ -} \ -if (value. _attr_ == _attr_) return; \ -if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \ -style = (id)value; \ -} else { \ -style = value.mutableCopy; \ -} \ -} else { \ -if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \ -style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \ -} \ -style. _attr_ = _attr_; \ -[self as_setParagraphStyle:style range:subRange]; \ -}]; +#define ParagraphStyleSet(_attr_) \ + [self enumerateAttribute:NSParagraphStyleAttributeName \ + inRange:range \ + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired \ + usingBlock:^(NSParagraphStyle * value, NSRange subRange, BOOL * stop) { \ + NSMutableParagraphStyle *style = nil; \ + if (value) { \ + if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \ + value = [NSParagraphStyle \ + as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \ + } \ + if (value._attr_ == _attr_) return; \ + if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \ + style = (id)value; \ + } else { \ + style = [value mutableCopy]; \ + } \ + } else { \ + if ([NSParagraphStyle defaultParagraphStyle]._attr_ == _attr_) return; \ + style = [[NSMutableParagraphStyle alloc] init]; \ + } \ + style._attr_ = _attr_; \ + [self as_setParagraphStyle:style range:subRange]; \ + }]; - (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range { ParagraphStyleSet(alignment); diff --git a/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm b/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm index bc19fd234..220bd38a7 100644 --- a/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm +++ b/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm @@ -20,7 +20,7 @@ @implementation NSParagraphStyle (ASText) + (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { if (CTStyle == NULL) return nil; - NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; #if TARGET_OS_IOS #pragma clang diagnostic push From c6916843dd6eb889a20886f54bedcf4c540c0334 Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Fri, 6 Aug 2021 09:23:00 -0700 Subject: [PATCH 02/13] build fixes --- Source/Private/ASDisplayNode+FrameworkPrivate.h | 4 ++++ Source/Private/ASInternalHelpers.mm | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index 53432f023..cf525a084 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -320,6 +320,10 @@ NS_INLINE UIAccessibilityTraits ASInteractiveAccessibilityTraitsMask() { return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; } +// dispatch_once variables must live outside of static inline function or else will be copied +// for each separate invocation. We want them shared across all invocations. +static BOOL shouldEnableAccessibilityForTesting; +static dispatch_once_t kShouldEnableAccessibilityForTestingOnceToken; NS_INLINE BOOL ASAccessibilityIsEnabled() { #if DEBUG return true; diff --git a/Source/Private/ASInternalHelpers.mm b/Source/Private/ASInternalHelpers.mm index e648ea992..26b4126aa 100644 --- a/Source/Private/ASInternalHelpers.mm +++ b/Source/Private/ASInternalHelpers.mm @@ -88,7 +88,7 @@ void ASInitializeFrameworkMainThread(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ASDisplayNodeCAssertMainThread(); - applicationUserInterfaceLayoutDirection = @([UIApplication sharedApplication].userInterfaceLayoutDirection); + applicationUserInterfaceLayoutDirection = @([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:UISemanticContentAttributeUnspecified]); // Ensure these values are cached on the main thread before needed in the background. if (ASActivateExperimentalFeature(ASExperimentalLayerDefaults)) { // Nop. We will gather default values on-demand in ASDefaultAllowsGroupOpacity and ASDefaultAllowsEdgeAntialiasing From 0d01fa61635823756a9f4af9dd7f504757ebdac4 Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Mon, 8 Nov 2021 10:29:38 -0800 Subject: [PATCH 03/13] remove unused variable --- Source/ASTextNode2.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 429e83384..7d9e53971 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -615,7 +615,6 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Holding it for the duration of the method is more efficient in this case. MutexLocker l(__instanceLock__); - NSAttributedString *oldAttributedText = _attributedText; if (!ASCompareAssignCopy(_attributedText, attributedText)) { return; } From 1ed7f4176f9fae429cecec315eb6eacd7c1ea0bf Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Tue, 9 Nov 2021 11:17:13 -0800 Subject: [PATCH 04/13] Fix build for ASDKLayoutTransition example --- Source/ASDisplayNode+Yoga.h | 2 ++ Source/ASDisplayNode+Yoga.mm | 13 ++++++- Source/ASDisplayNode+Yoga2.h | 47 ++++++++++++++++++++++++++ Source/ASDisplayNode+Yoga2.mm | 46 +++++++++++++++++++++++++ Source/ASTextNode2.mm | 3 +- Source/Layout/ASYogaUtilities.h | 4 +++ Source/Layout/ASYogaUtilities.mm | 11 ++++++ Source/Private/ASDisplayNodeInternal.h | 7 ++++ 8 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 Source/ASDisplayNode+Yoga2.h create mode 100644 Source/ASDisplayNode+Yoga2.mm diff --git a/Source/ASDisplayNode+Yoga.h b/Source/ASDisplayNode+Yoga.h index 000bb90e0..5437bedfb 100644 --- a/Source/ASDisplayNode+Yoga.h +++ b/Source/ASDisplayNode+Yoga.h @@ -19,12 +19,14 @@ ASDK_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Null @interface ASDisplayNode (Yoga) @property (copy) NSArray *yogaChildren; +@property (readonly, weak) ASDisplayNode *yogaParent; - (void)addYogaChild:(ASDisplayNode *)child; - (void)removeYogaChild:(ASDisplayNode *)child; - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index; - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; +- (UIUserInterfaceLayoutDirection)yogaLayoutDirection; @property BOOL yogaLayoutInProgress; // TODO: Make this atomic (lock). diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 619f5a780..216b2fcab 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -22,11 +22,14 @@ #import #import #import - #import +#import #define YOGA_LAYOUT_LOGGING 0 +// Access style property directly or use the getter to create one +#define _LOCKED_ACCESS_STYLE() (_style ?: [self _locked_style]) + #pragma mark - ASDisplayNode+Yoga @interface ASDisplayNode (YogaPrivate) @@ -131,6 +134,14 @@ - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute ? YGDirectionLTR : YGDirectionRTL); } +- (UIUserInterfaceLayoutDirection)yogaLayoutDirection +{ + AS::Yoga2::AssertEnabled(self); + return _LOCKED_ACCESS_STYLE().direction == YGDirectionRTL + ? UIUserInterfaceLayoutDirectionRightToLeft + : UIUserInterfaceLayoutDirectionLeftToRight; +} + - (void)setYogaParent:(ASDisplayNode *)yogaParent { ASLockScopeSelf(); diff --git a/Source/ASDisplayNode+Yoga2.h b/Source/ASDisplayNode+Yoga2.h new file mode 100644 index 000000000..5dd9f8729 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2.h @@ -0,0 +1,47 @@ +// +// ASDisplayNode+Yoga2.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 3/8/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#if defined(__cplusplus) + +#import +#import +#import + +#if YOGA +#import YOGA_HEADER_PATH +#endif + +NS_ASSUME_NONNULL_BEGIN + +namespace AS { +namespace Yoga2 { + +/** + * Returns whether Yoga2 is enabled for this node. + */ +bool GetEnabled(ASDisplayNode *node); + +inline void AssertEnabled() { + ASDisplayNodeCAssert(false, @"Expected Yoga2 to be enabled."); +} + +inline void AssertEnabled(ASDisplayNode *node) { + ASDisplayNodeCAssert(GetEnabled(node), @"Expected Yoga2 to be enabled."); +} + +inline void AssertDisabled(ASDisplayNode *node) { + ASDisplayNodeCAssert(!GetEnabled(node), @"Expected Yoga2 to be disabled."); +} + + +} // namespace Yoga2 +} // namespace AS + +NS_ASSUME_NONNULL_END + +#endif // defined(__cplusplus) diff --git a/Source/ASDisplayNode+Yoga2.mm b/Source/ASDisplayNode+Yoga2.mm new file mode 100644 index 000000000..6f0ca4ea0 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2.mm @@ -0,0 +1,46 @@ +// +// ASDisplayNode+Yoga2.mm +// AsyncDisplayKit +// +// Created by Adlai Holler on 3/8/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#import +#import +#import + +#if YOGA + +#import +#import +#import +#import +#import + +#import YOGA_HEADER_PATH + +namespace AS { +namespace Yoga2 { + +bool GetEnabled(ASDisplayNode *node) { + if (node) { + MutexLocker l(node->__instanceLock__); + return node->_flags.yoga; + } else { + return false; + } +} + +#else // !YOGA + +namespace AS { +namespace Yoga2 { + +bool GetEnabled(ASDisplayNode *node) { return false; } + +#endif // YOGA + +} // namespace Yoga2 +} // namespace AS + diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 7d9e53971..c292616c5 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -19,6 +19,7 @@ #import #import #import +#import #import #import @@ -622,8 +623,8 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Since truncation text matches style of attributedText, invalidate it now. [self _locked_invalidateTruncationText]; - NSUInteger length = attributedText.length; #if !YOGA + NSUInteger length = attributedText.length; if (length > 0) { ASLayoutElementStyle *style = [self _locked_style]; style.ascender = [[self class] ascenderWithAttributedString:attributedText]; diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index d5529fc45..2db89ec0b 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -17,6 +17,10 @@ // Should pass a string literal, not an NSString as the first argument to ASYogaLog #define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__); +/** Helper function for Yoga baseline measurement. */ +ASDK_EXTERN CGFloat ASTextGetBaseline(CGFloat height, ASDisplayNode *_Nullable yogaParent, + NSAttributedString *str); + @interface ASDisplayNode (YogaHelpers) + (ASDisplayNode *)yogaNode; diff --git a/Source/Layout/ASYogaUtilities.mm b/Source/Layout/ASYogaUtilities.mm index 68beede1a..40b4e3c85 100644 --- a/Source/Layout/ASYogaUtilities.mm +++ b/Source/Layout/ASYogaUtilities.mm @@ -10,6 +10,17 @@ #import #if YOGA /* YOGA */ +CGFloat ASTextGetBaseline(CGFloat height, ASDisplayNode *yogaParent, NSAttributedString *str) { + if (!yogaParent) return height; + NSUInteger len = str.length; + if (!len) return height; + BOOL isLast = (yogaParent.style.alignItems == ASStackLayoutAlignItemsBaselineLast); + UIFont *font = [str attribute:NSFontAttributeName + atIndex:(isLast ? len - 1 : 0) + effectiveRange:NULL]; + return isLast ? height + font.descender : font.ascender; +} + @implementation ASDisplayNode (YogaHelpers) + (ASDisplayNode *)yogaNode diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 9675ddd72..9016d1d73 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -122,6 +122,13 @@ static constexpr CACornerMask kASCACornerAllCorners = unsigned isDeallocating:1; #if YOGA + unsigned yoga:1; + unsigned shouldSuppressYogaCustomMeasure:1; + unsigned yogaIsApplyingLayout:1; + unsigned yogaRequestedNestedLayout:1; + + // NOTE: This has been replaced in the large YouTube merge. I can't remove it + // completely in this PR as there are still many places that use it. unsigned willApplyNextYogaCalculatedLayout:1; #endif // Automatically manages subnodes From 1564bb33d9340f9bc51c546370895204352e74aa Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Thu, 2 Dec 2021 11:28:30 -0800 Subject: [PATCH 05/13] fix unit tests --- Tests/ASDisplayViewAccessibilityTests.mm | 15 +++++++++++++++ Tests/ASTextNode2Tests.mm | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/ASDisplayViewAccessibilityTests.mm b/Tests/ASDisplayViewAccessibilityTests.mm index d511e98fe..8562999d2 100644 --- a/Tests/ASDisplayViewAccessibilityTests.mm +++ b/Tests/ASDisplayViewAccessibilityTests.mm @@ -29,10 +29,25 @@ extern void SortAccessibilityElements(NSMutableArray *elements); @interface ASDisplayViewAccessibilityTests : XCTestCase +@property (nonatomic) ASConfiguration *experimentalConfiguration; @end @implementation ASDisplayViewAccessibilityTests +- (void)setUp +{ + [super setUp]; + self.experimentalConfiguration = [ASConfiguration new]; + self.experimentalConfiguration.experimentalFeatures = ASExperimentalEnableNodeIsHiddenFromAcessibility | ASExperimentalEnableAcessibilityElementsReturnNil |ASExperimentalDoNotCacheAccessibilityElements; + [ASConfigurationManager test_resetWithConfiguration:self.experimentalConfiguration]; +} + +- (void)tearDown +{ + [super tearDown]; + [ASConfigurationManager test_resetWithConfiguration:nil]; +} + - (void)testAccessibilityElementsAccessors { // Setup nodes with accessibility info diff --git a/Tests/ASTextNode2Tests.mm b/Tests/ASTextNode2Tests.mm index 70d25cb26..56ca71932 100644 --- a/Tests/ASTextNode2Tests.mm +++ b/Tests/ASTextNode2Tests.mm @@ -74,7 +74,8 @@ - (void)testTruncation - (void)testAccessibility { - XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); + XCTAssertFalse(_textNode.isAccessibilityElement, @"Should not be an accessibility element. It is a container"); + XCTAssertTrue(_textNode.accessibilityElements.count > 0, @"Should have some accessibility elements"); XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText, @"Should have static text accessibility trait, instead has %llu", _textNode.accessibilityTraits); From a5139a7beb80fe8f98a0940af049b524ee2b5381 Mon Sep 17 00:00:00 2001 From: ricky cancro Date: Thu, 2 Dec 2021 13:46:56 -0800 Subject: [PATCH 06/13] Google seems to have improved truncating in ASTN2 This test passes on master but fails on the merge branch. Looking at the snapshot difference, it appears that google figured out a way to get more characters in before truncating. The text is still within its provided max size. --- ...hSmallerConstrainedSize_ASTextNode2@2x.png | Bin 4252 -> 4995 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png index 84ef8a2fc8a19fb06512bde99b04d544992c6304..e0ef83dbdab876c39bbc363df9adf7399f452b14 100644 GIT binary patch delta 3100 zcmY*bd0diN7gof*jG)Y=6jv%oO{v@^CLDKj$tB69VN3!<%{2+%sF~8-GAFfBF{w{0 z!`)QOWr`8AM$+6BX>?i?H!_!@kneT=`o2Hz@80*^d(OG%-sgGmQ)+MxWsvFX#nZl}8&&R%Gpja zXA0%yltbKc*wd-<3j&9S`vbL^Nt4!w5O@JE9mqfAt;=48a_ zmLSzMHG5o*CWsa9j94(m%&S)o4R0QFKB0)mDn`h5Eg8J9PDxpsR&8rvby$f%AKh<< z{%CWqb787X;6&R4fvUieC$7lJH~uD%Q7DL$4*-rFV!TC9n!!hn*L_GIP^RseEam8# z$A}X1-N+M>X^^6y`t(h|N!r49q|lP#Up%_HOi|EnQcZ;5cIO14Sdc_j&Yz(K$7tT2YJtAsUNKz$lR%q{esKvW;C@EsS^ zx_2)ka}*w7oPO$ZQL0nCt>0G%8!LQ|7py93r34YRN|_0n;c@j#)_9L1yr+s|7;Lr@ks|hy8-(r>1;2Kh!Ao9qA|{CKLw)KzVYCDJSX)G(JE{ zdekabCWw($#?YHK`+oi&Jm4^sx_MQW-Y1&rK*uMIuyl;nLg1+D;0;@gY2_6<=GsrE zX+#t?SdL3&zwHTm(EC-~#op_XaAW$nhr*%C^%@%m4!-1}4^z3^ex z2)<-o!lVR%FpBF_3_6CL+S@j+PNqrEKNZydLg^5tBE_Dp=30s9?r1Z$cNyy_cv~w; zFSh-4?D5&Moy*1?-kU;^z>S{I+TKUNVW94&>df;D>2Jh+L(~o{-^TQVbJgZO#94C> z*d5AzfjgUCop)X^&AoNeI487=>h#t1ivi)nNzmC(F~|z*sL5FRqscs$5>UC7yt1fC z7%t9&bAM-ct`R)%*j_JOeOpI8*=*M&`J+OC=n8rjnADL}S%%!bc9nanixC_KakaMEI@YJv(tBcerq$ zNrvv*<@LL*#LR20&CxQ9rB$v4)Q8h81DP(9X{>T~%E_r~Z8LmPK<=dkYAdgdTBB_> z{qt`XJ{*22@!bs^O7?rQtLn37lpnTgmxs1a*psHK_SgquNJCXMe?t$|m)19rEc%4( zP7HL%{s+>2CjZWb7Fh!EMco-1|25OD$Zk3I_gUtd#*xO2t&Tb-n#Rw@<0q2gsdS(9 zu8!6;=)214vERN<<%?ItB~&BfxKY9MsFqBWFV1@Qh4ll_Z(qx|u)egDz#JQsIgxwK z6y+h%Aoy4lL@X_h>AFY%(S*TfAGiy|8ZB=fhUJkL@Qe^~9?4!FQmsx2Wo!oev_&Ap zIrc9`cIG@&7eVa9hZ`3eW{>+)m(#$|bFYVJ=#q&7Ca!CDk%;P;E+fD1- z6=zyYNn{3nhyR1)kKkJ}HAfnOiI)kQ%5qARazVpjPibwEYSC2Zy?jqb2z($80v)leqcAo} z8bCz^az0@zVB*5vA$}mAr^a3CDV0#UFW>=_E_cW5!QAmD3H^AKUiYL<+n$Wf;>z8~ zI%h_dI(P4Q5@6MlJo8r*bqC)JwicH0RhQ>0Boq%muKQ*&fT>Dz4nod*Ql)m5KEj4Y z`MYs=RymkIXfPGbZvo~XD=-2+S)P}92(4|RM8-Z|LxyA9o0!J_{hrPF^=)}@(c{X? z=aydNoDEy5hYWDqI#F$fF-fuqlfs3d|BaLynj4vHAkqWkGmRXq30>%0RiFPfgS9Jt z7~u!+F9>NWD`60X=d{dV!BNzdCBZssj<&*b;I{%PHWw4y3J<{?3aUEu;z_I}(Q9&2 zcC_(4p@nCnc_FX=UfPY*vg{f z!wg0qrjr>pj%x#NFA1|AF6#}^Z%EuoEz4ie>!r}adkD-CA6pK{Yux5)4{ z@nk4$*QwKgrIBX7aSvpUi)EXDi~>YBfoOC6d#yd~66YVxDM0$10#rEL(?1$hy%GuJ zuM;>Kv&T?z=6#1_SpTa*%ur|kK2E&gsl{&RsL1C9g8)G2-%#oinVxtDgA>iygZXw{ zoNV!*xR!3O@e#vLGcofAri&V`GN$ZKvAOL@o}c==H{{_ihjktOeyTT5(`S4{U zz-8qFw_K@bq`ig|pvJ92*LHQKrPE9(O$X-8HbzRbbZ4tY-U_p3u6rGGk%GFu4ti7lgG)fev@&9K-Rg^^#kyrW^Rx(W@mXW$ z^K0_^npYI|`mIDjWA7_o_y`1gJn&tP-O5r#fndGQ z+ew?}4N6al2$2f-C;d_}8Q#$O1+WHX=9TVBwaXUD(|fN|@izI#wpHp_4vMU3KA&%<8QaJx>k!d<9nt^^ z0Kht*-wB5S076%tT?lm*#W4R;)petET{=e%fT`Y)HIC{ACQ_f7UH_L;xO#H&eNsOF zs1tk$`vQ_7v%_v>EdMPU-v*U#o1)a?yf;~Iz4_WE%ubBG6KHX@$2!bD%QINt9PGkx z(lYhYBB(!);4mdG_i(wL%aSvx9~NAG%(#Lt7T#x+#7pFfBOiX!)YWnt7?2dTzxyC) zusLj7nYUn{Y*cQ2A+%R<*K7k6hEQ15?YUBHmSNk``CUV&x~ajZ*==jn$8p%d_#TlWhCez!CNWIlTIO$&a+45tp=*_%lzS(MdYZyY!Zi+5C1MvRLQn zI8{8nBctXD$#$ZpEUbU-(mc5_8w0UW2L9@NOq> zSxDL_wFFGsiimXKX_RjWWYN`m20l8r9ev(nA`lP(R&WUPZ>u$>K~5Eh{)swDE4y|K zqOVr95r*)s5$@EbXI`uq$frg+vl%~G?lEwRR^4GV){X~#6##%Nl;|+TnWDuX#Kc(v zRC?Y;fbmSX@IEy5v|T<@_)qHSW8Srp|1(FsSRVio4b&vGOYf0Feaby7o)QPYkO)b( zKRbOZ_1#ZEV3SRDQ2*eKO3|fn7w+Fj7fv9v?*Q!GZPJIL_ot9T?@RM3s~H2bbKur8 zxAkyJK;VK3>1P?IP{OqOq9re0%@h(N;`d|f-hP0%`a7>)-IGi93ppyj85sar8gXHW z?z_S|_$cpC8HV1H_MGi6&f~p93X9soO=8fs_G==0rtA>tWC0l_*~3`TB_Z=z9!&zUF_>GHah zZ$EDTe37XbIARc?nk4!@lp*Lf4J%rxsnZKZ;ER=b{DCgN+1tz~IS(^h=QbY7OO5wn z9V`WFB=Ff^sZzeOM9n2*b2E8QNe!HnVL5Y8{8~0Qk^VsXL^v-s5Cv*S&;6NfH}WZ< zcW%5HjCuzHbG22~!!h>4IXfS`EqvGssm*dW8)GuxN9D4_N_z^my;uA7(D|{|`;6Ag zRU8~k{sNKQV;2(S)|B?2~^4(5fT=U{1 zjSdz-(XaH_nC(G_idZGLB41J8w3?26Z!5&XM?bdlyLk?!1A5-!HIKii$5{I@1v3Vs za8{AWE2RzP*lJsqrAp)donrU?3l-%?x7VChSoLv%iCGV0Qnq{2UC=YmQy^Jw+sG1Rd4Cb@G78en~>{ zdHvd3st&&m^RfU^=zC0XSBG@KmkALjPGUoouUTcSp}%@Vn2pNDym?83c69G}S8}=s zL7-!(auq+>Y!Uj+SUS0w=D)YEPrB4msU7%n(dg?`(?1^OcTBf1BZ$SyUevS-im+5v z_HjUceR_bRephI8yN&jY@lnqSuO_=OWrRj2gLqZ!=ioHO7q%E1gCq6b$GYB!ds=@< zrhgC8yZ)d~CUG{aHDqi5;ZFGROT~D=Q-o^Ikr|P9DpLc4hbL{RYskdfjYE?LPPHxdykMFFOAey7PH57u&RnGG$$}uDslt~ z%fYT6Wbz;Kv@<(4tTFh=MqPT+xLAIqcP(;6HwRx1MCB&dWQk{>|D%-QS5H4(Vk&|e y=}nS?t!n#BRQq~OR<)z)4Q$A6wM~@$@KyDoCIP-m&{4hi9rE!c5^CHj7ykp$wpdyK From 624caadb5060996878ba294de9aa2fdfb7b637d4 Mon Sep 17 00:00:00 2001 From: ricky Date: Wed, 8 Sep 2021 16:15:37 -0700 Subject: [PATCH 07/13] Rename ASNavigationController to ASDKNavigationController to fix name collision (#2020) As of iOS15 the AuthenticationServices framework has a class named `ASNavigationController`. We need to rename our `ASNavigationController` to protect against undefined behavior. Note: This change was based on this PR https://github.com/TextureGroup/Texture/pull/2014. We were slow in merging it and the author has not replied, so I'm making a new one to get this landed. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 24 +++++++++---------- ...ontroller.h => ASDKNavigationController.h} | 10 ++++---- ...troller.mm => ASDKNavigationController.mm} | 16 ++++++------- Source/ASVisibilityProtocols.h | 4 ++-- Source/AsyncDisplayKit.h | 2 +- ...ts.mm => ASDKNavigationControllerTests.mm} | 12 +++++----- 6 files changed, 34 insertions(+), 34 deletions(-) rename Source/{ASNavigationController.h => ASDKNavigationController.h} (63%) rename Source/{ASNavigationController.mm => ASDKNavigationController.mm} (86%) rename Tests/{ASNavigationControllerTests.mm => ASDKNavigationControllerTests.mm} (82%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index f542dae64..ce40a8a61 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -111,7 +111,7 @@ 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.mm */; }; - 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 680346941CE4052A0009FEB4 /* ASDKNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASDKNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */; }; 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B381CB57A5A001D4E68 /* ASImageContainerProtocolCategories.mm */; }; @@ -127,7 +127,7 @@ 68EE0DC01C1B4ED300BA1B99 /* ASMainSerialQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68EE0DBC1C1B4ED300BA1B99 /* ASMainSerialQueue.mm */; }; 68FC85E31CE29B7E00EDD713 /* ASTabBarController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68FC85E51CE29B7E00EDD713 /* ASTabBarController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85E11CE29B7E00EDD713 /* ASTabBarController.mm */; }; - 68FC85E61CE29B9400EDD713 /* ASNavigationController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */; }; + 68FC85E61CE29B9400EDD713 /* ASDKNavigationController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85DD1CE29AB700EDD713 /* ASDKNavigationController.mm */; }; 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.mm */; }; 6900C5F41E8072DA00BCD75C /* ASImageNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */; }; @@ -335,7 +335,7 @@ B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */; }; + BB5FC3CE1F9BA689007F191E /* ASDKNavigationControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASDKNavigationControllerTests.mm */; }; BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */; }; C018DF21216BF26700181FDA /* ASAbstractLayoutController+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */; }; C057D9BD20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */; }; @@ -702,8 +702,8 @@ 68C215571DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewLayoutInspector.mm; sourceTree = ""; }; 68EE0DBB1C1B4ED300BA1B99 /* ASMainSerialQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMainSerialQueue.h; sourceTree = ""; }; 68EE0DBC1C1B4ED300BA1B99 /* ASMainSerialQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMainSerialQueue.mm; sourceTree = ""; }; - 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNavigationController.h; sourceTree = ""; }; - 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNavigationController.mm; sourceTree = ""; }; + 68FC85DC1CE29AB700EDD713 /* ASDKNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDKNavigationController.h; sourceTree = ""; }; + 68FC85DD1CE29AB700EDD713 /* ASDKNavigationController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDKNavigationController.mm; sourceTree = ""; }; 68FC85E01CE29B7E00EDD713 /* ASTabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTabBarController.h; sourceTree = ""; }; 68FC85E11CE29B7E00EDD713 /* ASTabBarController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTabBarController.mm; sourceTree = ""; }; 68FC85E71CE29C7D00EDD713 /* ASVisibilityProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVisibilityProtocols.h; sourceTree = ""; }; @@ -868,7 +868,7 @@ B30BF6501C5964B0004FCD53 /* ASLayoutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutManager.h; path = TextKit/ASLayoutManager.h; sourceTree = ""; }; B30BF6511C5964B0004FCD53 /* ASLayoutManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutManager.mm; path = TextKit/ASLayoutManager.mm; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNavigationControllerTests.mm; sourceTree = ""; }; + BB5FC3CD1F9BA688007F191E /* ASDKNavigationControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDKNavigationControllerTests.mm; sourceTree = ""; }; BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTabBarControllerTests.mm; sourceTree = ""; }; BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASAbstractLayoutController+FrameworkPrivate.h"; sourceTree = ""; }; @@ -1244,8 +1244,8 @@ 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */, 0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */, 0516FA3F1A1563D200B4EBED /* ASMultiplexImageNode.mm */, - 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */, - 68FC85DD1CE29AB700EDD713 /* ASNavigationController.mm */, + 68FC85DC1CE29AB700EDD713 /* ASDKNavigationController.h */, + 68FC85DD1CE29AB700EDD713 /* ASDKNavigationController.mm */, CCED5E3C2020D36800395C40 /* ASNetworkImageLoadInfo.h */, CCED5E3D2020D36800395C40 /* ASNetworkImageLoadInfo.mm */, 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */, @@ -1356,7 +1356,7 @@ CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.mm */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm */, - BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */, + BB5FC3CD1F9BA688007F191E /* ASDKNavigationControllerTests.mm */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.mm */, ACF6ED591B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm */, AE6987C01DD04E1000B9E458 /* ASPagerNodeTests.mm */, @@ -1980,7 +1980,7 @@ B35061FE1B010EFD0018CF92 /* ASDisplayNodeExtras.h in Headers */, CC0F88601E4280B800576FED /* _ASCollectionViewCell.h in Headers */, B35062001B010EFD0018CF92 /* ASEditableTextNode.h in Headers */, - 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */, + 680346941CE4052A0009FEB4 /* ASDKNavigationController.h in Headers */, B350621B1B010EFD0018CF92 /* ASTableLayoutController.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */, @@ -2329,7 +2329,7 @@ CC01EB6F23105C7F00CDB61A /* ASImageNodeSnapshotTests.mm in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.mm in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.mm in Sources */, - BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.mm in Sources */, + BB5FC3CE1F9BA689007F191E /* ASDKNavigationControllerTests.mm in Sources */, 81FF150722EB5F410039311A /* ASButtonNodeSnapshotTests.mm in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.mm in Sources */, @@ -2528,7 +2528,7 @@ B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.mm in Sources */, CC35CEC420DD7F600006448D /* ASCollections.mm in Sources */, - 68FC85E61CE29B9400EDD713 /* ASNavigationController.mm in Sources */, + 68FC85E61CE29B9400EDD713 /* ASDKNavigationController.mm in Sources */, 9C0BA4A42582CE35001C293B /* ASTextAttribute.mm in Sources */, 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, diff --git a/Source/ASNavigationController.h b/Source/ASDKNavigationController.h similarity index 63% rename from Source/ASNavigationController.h rename to Source/ASDKNavigationController.h index 0d259b24a..aefcccddd 100644 --- a/Source/ASNavigationController.h +++ b/Source/ASDKNavigationController.h @@ -1,5 +1,5 @@ // -// ASNavigationController.h +// ASDKNavigationController.h // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. @@ -14,16 +14,16 @@ NS_ASSUME_NONNULL_BEGIN /** - * ASNavigationController + * ASDKNavigationController * - * @discussion ASNavigationController is a drop in replacement for UINavigationController + * @discussion ASDKNavigationController is a drop in replacement for UINavigationController * which improves memory efficiency by implementing the @c ASManagesChildVisibilityDepth protocol. - * You can use ASNavigationController with regular UIViewControllers, as well as ASDKViewControllers. + * You can use ASDKNavigationController with regular UIViewControllers, as well as ASDKViewControllers. * It is safe to subclass or use even where AsyncDisplayKit is not adopted. * * @see ASManagesChildVisibilityDepth */ -@interface ASNavigationController : UINavigationController +@interface ASDKNavigationController : UINavigationController @end diff --git a/Source/ASNavigationController.mm b/Source/ASDKNavigationController.mm similarity index 86% rename from Source/ASNavigationController.mm rename to Source/ASDKNavigationController.mm index 2fbc4880c..fcda885ac 100644 --- a/Source/ASNavigationController.mm +++ b/Source/ASDKNavigationController.mm @@ -1,5 +1,5 @@ // -// ASNavigationController.mm +// ASDKNavigationController.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. @@ -7,11 +7,11 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import #import #import -@implementation ASNavigationController +@implementation ASDKNavigationController { BOOL _parentManagesVisibilityDepth; NSInteger _visibilityDepth; @@ -59,7 +59,7 @@ - (NSInteger)visibilityDepthOfChildViewController:(UIViewController *)childViewC - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated { - as_activity_create_for_scope("Pop multiple from ASNavigationController"); + as_activity_create_for_scope("Pop multiple from ASDKNavigationController"); NSArray *viewControllers = [super popToViewController:viewController animated:animated]; os_log_info(ASNodeLog(), "Popped %@ to %@, removing %@", self, viewController, ASGetDescriptionValueString(viewControllers)); @@ -69,7 +69,7 @@ - (NSArray *)popToViewController:(UIViewController *)viewController animated:(BO - (NSArray *)popToRootViewControllerAnimated:(BOOL)animated { - as_activity_create_for_scope("Pop to root of ASNavigationController"); + as_activity_create_for_scope("Pop to root of ASDKNavigationController"); NSArray *viewControllers = [super popToRootViewControllerAnimated:animated]; os_log_info(ASNodeLog(), "Popped view controllers %@ from %@", ASGetDescriptionValueString(viewControllers), self); @@ -87,7 +87,7 @@ - (void)setViewControllers:(NSArray *)viewControllers - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated { - as_activity_create_for_scope("Set view controllers of ASNavigationController"); + as_activity_create_for_scope("Set view controllers of ASDKNavigationController"); os_log_info(ASNodeLog(), "Set view controllers of %@ to %@ animated: %d", self, ASGetDescriptionValueString(viewControllers), animated); [super setViewControllers:viewControllers animated:animated]; [self visibilityDepthDidChange]; @@ -95,7 +95,7 @@ - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { - as_activity_create_for_scope("Push view controller on ASNavigationController"); + as_activity_create_for_scope("Push view controller on ASDKNavigationController"); os_log_info(ASNodeLog(), "Pushing %@ onto %@", viewController, self); [super pushViewController:viewController animated:animated]; [self visibilityDepthDidChange]; @@ -103,7 +103,7 @@ - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)ani - (UIViewController *)popViewControllerAnimated:(BOOL)animated { - as_activity_create_for_scope("Pop view controller from ASNavigationController"); + as_activity_create_for_scope("Pop view controller from ASDKNavigationController"); UIViewController *viewController = [super popViewControllerAnimated:animated]; os_log_info(ASNodeLog(), "Popped %@ from %@", viewController, self); [self visibilityDepthDidChange]; diff --git a/Source/ASVisibilityProtocols.h b/Source/ASVisibilityProtocols.h index b9f5c7ba3..d8ab17731 100644 --- a/Source/ASVisibilityProtocols.h +++ b/Source/ASVisibilityProtocols.h @@ -53,7 +53,7 @@ ASDK_EXTERN ASLayoutRangeMode ASLayoutRangeModeForVisibilityDepth(NSUInteger vis * has changed. * * If implemented by a view controller container, use this method to notify child view controllers that their view - * depth has changed @see ASNavigationController.m + * depth has changed @see ASDKNavigationController.m * * If implemented on an ASDKViewController, use this method to reduce or increase the resources that your * view controller uses. A higher visibility depth view controller should decrease it's resource usage, a lower @@ -80,7 +80,7 @@ ASDK_EXTERN ASLayoutRangeMode ASLayoutRangeModeForVisibilityDepth(NSUInteger vis /** * @abstract Container view controllers should adopt this protocol to indicate that they will manage their child's - * visibilityDepth. For example, ASNavigationController adopts this protocol and manages its childrens visibility + * visibilityDepth. For example, ASDKNavigationController adopts this protocol and manages its childrens visibility * depth. * * If you adopt this protocol, you *must* also emit visibilityDepthDidChange messages to child view controllers. diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index ebe0c1a97..4f1e7e3a1 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -65,7 +65,7 @@ #import #import -#import +#import #import #import diff --git a/Tests/ASNavigationControllerTests.mm b/Tests/ASDKNavigationControllerTests.mm similarity index 82% rename from Tests/ASNavigationControllerTests.mm rename to Tests/ASDKNavigationControllerTests.mm index 379905a4b..22897963a 100644 --- a/Tests/ASNavigationControllerTests.mm +++ b/Tests/ASDKNavigationControllerTests.mm @@ -1,5 +1,5 @@ // -// ASNavigationControllerTests.mm +// ASDKNavigationControllerTests.mm // Texture // // Copyright (c) Pinterest, Inc. All rights reserved. @@ -10,16 +10,16 @@ #import -@interface ASNavigationControllerTests : XCTestCase +@interface ASDKNavigationControllerTests : XCTestCase @end -@implementation ASNavigationControllerTests +@implementation ASDKNavigationControllerTests - (void)testSetViewControllers { ASDKViewController *firstController = [ASDKViewController new]; ASDKViewController *secondController = [ASDKViewController new]; NSArray *expectedViewControllerStack = @[firstController, secondController]; - ASNavigationController *navigationController = [ASNavigationController new]; + ASDKNavigationController *navigationController = [ASDKNavigationController new]; [navigationController setViewControllers:@[firstController, secondController]]; XCTAssertEqual(navigationController.topViewController, secondController); XCTAssertEqual(navigationController.visibleViewController, secondController); @@ -30,7 +30,7 @@ - (void)testPopViewController { ASDKViewController *firstController = [ASDKViewController new]; ASDKViewController *secondController = [ASDKViewController new]; NSArray *expectedViewControllerStack = @[firstController]; - ASNavigationController *navigationController = [ASNavigationController new]; + ASDKNavigationController *navigationController = [ASDKNavigationController new]; [navigationController setViewControllers:@[firstController, secondController]]; [navigationController popViewControllerAnimated:false]; XCTAssertEqual(navigationController.topViewController, firstController); @@ -42,7 +42,7 @@ - (void)testPushViewController { ASDKViewController *firstController = [ASDKViewController new]; ASDKViewController *secondController = [ASDKViewController new]; NSArray *expectedViewControllerStack = @[firstController, secondController]; - ASNavigationController *navigationController = [[ASNavigationController new] initWithRootViewController:firstController]; + ASDKNavigationController *navigationController = [[ASDKNavigationController new] initWithRootViewController:firstController]; [navigationController pushViewController:secondController animated:false]; XCTAssertEqual(navigationController.topViewController, secondController); XCTAssertEqual(navigationController.visibleViewController, secondController); From cb12cfba8fa1ded75cd077c6a78953ffe296fe4d Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 9 Sep 2021 16:33:53 -0700 Subject: [PATCH 08/13] [3.1.0] Create new version of ASDK (#2021) With the breaking change of renaming ASNavigationController to ASDKNavigationController, we have released a new version of Texture. Please see `ThreeMigrationGuide.md` for how to handle the breaking changes in 3.1.0. --- Texture.podspec | 2 +- ThreeMigrationGuide.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Texture.podspec b/Texture.podspec index 9b5cbe233..3c57948c8 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Texture' - spec.version = '3.0.0' + spec.version = '3.1.0' spec.license = { :type => 'Apache 2', } spec.homepage = 'http://texturegroup.org' spec.authors = { 'Huy Nguyen' => 'hi@huynguyen.dev', 'Garrett Moon' => 'garrett@excitedpixel.com', 'Scott Goodson' => 'scottgoodson@gmail.com', 'Michael Schneider' => 'mischneider1@gmail.com', 'Adlai Holler' => 'adlai@icloud.com' } diff --git a/ThreeMigrationGuide.md b/ThreeMigrationGuide.md index 4ea5bf372..b3eb0a6c1 100644 --- a/ThreeMigrationGuide.md +++ b/ThreeMigrationGuide.md @@ -1,7 +1,11 @@ -## Texture 3.0 Migration Guide +## Texture 3.1 Migration Guide Got a tip for upgrading? Please open a PR to this document! +- Rename all instances of ASNavigationController to ASDKNavigationController + +## Texture 3.0 Migration Guide + - Rename all instances of ASViewController to ASDKViewController ### Breaking API Changes From 3d825445d3e835b657f1525b558fcd30abfd1000 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 29 Sep 2021 07:46:24 -0700 Subject: [PATCH 09/13] [3.1.0] Update CHANGELOG --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128b49a7a..eafa3a02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## [3.1.0](https://github.com/TextureGroup/Texture/tree/3.1.0) (2021-09-09) + +[Full Changelog](https://github.com/TextureGroup/Texture/compare/3.0.0...3.1.0) + +**Fixed bugs:** + +- Fix hit point when ASCollectionNode inverted set to true [\#1781](https://github.com/TextureGroup/Texture/pull/1781) ([bdolman](https://github.com/bdolman)) + +**Merged pull requests:** + +- \[Minor Breaking API\] Rename ASNavigationController to ASDKNavigationController to fix name collision [\#2020](https://github.com/TextureGroup/Texture/pull/2020) ([rcancro](https://github.com/rcancro)) +- \[RTL\] Guard access of flipsHorizontallyInOppositeLayoutDirection for iOS \>= 11 [\#2003](https://github.com/TextureGroup/Texture/pull/2003) ([rcancro](https://github.com/rcancro)) +- \[RTL/Batching\] Make ASDisplayShouldFetchBatchForScrollView aware of flipped CV layouts [\#1985](https://github.com/TextureGroup/Texture/pull/1985) ([rcancro](https://github.com/rcancro)) +- \[Layout\] Add RTL support to LayoutSpecs [\#1983](https://github.com/TextureGroup/Texture/pull/1983) ([rcancro](https://github.com/rcancro)) +- Expand ASExperimentalRangeUpdateOnChangesetUpdate to ASTableView [\#1979](https://github.com/TextureGroup/Texture/pull/1979) ([rqueue](https://github.com/rqueue)) +- Docs: Remove Facebook and shift everything around, add Remix by Buffer [\#1978](https://github.com/TextureGroup/Texture/pull/1978) ([ay8s](https://github.com/ay8s)) +- Add experiment to ensure ASCollectionView's range controller updates … [\#1976](https://github.com/TextureGroup/Texture/pull/1976) ([rqueue](https://github.com/rqueue)) +- Remove trailing semicolons between method parameters and body [\#1973](https://github.com/TextureGroup/Texture/pull/1973) ([sdefresne](https://github.com/sdefresne)) +- Fix order-dependent ASTextNodeTests [\#1963](https://github.com/TextureGroup/Texture/pull/1963) ([tjaneczko](https://github.com/tjaneczko)) +- Update asdkGram swift sample to swift version 5.3 [\#1962](https://github.com/TextureGroup/Texture/pull/1962) ([MussaCharles](https://github.com/MussaCharles)) +- Fix mutation of variable that is never read [\#1961](https://github.com/TextureGroup/Texture/pull/1961) ([ZevEisenberg](https://github.com/ZevEisenberg)) +- Remove redundant assignment [\#1960](https://github.com/TextureGroup/Texture/pull/1960) ([ZevEisenberg](https://github.com/ZevEisenberg)) +- Podfile improvements [\#1957](https://github.com/TextureGroup/Texture/pull/1957) ([ZevEisenberg](https://github.com/ZevEisenberg)) +- Fix WKWebView Accessibility [\#1955](https://github.com/TextureGroup/Texture/pull/1955) ([ZevEisenberg](https://github.com/ZevEisenberg)) +- fix missing hidden class [\#1952](https://github.com/TextureGroup/Texture/pull/1952) ([joprice](https://github.com/joprice)) +- use https for slack link [\#1950](https://github.com/TextureGroup/Texture/pull/1950) ([joprice](https://github.com/joprice)) +- Exposes a new option in ASImageDownloaderProtocol to retry image downloads [\#1948](https://github.com/TextureGroup/Texture/pull/1948) ([chggr](https://github.com/chggr)) +- All ASCellNode nodes to be non accessible if needed [\#1941](https://github.com/TextureGroup/Texture/pull/1941) ([decim92](https://github.com/decim92)) +- \[ASTextNode2\] Make some ASTextNode2 layout files public [\#1939](https://github.com/TextureGroup/Texture/pull/1939) ([rcancro](https://github.com/rcancro)) +- Ship ASExperimentalDispatchApply [\#1924](https://github.com/TextureGroup/Texture/pull/1924) ([nguyenhuy](https://github.com/nguyenhuy)) +- Fix failing ASConfigurationTests [\#1923](https://github.com/TextureGroup/Texture/pull/1923) ([nguyenhuy](https://github.com/nguyenhuy)) +- More on ASDataController's main-thread-only mode [\#1915](https://github.com/TextureGroup/Texture/pull/1915) ([nguyenhuy](https://github.com/nguyenhuy)) +- Add an experiment that makes ASDataController to do everything on main thread [\#1911](https://github.com/TextureGroup/Texture/pull/1911) ([nguyenhuy](https://github.com/nguyenhuy)) +- Disable text kit lock [\#1910](https://github.com/TextureGroup/Texture/pull/1910) ([garrettmoon](https://github.com/garrettmoon)) +- Do not expose tgmath.h to all clients of Texture [\#1900](https://github.com/TextureGroup/Texture/pull/1900) ([bolsinga](https://github.com/bolsinga)) +- Call will / did display node for ASTextNode. Fixes \#1680 [\#1893](https://github.com/TextureGroup/Texture/pull/1893) ([garrettmoon](https://github.com/garrettmoon)) +- Remove background deallocation helper code [\#1890](https://github.com/TextureGroup/Texture/pull/1890) ([bolsinga](https://github.com/bolsinga)) +- \[Accessibility\] Ship ASExperimentalDoNotCacheAccessibilityElements [\#1888](https://github.com/TextureGroup/Texture/pull/1888) ([rcancro](https://github.com/rcancro)) + ## [3.0.0](https://github.com/TextureGroup/Texture/tree/3.0.0) (2020-07-15) [Full Changelog](https://github.com/TextureGroup/Texture/compare/3.0.0-rc.2...3.0.0) From 64c49d2169d748a2f3f2e5207aa5bc45cbdfb6a7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 29 Sep 2021 08:00:34 -0700 Subject: [PATCH 10/13] [3.1.0] Update .github_changelog_generator --- .github_changelog_generator | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index 4df42730a..71166a913 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -1,3 +1,3 @@ issues=false -since-tag=3.0.0-rc.2 -future-release=3.0.0 +since-tag=3.1.0 +future-release=3.2.0 From 59431f84033154cc571121d2cce110364eaa847c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 29 Sep 2021 08:02:08 -0700 Subject: [PATCH 11/13] Update RELEASE.md --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index f7ea00e92..b494f6f8a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,7 @@ This document describes the process for a public Texture release. ### Process - Run `github_changelog_generator` in Texture project directory: `github_changelog_generator --token --user TextureGroup --project Texture`. To avoid hitting rate limit, the generator will replace the entire file with just the changes from this version – revert that giant deletion to get the entire new changelog. - Update `spec.version` within `Texture.podspec` and the `since-tag` and `future-release` fields in `.github_changelog_generator`. -- Create a new PR with the updated `Texture.podspec` and the newly generated changelog. +- Create a new PR with the updated `Texture.podspec` and `.github_changelog_generator`, and the newly generated changelog. - After merging in the PR, [create a new GitHub release](https://github.com/TextureGroup/Texture/releases/new). Use the generated changelog for the new release. - Push to Cocoapods with `pod trunk push` From da7a7069e1c5c8493a15b8385570d0b1c63fa7d3 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 29 Sep 2021 14:12:03 -0700 Subject: [PATCH 12/13] Remove AssetsLibrary dependency for tvOS (#2034) - The framework isn't available on tvOS. This causes CocoaPods linting to fail which prevented me from pushing the new release out. - One way to fix this is to have a different `default_subspecs` for tvOS that doesn't have AssetsLibrary subspec, but per-platform `default_subspecs` doesn't seem to be supported by CocoaPods. So I updated the subspec itself to only depend on the framework for iOS. This means the subspec is empty/useless for tvOS (and other platforms FWIW). - Tested with `pod spec lint Texture.podspec`. - Fixes #1992 and part of #1549. Also unblocks 3.1.0 release. - For the long term, we can remove the subspec entirely when iOS 9 is deprecated (#1828). --- Texture.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Texture.podspec b/Texture.podspec index 3c57948c8..86caec693 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -83,8 +83,8 @@ Pod::Spec.new do |spec| end spec.subspec 'AssetsLibrary' do |assetslib| - assetslib.frameworks = 'AssetsLibrary' - assetslib.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_ASSETS_LIBRARY=1' } + assetslib.ios.frameworks = 'AssetsLibrary' + assetslib.ios.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AS_USE_ASSETS_LIBRARY=1' } assetslib.dependency 'Texture/Core' end From b9f1713a8d74ab42b88a93f66742a4b07caf3db9 Mon Sep 17 00:00:00 2001 From: ricky Date: Thu, 2 Dec 2021 21:11:54 -0800 Subject: [PATCH 13/13] Try to fix the CI (#2047) It looks like github updated so that `macos-latest` is now macos-11. It can't find Xcode_11_5. Let's try to set the runs-on version explicitly and see if some magic happens. also allow warnings for podlint because that started failing too --- .github/workflows/ci-master-only.yml | 2 +- .github/workflows/ci-pull-requests-only.yml | 2 +- .github/workflows/ci.yml | 2 +- build.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-master-only.yml b/.github/workflows/ci-master-only.yml index a58671d5c..0362e641a 100644 --- a/.github/workflows/ci-master-only.yml +++ b/.github/workflows/ci-master-only.yml @@ -10,7 +10,7 @@ jobs: env: DEVELOPER_DIR: /Applications/Xcode_11.5.app/Contents/Developer name: Verify that podspec lints - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout the Git repository uses: actions/checkout@v2 diff --git a/.github/workflows/ci-pull-requests-only.yml b/.github/workflows/ci-pull-requests-only.yml index 859b525c7..f4d550f9d 100644 --- a/.github/workflows/ci-pull-requests-only.yml +++ b/.github/workflows/ci-pull-requests-only.yml @@ -18,7 +18,7 @@ jobs: - mode: cocoapods-lint-other-subspecs name: Verify that other subspecs lint name: ${{ matrix.name }} - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout the Git repository uses: actions/checkout@v2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb2212d03..27d20ade9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - mode: examples-pt4 name: Build examples (examples-pt4) name: ${{ matrix.name }} - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout the Git repository uses: actions/checkout@v2 diff --git a/build.sh b/build.sh index 547194124..eccbbcbe6 100755 --- a/build.sh +++ b/build.sh @@ -66,7 +66,7 @@ function build_example { # Lint subspec function lint_subspec { - set -o pipefail && pod env && pod lib lint --subspec="$1" + set -o pipefail && pod env && pod lib lint --allow-warnings --subspec="$1" } function cleanup {