From 60c12b32e1b918c428dec684541c49e9e2ab49ce Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 16 Oct 2018 15:47:18 -0700 Subject: [PATCH 1/2] Expose textLayoutForConstraint: - Expose textLayoutForConstraint:, but make unavailable on ASTextNode - Refactor compatibleLayoutWithContainer:text: into a static method --- Source/ASTextNode+Beta.h | 7 ++ Source/ASTextNode.mm | 10 ++ Source/ASTextNode2.mm | 197 ++++++++++++++++++++------------------- 3 files changed, 118 insertions(+), 96 deletions(-) diff --git a/Source/ASTextNode+Beta.h b/Source/ASTextNode+Beta.h index 09259bd7f..7fd48939f 100644 --- a/Source/ASTextNode+Beta.h +++ b/Source/ASTextNode+Beta.h @@ -11,6 +11,8 @@ #import +@class ASTextLayout; + NS_ASSUME_NONNULL_BEGIN @interface ASTextNode () @@ -35,6 +37,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly) BOOL usingExperiment; +/* + * Returns layout for textNode that fit to constrainedSize's max size. + */ +- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index c29d03312..3fb4afb35 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -28,6 +28,8 @@ #import #import +#import + #import #import @@ -1384,6 +1386,14 @@ + (id)allocWithZone:(struct _NSZone *)zone } } +#pragma mark - Unavailable + +- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize +{ + ASDisplayNodeFailAssert(@"This method is only available in ASTextNode2"); + return nil; +} + @end @implementation ASTextNode (Deprecated) diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index cca660f13..fee2970cb 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -55,6 +55,95 @@ @implementation ASTextCacheValue });\ } +/** + * If it can't find a compatible layout, this method creates one. + * + * NOTE: Be careful to copy `text` if needed. + */ +static NS_RETURNS_RETAINED ASTextLayout *ASTextNodeCompatibleLayoutWithContainerAndText(ASTextContainer *container, NSAttributedString *text) { + // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) + static ASDN::StaticMutex& layoutCacheLock = *new ASDN::StaticMutex; + static NSCache *textLayoutCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + textLayoutCache = [[NSCache alloc] init]; + }); + + layoutCacheLock.lock(); + + ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; + if (cacheValue == nil) { + cacheValue = [[ASTextCacheValue alloc] init]; + [textLayoutCache setObject:cacheValue forKey:[text copy]]; + } + + // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. + ASDN::MutexLocker lock(cacheValue->_m); + layoutCacheLock.unlock(); + + CGRect containerBounds = (CGRect){ .size = container.size }; + { + for (let &t : cacheValue->_layouts) { + CGSize constrainedSize = std::get<0>(t); + ASTextLayout *layout = std::get<1>(t); + + CGSize layoutSize = layout.textBoundingSize; + // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. + // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. + // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. + // 3. Thus, those two values (constrained width & returned width) form a range, where + // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our + // width is in that range, between the min and max of those two values. + CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, constrainedSize.width), MIN(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(containerBounds, minRect)) { + continue; + } + CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, constrainedSize.width), MAX(layoutSize.height, constrainedSize.height)); + if (!CGRectContainsRect(maxRect, containerBounds)) { + continue; + } + if (!CGSizeEqualToSize(container.size, constrainedSize)) { + continue; + } + + // Now check container params. + ASTextContainer *otherContainer = layout.container; + if (!UIEdgeInsetsEqualToEdgeInsets(container.insets, otherContainer.insets)) { + continue; + } + if (!ASObjectIsEqual(container.exclusionPaths, otherContainer.exclusionPaths)) { + continue; + } + if (container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { + continue; + } + if (container.truncationType != otherContainer.truncationType) { + continue; + } + if (!ASObjectIsEqual(container.truncationToken, otherContainer.truncationToken)) { + continue; + } + // TODO: When we get a cache hit, move this entry to the front (LRU). + return layout; + } + } + + // Cache Miss. Compute the text layout. + ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; + + // 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(); + } + } + + return layout; +} + static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; @@ -256,7 +345,7 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; [self prepareAttributedString:mutableText isForIntrinsicSize:isCalculatingIntrinsicSize]; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:_textContainer text:mutableText]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, mutableText); return layout.textBoundingSize; } @@ -428,104 +517,12 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer }; } -/** - * If it can't find a compatible layout, this method creates one. - * - * NOTE: Be careful to copy `text` if needed. - */ -+ (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container - text:(NSAttributedString *)text NS_RETURNS_RETAINED - -{ - // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) - static ASDN::StaticMutex& layoutCacheLock = *new ASDN::StaticMutex; - static NSCache *textLayoutCache; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - textLayoutCache = [[NSCache alloc] init]; - }); - - layoutCacheLock.lock(); - - ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; - if (cacheValue == nil) { - cacheValue = [[ASTextCacheValue alloc] init]; - [textLayoutCache setObject:cacheValue forKey:[text copy]]; - } - - // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. - ASDN::MutexLocker lock(cacheValue->_m); - layoutCacheLock.unlock(); - - CGRect containerBounds = (CGRect){ .size = container.size }; - { - for (let &t : cacheValue->_layouts) { - CGSize constrainedSize = std::get<0>(t); - ASTextLayout *layout = std::get<1>(t); - - CGSize layoutSize = layout.textBoundingSize; - // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. - // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. - // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. - // 3. Thus, those two values (constrained width & returned width) form a range, where - // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our - // width is in that range, between the min and max of those two values. - CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, constrainedSize.width), MIN(layoutSize.height, constrainedSize.height)); - if (!CGRectContainsRect(containerBounds, minRect)) { - continue; - } - CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, constrainedSize.width), MAX(layoutSize.height, constrainedSize.height)); - if (!CGRectContainsRect(maxRect, containerBounds)) { - continue; - } - if (!CGSizeEqualToSize(container.size, constrainedSize)) { - continue; - } - - // Now check container params. - ASTextContainer *otherContainer = layout.container; - if (!UIEdgeInsetsEqualToEdgeInsets(container.insets, otherContainer.insets)) { - continue; - } - if (!ASObjectIsEqual(container.exclusionPaths, otherContainer.exclusionPaths)) { - continue; - } - if (container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { - continue; - } - if (container.truncationType != otherContainer.truncationType) { - continue; - } - if (!ASObjectIsEqual(container.truncationToken, otherContainer.truncationToken)) { - continue; - } - // TODO: When we get a cache hit, move this entry to the front (LRU). - return layout; - } - } - - // Cache Miss. Compute the text layout. - ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; - - // 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(); - } - } - - return layout; -} - + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing { ASTextContainer *container = layoutDict[@"container"]; NSAttributedString *text = layoutDict[@"text"]; UIColor *bgColor = layoutDict[@"bgColor"]; - ASTextLayout *layout = [self compatibleLayoutWithContainer:container text:text]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(container, text); if (isCancelledBlock()) { return; @@ -574,7 +571,7 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:containerCopy text:_attributedText]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); NSRange visibleRange = layout.visibleRange; NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); @@ -830,7 +827,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:containerCopy text:_attributedText]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); visibleRange = layout.visibleRange; } NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; @@ -1090,6 +1087,14 @@ - (BOOL)isTruncated return NO; } +- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize +{ + ASLockScopeSelf(); + ASTextContainer *container = [_textContainer copy]; + container.size = constrainedSize.max; + return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText); +} + - (NSUInteger)maximumNumberOfLines { // _textContainer is invariant and this is just atomic access. From 14a905cf7d1ba8812f6f0f86d0727fb231ce03d7 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 17 Oct 2018 15:45:43 -0700 Subject: [PATCH 2/2] Instead of textLayoutForConstraint: expose shouldTruncateForConstrainedSize: in ASTextNode --- Source/ASTextNode+Beta.h | 8 +++----- Source/ASTextNode.mm | 15 +++++---------- Source/ASTextNode2.mm | 13 ++++++++----- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Source/ASTextNode+Beta.h b/Source/ASTextNode+Beta.h index 7fd48939f..ad897c5f0 100644 --- a/Source/ASTextNode+Beta.h +++ b/Source/ASTextNode+Beta.h @@ -11,8 +11,6 @@ #import -@class ASTextLayout; - NS_ASSUME_NONNULL_BEGIN @interface ASTextNode () @@ -37,10 +35,10 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly) BOOL usingExperiment; -/* - * Returns layout for textNode that fit to constrainedSize's max size. +/** + * Returns a Boolean indicating if the text node will truncate for the given constrained size */ -- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize; +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize; @end diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 3fb4afb35..accbea3ad 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -28,8 +28,6 @@ #import #import -#import - #import #import @@ -1220,6 +1218,11 @@ - (BOOL)isTruncated return ASLockedSelf([[self _locked_renderer] isTruncated]); } +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize +{ + return ASLockedSelf([[self _locked_rendererWithBounds:{.size = constrainedSize.max}] isTruncated]); +} + - (void)setPointSizeScaleFactors:(NSArray *)pointSizeScaleFactors { if (ASLockedSelfCompareAssignCopy(_pointSizeScaleFactors, pointSizeScaleFactors)) { @@ -1386,14 +1389,6 @@ + (id)allocWithZone:(struct _NSZone *)zone } } -#pragma mark - Unavailable - -- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize -{ - ASDisplayNodeFailAssert(@"This method is only available in ASTextNode2"); - return nil; -} - @end @implementation ASTextNode (Deprecated) diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index fee2970cb..6d894be82 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -1083,15 +1083,18 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode - (BOOL)isTruncated { - AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - return NO; + return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine == nil); } -- (ASTextLayout *)textLayoutForConstraint:(ASSizeRange)constrainedSize +- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize +{ + return ASLockedSelf([self locked_textLayoutForSize:constrainedSize.max].truncatedLine == nil); +} + +- (ASTextLayout *)locked_textLayoutForSize:(CGSize)size { - ASLockScopeSelf(); ASTextContainer *container = [_textContainer copy]; - container.size = constrainedSize.max; + container.size = size; return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText); }