Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose a way to determine if a text node will truncate for a given constrained size #trivial #1177

Merged
merged 2 commits into from
Oct 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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