forked from TextureGroup/Texture
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathASTextNodeWordKerner.mm
More file actions
130 lines (109 loc) · 6.47 KB
/
Copy pathASTextNodeWordKerner.mm
File metadata and controls
130 lines (109 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//
// ASTextNodeWordKerner.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/ASTextNodeWordKerner.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASTextNodeTypes.h>
@implementation ASTextNodeWordKerner
#pragma mark - NSLayoutManager Delegate
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange
{
NSUInteger glyphCount = glyphRange.length;
NSGlyphProperty *newGlyphProperties = NULL;
BOOL usesWordKerning = NO;
// If our typing attributes specify word kerning, specify the spaces as whitespace control characters so we can customize their width.
// Are any of the characters spaces?
NSString *textStorageString = layoutManager.textStorage.string;
for (NSUInteger arrayIndex = 0; arrayIndex < glyphCount; arrayIndex++) {
NSUInteger characterIndex = characterIndexes[arrayIndex];
if ([textStorageString characterAtIndex:characterIndex] != ' ')
continue;
// If we've set the whitespace control character for this space already, we have nothing to do.
if (properties[arrayIndex] == NSGlyphPropertyControlCharacter) {
usesWordKerning = YES;
continue;
}
// Create new glyph properties, if necessary.
if (!newGlyphProperties) {
newGlyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount);
memcpy(newGlyphProperties, properties, (sizeof(NSGlyphProperty) * glyphCount));
}
// It's a space. Make it a whitespace control character.
newGlyphProperties[arrayIndex] = NSGlyphPropertyControlCharacter;
}
// If we don't have any custom glyph properties, return 0 to indicate to the layout manager that it should use the standard glyphs+properties.
if (!newGlyphProperties) {
if (usesWordKerning) {
// If the text does use word kerning we have to make sure we return the correct glyphCount, or the layout manager will just use the default properties and ignore our kerning.
[layoutManager setGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
return glyphCount;
} else {
return 0;
}
}
// Otherwise, use our custom glyph properties.
[layoutManager setGlyphs:glyphs properties:newGlyphProperties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
free(newGlyphProperties);
return glyphCount;
}
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex
{
// If it's a space character and we have custom word kerning, use the whitespace action control character.
if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ')
return NSControlCharacterActionWhitespace;
return defaultAction;
}
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex
{
CGFloat wordKernedSpaceWidth = [self _wordKernedSpaceWidthForCharacterAtIndex:characterIndex atGlyphPosition:glyphPosition forTextContainer:textContainer layoutManager:layoutManager];
return CGRectMake(glyphPosition.x, glyphPosition.y, wordKernedSpaceWidth, CGRectGetHeight(proposedRect));
}
- (CGFloat)_wordKernedSpaceWidthForCharacterAtIndex:(NSUInteger)characterIndex atGlyphPosition:(CGPoint)glyphPosition forTextContainer:(NSTextContainer *)textContainer layoutManager:(NSLayoutManager *)layoutManager
{
// We use a map table for pointer equality and non-copying keys.
static NSMapTable *spaceSizes;
// NSMapTable is a defined thread unsafe class, so we need to synchronize
// access in a light manner. So we use dispatch_sync on this queue for all
// access to the map table.
static dispatch_queue_t mapQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
spaceSizes = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:1];
mapQueue = dispatch_queue_create("org.AsyncDisplayKit.wordKerningQueue", DISPATCH_QUEUE_SERIAL);
});
CGFloat ordinarySpaceWidth;
UIFont *font = [layoutManager.textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
CGFloat wordKerning = [[layoutManager.textStorage attribute:ASTextNodeWordKerningAttributeName atIndex:characterIndex effectiveRange:NULL] floatValue];
__block NSNumber *ordinarySpaceSizeValue;
dispatch_sync(mapQueue, ^{
ordinarySpaceSizeValue = [spaceSizes objectForKey:font];
});
if (ordinarySpaceSizeValue == nil) {
ordinarySpaceWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width;
dispatch_async(mapQueue, ^{
[spaceSizes setObject:@(ordinarySpaceWidth) forKey:font];
});
} else {
ordinarySpaceWidth = [ordinarySpaceSizeValue floatValue];
}
CGFloat totalKernedWidth = (ordinarySpaceWidth + wordKerning);
// TextKit normally handles whitespace by increasing the advance of the previous glyph, rather than displaying an
// actual glyph for the whitespace itself. However, in order to implement word kerning, we explicitly require a
// discrete glyph whose bounding box we can specify. The problem is that TextKit does not know this glyph is
// invisible. From TextKit's perspective, this whitespace glyph is a glyph that MUST be displayed. Thus when it
// comes to determining linebreaks, the width of this trailing whitespace glyph is considered. This causes
// our text to wrap sooner than it otherwise would, as room is allocated at the end of each line for a glyph that
// isn't actually visible. To implement our desired behavior, we check to see if the current whitespace glyph
// would break to the next line. If it breaks to the next line, then this constitutes trailing whitespace, and
// we specify enough room to fill up the remainder of the line, but nothing more.
if (glyphPosition.x + totalKernedWidth > textContainer.size.width) {
return (textContainer.size.width - glyphPosition.x);
}
return totalKernedWidth;
}
@end