Swiftgram/submodules/AsyncDisplayKit/Source/TextKit/ASTextKitFontSizeAdjuster.mm
Peter 9bc996374f Add 'submodules/AsyncDisplayKit/' from commit '02bedc12816e251ad71777f9d2578329b6d2bef6'
git-subtree-dir: submodules/AsyncDisplayKit
git-subtree-mainline: d06f423e0ed3df1fed9bd10d79ee312a9179b632
git-subtree-split: 02bedc12816e251ad71777f9d2578329b6d2bef6
2019-06-11 18:42:43 +01:00

242 lines
9.7 KiB
Plaintext

//
// ASTextKitFontSizeAdjuster.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASTextKitFontSizeAdjuster.h>
#if AS_ENABLE_TEXTNODE
#import <tgmath.h>
#import <mutex>
#import <AsyncDisplayKit/ASLayoutManager.h>
#import <AsyncDisplayKit/ASTextKitContext.h>
#import <AsyncDisplayKit/ASThread.h>
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
@interface ASTextKitFontSizeAdjuster()
@property (nonatomic, readonly) NSLayoutManager *sizingLayoutManager;
@property (nonatomic, readonly) NSTextContainer *sizingTextContainer;
@end
@implementation ASTextKitFontSizeAdjuster
{
__weak ASTextKitContext *_context;
ASTextKitAttributes _attributes;
BOOL _measured;
CGFloat _scaleFactor;
AS::Mutex __instanceLock__;
}
@synthesize sizingLayoutManager = _sizingLayoutManager;
@synthesize sizingTextContainer = _sizingTextContainer;
- (instancetype)initWithContext:(ASTextKitContext *)context
constrainedSize:(CGSize)constrainedSize
textKitAttributes:(const ASTextKitAttributes &)textComponentAttributes;
{
if (self = [super init]) {
_context = context;
_constrainedSize = constrainedSize;
_attributes = textComponentAttributes;
}
return self;
}
+ (void)adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString withScaleFactor:(CGFloat)scaleFactor
{
if (scaleFactor == 1.0) return;
[attrString beginEditing];
// scale all the attributes that will change the bounding box
[attrString enumerateAttributesInRange:NSMakeRange(0, attrString.length) options:0 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
if (attrs[NSFontAttributeName] != nil) {
UIFont *font = attrs[NSFontAttributeName];
font = [font fontWithSize:std::round(font.pointSize * scaleFactor)];
[attrString removeAttribute:NSFontAttributeName range:range];
[attrString addAttribute:NSFontAttributeName value:font range:range];
}
if (attrs[NSKernAttributeName] != nil) {
NSNumber *kerning = attrs[NSKernAttributeName];
[attrString removeAttribute:NSKernAttributeName range:range];
[attrString addAttribute:NSKernAttributeName value:@([kerning floatValue] * scaleFactor) range:range];
}
if (attrs[NSParagraphStyleAttributeName] != nil) {
NSMutableParagraphStyle *paragraphStyle = [attrs[NSParagraphStyleAttributeName] mutableCopy];
paragraphStyle.lineSpacing = (paragraphStyle.lineSpacing * scaleFactor);
paragraphStyle.paragraphSpacing = (paragraphStyle.paragraphSpacing * scaleFactor);
paragraphStyle.firstLineHeadIndent = (paragraphStyle.firstLineHeadIndent * scaleFactor);
paragraphStyle.headIndent = (paragraphStyle.headIndent * scaleFactor);
paragraphStyle.tailIndent = (paragraphStyle.tailIndent * scaleFactor);
paragraphStyle.minimumLineHeight = (paragraphStyle.minimumLineHeight * scaleFactor);
paragraphStyle.maximumLineHeight = (paragraphStyle.maximumLineHeight * scaleFactor);
[attrString removeAttribute:NSParagraphStyleAttributeName range:range];
[attrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
}
}];
[attrString endEditing];
}
- (NSUInteger)lineCountForString:(NSAttributedString *)attributedString
{
NSUInteger lineCount = 0;
NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager];
NSTextContainer *sizingTextContainer = [self sizingTextContainer];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
[textStorage addLayoutManager:sizingLayoutManager];
[sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer];
for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [sizingLayoutManager numberOfGlyphs] && lineCount <= _attributes.maximumNumberOfLines; lineCount++) {
[sizingLayoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange];
}
[textStorage removeLayoutManager:sizingLayoutManager];
return lineCount;
}
- (CGSize)boundingBoxForString:(NSAttributedString *)attributedString
{
NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager];
NSTextContainer *sizingTextContainer = [self sizingTextContainer];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
[textStorage addLayoutManager:sizingLayoutManager];
[sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer];
CGRect textRect = [sizingLayoutManager boundingRectForGlyphRange:NSMakeRange(0, [textStorage length])
inTextContainer:sizingTextContainer];
[textStorage removeLayoutManager:sizingLayoutManager];
return textRect.size;
}
- (NSLayoutManager *)sizingLayoutManager
{
AS::MutexLocker l(__instanceLock__);
if (_sizingLayoutManager == nil) {
_sizingLayoutManager = [[ASLayoutManager alloc] init];
_sizingLayoutManager.usesFontLeading = NO;
if (_sizingTextContainer == nil) {
// make this text container unbounded in height so that the layout manager will compute the total
// number of lines and not stop counting when height runs out.
_sizingTextContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(_constrainedSize.width, CGFLOAT_MAX)];
_sizingTextContainer.lineFragmentPadding = 0;
// use 0 regardless of what is in the attributes so that we get an accurate line count
_sizingTextContainer.maximumNumberOfLines = 0;
_sizingTextContainer.lineBreakMode = _attributes.lineBreakMode;
_sizingTextContainer.exclusionPaths = _attributes.exclusionPaths;
}
[_sizingLayoutManager addTextContainer:_sizingTextContainer];
}
return _sizingLayoutManager;
}
- (CGFloat)scaleFactor
{
if (_measured) {
return _scaleFactor;
}
if ([_attributes.pointSizeScaleFactors count] == 0 || isinf(_constrainedSize.width)) {
_measured = YES;
_scaleFactor = 1.0;
return _scaleFactor;
}
__block CGFloat adjustedScale = 1.0;
// We add the scale factor of 1 to our scaleFactors array so that in the first iteration of the loop below, we are
// actually determining if we need to scale at all. If something doesn't fit, we will continue to iterate our scale factors.
NSArray *scaleFactors = [@[@(1)] arrayByAddingObjectsFromArray:_attributes.pointSizeScaleFactors];
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
// Check for two different situations (and correct for both)
// 1. The longest word in the string fits without being wrapped
// 2. The entire text fits in the given constrained size.
NSString *str = textStorage.string;
NSArray *words = [str componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *longestWordNeedingResize = @"";
for (NSString *word in words) {
if ([word length] > [longestWordNeedingResize length]) {
longestWordNeedingResize = word;
}
}
// check to see if we may need to shrink for any of these things
BOOL longestWordFits = [longestWordNeedingResize length] ? NO : YES;
BOOL maxLinesFits = self->_attributes.maximumNumberOfLines > 0 ? NO : YES;
BOOL heightFits = isinf(self->_constrainedSize.height) ? YES : NO;
CGSize longestWordSize = CGSizeZero;
if (longestWordFits == NO) {
NSRange longestWordRange = [str rangeOfString:longestWordNeedingResize];
NSAttributedString *attrString = [textStorage attributedSubstringFromRange:longestWordRange];
longestWordSize = [attrString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
}
// we may need to shrink for some reason, so let's iterate through our scale factors to see if we actually need to shrink
// Note: the first scale factor in the array is 1.0 so will make sure that things don't fit without shrinking
for (NSNumber *adjustedScaleObj in scaleFactors) {
if (longestWordFits && maxLinesFits && heightFits) {
break;
}
adjustedScale = [adjustedScaleObj floatValue];
if (longestWordFits == NO) {
// we need to check the longest word to make sure it fits
longestWordFits = std::ceil((longestWordSize.width * adjustedScale) <= self->_constrainedSize.width);
}
// if the longest word fits, go ahead and check max line and height. If it didn't fit continue to the next scale factor
if (longestWordFits == YES) {
// scale our string by the current scale factor
NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage];
[[self class] adjustFontSizeForAttributeString:scaledString withScaleFactor:adjustedScale];
// check to see if this scaled string fit in the max lines
if (maxLinesFits == NO) {
maxLinesFits = ([self lineCountForString:scaledString] <= self->_attributes.maximumNumberOfLines);
}
// if max lines still doesn't fit, continue without checking that we fit in the constrained height
if (maxLinesFits == YES && heightFits == NO) {
// max lines fit so make sure that we fit in the constrained height.
CGSize stringSize = [self boundingBoxForString:scaledString];
heightFits = (stringSize.height <= self->_constrainedSize.height);
}
}
}
}];
_measured = YES;
_scaleFactor = adjustedScale;
return _scaleFactor;
}
@end
#endif