Swiftgram/AsyncDisplayKit/Details/ASTextNodeRenderer.mm
2014-09-24 17:56:19 -07:00

628 lines
26 KiB
Plaintext

/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "ASTextNodeRenderer.h"
#import <CoreText/CoreText.h>
#import "ASAssert.h"
#import "ASTextNodeTextKitHelpers.h"
#import "ASTextNodeWordKerner.h"
#import "ASThread.h"
static const CGFloat ASTextNodeRendererGlyphTouchHitSlop = 5.0;
static const CGFloat ASTextNodeRendererTextCapHeightPadding = 1.3;
@interface ASTextNodeRenderer ()
@end
@implementation ASTextNodeRenderer {
CGSize _constrainedSize;
CGSize _calculatedSize;
NSAttributedString *_attributedString;
NSAttributedString *_truncationString;
NSLineBreakMode _truncationMode;
NSRange _truncationCharacterRange;
NSRange _visibleRange;
ASTextNodeWordKerner *_wordKerner;
ASDN::RecursiveMutex _textKitLock;
NSLayoutManager *_layoutManager;
NSTextStorage *_textStorage;
NSTextContainer *_textContainer;
}
#pragma mark - Initialization
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
truncationString:(NSAttributedString *)truncationString
truncationMode:(NSLineBreakMode)truncationMode
constrainedSize:(CGSize)constrainedSize
{
if (self = [super init]) {
_attributedString = attributedString;
_truncationString = truncationString;
_truncationMode = truncationMode;
_truncationCharacterRange = NSMakeRange(NSNotFound, truncationString.length);
_constrainedSize = constrainedSize;
}
return self;
}
/*
* Use this method to lazily construct the TextKit components.
*/
- (void)_initializeTextKitComponentsIfNeeded
{
ASDN::MutexLocker l(_textKitLock);
if (_layoutManager == nil) {
[self _initializeTextKitComponentsWithAttributedString:_attributedString];
}
}
- (void)_initializeTextKitComponentsWithAttributedString:(NSAttributedString *)attributedString
{
ASDN::MutexLocker l(_textKitLock);
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. :(
static ASDN::StaticMutex mutex = ASDISPLAYNODE_MUTEX_INITIALIZER;
ASDN::StaticMutexLocker gl(mutex);
// Create the TextKit component stack with our default configuration.
_textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]);
_layoutManager = [[NSLayoutManager alloc] init];
_layoutManager.usesFontLeading = NO;
_wordKerner = [[ASTextNodeWordKerner alloc] init];
_layoutManager.delegate = _wordKerner;
[_textStorage addLayoutManager:_layoutManager];
_textContainer = [[NSTextContainer alloc] initWithSize:_constrainedSize];
// We want the text laid out up to the very edges of the container.
_textContainer.lineFragmentPadding = 0;
// Translate our truncation mode into a line break mode on the container
_textContainer.lineBreakMode = _truncationMode;
[_layoutManager addTextContainer:_textContainer];
ASDN::StaticMutexUnlocker gu(mutex);
[self _invalidateLayout];
}
#pragma mark - Layout Initialization
- (void)_invalidateLayout
{
ASDN::MutexLocker l(_textKitLock);
// Force a layout, which means we have to recompute our truncation parameters
NSInteger originalStringLength = _textStorage.string.length;
[self _calculateSize];
NSRange visibleGlyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
_visibleRange = [_layoutManager characterRangeForGlyphRange:visibleGlyphRange actualGlyphRange:NULL];
// Check if text is truncated, and if so apply our truncation string
if (_visibleRange.length < originalStringLength && _truncationString.length > 0) {
NSInteger firstCharacterIndexToReplace = [self _calculateCharacterIndexBeforeTruncationMessage];
if (firstCharacterIndexToReplace == 0 || firstCharacterIndexToReplace == NSNotFound) {
// Something went horribly wrong, short-circuit
[self _calculateSize];
return;
}
// Update/truncate the visible range of text
_visibleRange = NSMakeRange(0, firstCharacterIndexToReplace);
NSRange truncationReplacementRange = NSMakeRange(firstCharacterIndexToReplace, _textStorage.length - firstCharacterIndexToReplace);
// Replace the end of the visible message with the truncation string
[_textStorage replaceCharactersInRange:truncationReplacementRange
withAttributedString:_truncationString];
_truncationCharacterRange = NSMakeRange(firstCharacterIndexToReplace, _truncationString.length);
// We must recompute the calculated size because we may have changed it in
// changing the string
[self _calculateSize];
}
}
#pragma mark - Sizing
/*
* Calculates the size of the text in the renderer based on the parameters
* stored in the ivars of this class.
*
* This method can be expensive, so it is important that it not be called
* frequently. It not only sizes the text, but it also configures the TextKit
* components for drawing, and responding to all other queries made to this
* class.
*/
- (void)_calculateSize
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
// Force glyph generation and layout, which may not have happened yet (and
// isn't triggered by -usedRectForTextContainer:).
[_layoutManager ensureLayoutForTextContainer:_textContainer];
CGRect constrainedRect = CGRect{CGPointZero, _constrainedSize};
CGRect boundingRect = [_layoutManager usedRectForTextContainer:_textContainer];
// TextKit often returns incorrect glyph bounding rects in the horizontal
// direction, so we clip to our bounding rect to make sure our width
// calculations aren't being offset by glyphs going beyond the constrained
// rect.
boundingRect = CGRectIntersection(boundingRect, (CGRect){.size = constrainedRect.size});
_calculatedSize = boundingRect.size;
}
- (CGSize)size
{
[self _initializeTextKitComponentsIfNeeded];
return _calculatedSize;
}
#pragma mark - Layout
- (CGRect)trailingRect
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
// If have an empty string, then our whole bounds constitute trailing space.
if ([_textStorage length] == 0) {
return CGRectMake(0, 0, _calculatedSize.width, _calculatedSize.height);
}
// Take everything after our final character as trailing space.
NSArray *finalRects = [self rectsForTextRange:NSMakeRange([_textStorage length] - 1, 1) measureOption:ASTextNodeRendererMeasureOptionLineHeight];
CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue];
CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect));
CGSize size = CGSizeMake(_calculatedSize.width - origin.x, _calculatedSize.height - origin.y);
return (CGRect){origin, size};
}
- (CGRect)frameForTextRange:(NSRange)textRange
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
// Bail on invalid range.
if (NSMaxRange(textRange) > [_textStorage length]) {
ASDisplayNodeAssertNotNil(nil, @"Invalid range");
return CGRectZero;
}
// Force glyph generation and layout.
[_layoutManager ensureLayoutForTextContainer:_textContainer];
NSRange glyphRange = [_layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
CGRect textRect = [_layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textContainer];
return textRect;
}
- (NSArray *)rectsForTextRange:(NSRange)textRange
measureOption:(ASTextNodeRendererMeasureOption)measureOption
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
BOOL textRangeIsValid = (NSMaxRange(textRange) <= [_textStorage length]);
ASDisplayNodeAssertTrue(textRangeIsValid);
if (!textRangeIsValid) {
return @[];
}
// Used for block measure option
__block CGRect firstRect = CGRectNull;
__block CGRect lastRect = CGRectNull;
__block CGRect blockRect = CGRectNull;
NSMutableArray *textRects = [NSMutableArray array];
NSString *string = _textStorage.string;
NSRange totalGlyphRange = [_layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
[_layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
CGRect lineRect = CGRectNull;
// If we're empty, don't bother looping through glyphs, use the default.
if (CGRectIsEmpty(usedRect)) {
lineRect = usedRect;
} else {
// TextKit's bounding rect computations are just a touch off, so we actually
// compose the rects by hand from the center of the given TextKit bounds and
// imposing the font attributes returned by the glyph's font.
NSRange lineGlyphRange = NSIntersectionRange(totalGlyphRange, glyphRange);
for (NSUInteger i = lineGlyphRange.location; i < NSMaxRange(lineGlyphRange) && i < string.length; i++) {
// We grab the properly sized rect for the glyph
CGRect properGlyphRect = [self _rectForGlyphAtIndex:i measureOption:measureOption];
// Don't count empty glyphs towards our line rect.
if (!CGRectIsEmpty(properGlyphRect)) {
lineRect = CGRectIsNull(lineRect) ? properGlyphRect
: CGRectUnion(lineRect, properGlyphRect);
}
}
}
if (!CGRectIsNull(lineRect)) {
if (measureOption == ASTextNodeRendererMeasureOptionBlock) {
// For the block measurement option we store the first & last rect as
// special cases, then merge everything else into a single block rect
if (CGRectIsNull(firstRect)) {
// We don't have a firstRect, so we must be on the first line.
firstRect = lineRect;
} else if(CGRectIsNull(lastRect)) {
// We don't have a lastRect, but we do have a firstRect, so we must
// be on the second line. No need to merge in the blockRect just yet
lastRect = lineRect;
} else if(CGRectIsNull(blockRect)) {
// We have both a first and last rect, so we must be on the third line
// we don't have any blockRect to merge it into, so we just set it
// directly.
blockRect = lastRect;
lastRect = lineRect;
} else {
// Everything is already set, so we just merge this line into the
// block.
blockRect = CGRectUnion(blockRect, lastRect);
lastRect = lineRect;
}
} else {
// If the block option isn't being used then each line is being treated
// individually.
[textRects addObject:[NSValue valueWithCGRect:lineRect]];
}
}
}];
if (measureOption == ASTextNodeRendererMeasureOptionBlock) {
// Block measure option is handled differently with just 3 vars for the entire range.
if (!CGRectIsNull(firstRect)) {
if (!CGRectIsNull(blockRect)) {
CGFloat rightEdge = MAX(CGRectGetMaxX(blockRect), CGRectGetMaxX(lastRect));
if (rightEdge > CGRectGetMaxX(firstRect)) {
// Force the right side of the first rect to properly align with the
// right side of the rightmost of the block and last rect
firstRect.size.width += rightEdge - CGRectGetMaxX(firstRect);
}
// Force the left side of the block rect to properly align with the
// left side of the leftmost of the first and last rect
blockRect.origin.x = MIN(CGRectGetMinX(firstRect), CGRectGetMinX(lastRect));
// Force the right side of the block rect to properly align with the
// right side of the rightmost of the first and last rect
blockRect.size.width += MAX(CGRectGetMaxX(firstRect), CGRectGetMaxX(lastRect)) - CGRectGetMaxX(blockRect);
}
if (!CGRectIsNull(lastRect)) {
// Force the left edge of the last rect to properly align with the
// left side of the leftmost of the first and block rect, if necessary.
CGFloat leftEdge = MIN(CGRectGetMinX(blockRect), CGRectGetMinX(firstRect));
CGFloat lastRectNudgeAmount = MAX(CGRectGetMinX(lastRect) - leftEdge, 0);
lastRect.origin.x = MIN(leftEdge, CGRectGetMinX(lastRect));
lastRect.size.width += lastRectNudgeAmount;
}
[textRects addObject:[NSValue valueWithCGRect:firstRect]];
}
if (!CGRectIsNull(blockRect)) {
[textRects addObject:[NSValue valueWithCGRect:blockRect]];
}
if (!CGRectIsNull(lastRect)) {
[textRects addObject:[NSValue valueWithCGRect:lastRect]];
}
}
return textRects;
}
- (CGRect)_rectForGlyphAtIndex:(NSUInteger)glyphIndex
measureOption:(ASTextNodeRendererMeasureOption)measureOption
{
ASDN::MutexLocker l(_textKitLock);
NSUInteger charIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex];
CGGlyph glyph = [_layoutManager glyphAtIndex:glyphIndex];
CTFontRef font = (__bridge_retained CTFontRef)[_textStorage attribute:NSFontAttributeName
atIndex:charIndex
effectiveRange:NULL];
if (font == nil) {
font = (__bridge_retained CTFontRef)[UIFont systemFontOfSize:12.0];
}
// Glyph Advance
// +-------------------------+
// | |
// | |
// +------------------------+--|-------------------------|--+-----------+-----+ What TextKit returns sometimes
// | | | XXXXXXXXXXX + | | | (approx. correct height, but
// | ---------|--+---------+ XXX XXXX +|-----------|-----| sometimes inaccurate bounding
// | | | XXX XXXXX| | | widths)
// | | | XX XX | | |
// | | | XX | | |
// | | | XXX | | |
// | | | XX | | |
// | | | XXXXXXXXXXX | | |
// | Cap Height->| | XX | | |
// | | | XX | Ascent-->| |
// | | | XX | | |
// | | | XX | | |
// | | | X | | |
// | | | X | | |
// | | | X | | |
// | | | XX | | |
// | | | X | | |
// | ---------|-------+ X +-------------------------------------|
// | | XX | |
// | | X | |
// | | XX Descent------>| |
// | | XXXXXX | |
// | | XXX | |
// +------------------------+-------------------------------------------------+
// |
// +--+Actual bounding box
CGRect glyphRect = [_layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
inTextContainer:_textContainer];
// If it is a NSTextAttachment, we don't have the matched glyph and use width of glyphRect instead of advance.
CGFloat advance = (glyph == kCGFontIndexInvalid) ? glyphRect.size.width : CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, NULL, 1);
// We treat the center of the glyph's bounding box as the center of our new rect
CGPoint glyphCenter = CGPointMake(CGRectGetMidX(glyphRect), CGRectGetMidY(glyphRect));
CGRect properGlyphRect;
if (measureOption == ASTextNodeRendererMeasureOptionCapHeight
|| measureOption == ASTextNodeRendererMeasureOptionBlock) {
CGFloat ascent = CTFontGetAscent(font);
CGFloat descent = CTFontGetDescent(font);
CGFloat capHeight = CTFontGetCapHeight(font);
CGFloat leading = CTFontGetLeading(font);
CGFloat glyphHeight = ascent + descent;
// For visual balance, we add the cap height padding above the cap, and
// below the baseline, we scale by the descent so it grows with the size of
// the text.
CGFloat topPadding = ASTextNodeRendererTextCapHeightPadding * descent;
CGFloat bottomPadding = topPadding;
properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5,
glyphCenter.y - glyphHeight * 0.5 + (ascent - capHeight) - topPadding + leading,
advance,
capHeight + topPadding + bottomPadding);
} else {
// We are just measuring the line heights here, so we can use the
// heights used by TextKit, which tend to be pretty good.
properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5,
glyphRect.origin.y,
advance,
glyphRect.size.height);
}
CFRelease(font);
return properGlyphRect;
}
- (void)enumerateTextIndexesAtPosition:(CGPoint)position usingBlock:(as_renderer_index_block_t)block
{
if (position.x > _constrainedSize.width
|| position.y > _constrainedSize.height
|| block == NULL) {
// Short circuit if the position is outside the size of this renderer, or
// if the block is null.
return;
}
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
// We break it up into a 44pt box for the touch, and find the closest link
// attribute-containing glyph to the center of the touch.
CGFloat squareSide = 44.f;
// Should be odd if you want to test the center of the touch.
NSInteger pointsOnASide = 3;
// The distance between any 2 of the adjacent points
CGFloat pointSeparation = squareSide / pointsOnASide;
// These are for tracking which point we're on. We start with -pointsOnASide/2
// and go to pointsOnASide/2. So if pointsOnASide=3, we go from -1 to 1.
NSInteger endIndex = pointsOnASide / 2;
NSInteger startIndex = -endIndex;
BOOL stop = NO;
for (NSInteger i = startIndex; i <= endIndex && !stop; i++) {
for (NSInteger j = startIndex; j <= endIndex && !stop; j++) {
CGPoint currentPoint = CGPointMake(position.x + i * pointSeparation,
position.y + j * pointSeparation);
// We ask the layout manager for the proper glyph at the touch point
NSUInteger glyphIndex = [_layoutManager glyphIndexForPoint:currentPoint
inTextContainer:_textContainer];
// If it's an invalid glyph, quit.
BOOL isValidGlyph = NO;
[_layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph];
if (!isValidGlyph) {
continue;
}
NSUInteger characterIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex];
CGRect glyphRect = [self _rectForGlyphAtIndex:glyphIndex
measureOption:ASTextNodeRendererMeasureOptionLineHeight];
// Sometimes TextKit plays jokes on us and returns glyphs that really
// aren't close to the point in question. Silly TextKit...
if (!CGRectContainsPoint(CGRectInset(glyphRect, -ASTextNodeRendererGlyphTouchHitSlop, -ASTextNodeRendererGlyphTouchHitSlop), currentPoint)) {
continue;
}
block(characterIndex, glyphRect, &stop);
}
}
}
#pragma mark - Truncation
/*
* Calculates the intersection of the truncation message within the end of the
* last line.
*
* This is accomplished by temporarily adding an exclusion rect for the size of
* the truncation string at the end of the last line of text, and forcing the
* layout manager to re-layout and clip the text such that we get a natural
* clipping based on the settings of the layout manager.
*/
- (NSUInteger)_calculateCharacterIndexBeforeTruncationMessage
{
ASDN::MutexLocker l(_textKitLock);
CGRect constrainedRect = (CGRect){.size = _calculatedSize};
NSRange visibleGlyphRange = [_layoutManager glyphRangeForBoundingRect:constrainedRect inTextContainer:_textContainer];
NSInteger lastVisibleGlyphIndex = (NSMaxRange(visibleGlyphRange) - 1);
CGRect lastLineRect = [_layoutManager lineFragmentRectForGlyphAtIndex:lastVisibleGlyphIndex effectiveRange:NULL];
// Calculate the bounding rectangle for the truncation message
ASTextKitComponents truncationComponents = ASTextKitComponentsCreate(_truncationString, constrainedRect.size);
// Size the truncation message
[truncationComponents.layoutManager ensureLayoutForTextContainer:truncationComponents.textContainer];
NSRange truncationGlyphRange = [truncationComponents.layoutManager glyphRangeForTextContainer:truncationComponents.textContainer];
CGRect truncationUsedRect = [truncationComponents.layoutManager boundingRectForGlyphRange:truncationGlyphRange inTextContainer:truncationComponents.textContainer];
CGRect translatedTruncationRect = CGRectMake(CGRectGetMaxX(constrainedRect) - truncationUsedRect.size.width,
CGRectGetMinY(lastLineRect),
truncationUsedRect.size.width,
truncationUsedRect.size.height);
// Determine which glyph is the first to be clipped / overlaps the truncation message.
CGPoint beginningOfTruncationMessage = CGPointMake(translatedTruncationRect.origin.x, CGRectGetMidY(translatedTruncationRect));
NSUInteger firstClippedGlyphIndex = [_layoutManager glyphIndexForPoint:beginningOfTruncationMessage inTextContainer:_textContainer fractionOfDistanceThroughGlyph:NULL];
NSUInteger firstCharacterIndexToReplace = [_layoutManager characterIndexForGlyphAtIndex:firstClippedGlyphIndex];
ASDisplayNodeAssert(firstCharacterIndexToReplace != NSNotFound, @"The beginning of the truncation message exclusion rect (%@) didn't intersect any glyphs", NSStringFromCGPoint(beginningOfTruncationMessage));
// Break on word boundaries
return [self _findTruncationInsertionPointAtOrBeforeCharacterIndex:firstCharacterIndexToReplace];
}
+ (NSCharacterSet *)_truncationCharacterSet
{
static NSCharacterSet *truncationCharacterSet;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableCharacterSet *mutableCharacterSet = [[NSMutableCharacterSet alloc] init];
[mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[mutableCharacterSet addCharactersInString:@".,!?:;"];
truncationCharacterSet = mutableCharacterSet;
});
return truncationCharacterSet;
}
/**
* @abstract Finds the first whitespace at or before the character index do we don't truncate in the middle of words
* @discussion If there are multiple whitespaces together (say a space and a newline), this will backtrack to the first one
*/
- (NSUInteger)_findTruncationInsertionPointAtOrBeforeCharacterIndex:(NSUInteger)firstCharacterIndexToReplace
{
ASDN::MutexLocker l(_textKitLock);
// Don't attempt to truncate beyond the beginning of the string
if (firstCharacterIndexToReplace >= _textStorage.length) {
return 0;
}
// Find the glyph range of the line fragment containing the first character to replace.
NSRange lineGlyphRange;
[_layoutManager lineFragmentRectForGlyphAtIndex:[_layoutManager glyphIndexForCharacterAtIndex:firstCharacterIndexToReplace]
effectiveRange:&lineGlyphRange];
// Look for the first whitespace from the end of the line, starting from the truncation point
NSUInteger startingSearchIndex = [_layoutManager characterIndexForGlyphAtIndex:lineGlyphRange.location];
NSUInteger endingSearchIndex = firstCharacterIndexToReplace;
NSRange rangeToSearch = NSMakeRange(startingSearchIndex, (endingSearchIndex - startingSearchIndex));
NSCharacterSet *truncationCharacterSet = [[self class] _truncationCharacterSet];
NSRange rangeOfLastVisibleWhitespace = [_textStorage.string rangeOfCharacterFromSet:truncationCharacterSet
options:NSBackwardsSearch
range:rangeToSearch];
// Couldn't find a good place to truncate. Might be because there is no whitespace in the text, or we're dealing
// with a foreign language encoding. Settle for truncating at the original place, which may be mid-word.
if (rangeOfLastVisibleWhitespace.location == NSNotFound) {
return firstCharacterIndexToReplace;
} else {
return rangeOfLastVisibleWhitespace.location;
}
}
#pragma mark - Drawing
- (void)drawInRect:(CGRect)bounds isRasterizing:(BOOL)isRasterizing
{
CGContextRef context = UIGraphicsGetCurrentContext();
ASDisplayNodeAssert(context, @"This is no good without a context.");
CGContextSaveGState(context);
[self _initializeTextKitComponentsIfNeeded];
NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
{
ASDN::MutexLocker l(_textKitLock);
[_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:bounds.origin];
[_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:bounds.origin];
}
CGContextRestoreGState(context);
}
#pragma mark - String Ranges
- (NSUInteger)lineCount
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
NSUInteger lineCount = 0;
for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [_layoutManager numberOfGlyphs]; lineCount++) {
[_layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange];
}
return lineCount;
}
- (NSRange)visibleRange
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
return _visibleRange;
}
- (NSRange)truncationStringCharacterRange
{
ASDN::MutexLocker l(_textKitLock);
[self _initializeTextKitComponentsIfNeeded];
return _truncationCharacterRange;
}
@end