Skip to content

Commit

Permalink
Expose a way to determine if a text node will truncate for a given co…
Browse files Browse the repository at this point in the history
…nstrained size #trivial (#1177)

* Expose textLayoutForConstraint:

- Expose textLayoutForConstraint:, but make unavailable on ASTextNode
- Refactor compatibleLayoutWithContainer:text: into a static method

* Instead of textLayoutForConstraint: expose shouldTruncateForConstrainedSize: in ASTextNode
  • Loading branch information
maicki committed Oct 21, 2018
1 parent c958812 commit 776e475
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 98 deletions.
5 changes: 5 additions & 0 deletions Source/ASTextNode+Beta.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (readonly) BOOL usingExperiment;

/**
* Returns a Boolean indicating if the text node will truncate for the given constrained size
*/
- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize;

@end

NS_ASSUME_NONNULL_END
5 changes: 5 additions & 0 deletions Source/ASTextNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1218,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<NSNumber *> *)pointSizeScaleFactors
{
if (ASLockedSelfCompareAssignCopy(_pointSizeScaleFactors, pointSizeScaleFactors)) {
Expand Down
204 changes: 106 additions & 98 deletions Source/ASTextNode2.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSAttributedString *, ASTextCacheValue *> *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";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<NSAttributedString *, ASTextCacheValue *> *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;
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -1086,8 +1083,19 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode

- (BOOL)isTruncated
{
AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE();
return NO;
return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine == nil);
}

- (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize
{
return ASLockedSelf([self locked_textLayoutForSize:constrainedSize.max].truncatedLine == nil);
}

- (ASTextLayout *)locked_textLayoutForSize:(CGSize)size
{
ASTextContainer *container = [_textContainer copy];
container.size = size;
return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText);
}

- (NSUInteger)maximumNumberOfLines
Expand Down

0 comments on commit 776e475

Please sign in to comment.