mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Rename CK classes to AS classes
This commit is contained in:
committed by
Levi McCallum
parent
de66819286
commit
a0c05ebffc
374
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.mm
Executable file
374
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.mm
Executable file
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* 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 "ASTextKitRenderer+Positioning.h"
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#import "ASAssert.h"
|
||||
|
||||
#import "ASTextKitContext.h"
|
||||
#import "ASTextKitShadower.h"
|
||||
|
||||
static const CGFloat ASTextKitRendererGlyphTouchHitSlop = 5.0;
|
||||
static const CGFloat ASTextKitRendererTextCapHeightPadding = 1.3;
|
||||
|
||||
@implementation ASTextKitRenderer (Tracking)
|
||||
|
||||
- (NSArray *)rectsForTextRange:(NSRange)textRange
|
||||
measureOption:(ASTextKitRendererMeasureOption)measureOption
|
||||
{
|
||||
__block NSArray *textRects = @[];
|
||||
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
|
||||
BOOL textRangeIsValid = (NSMaxRange(textRange) <= [textStorage length]);
|
||||
ASDisplayNodeCAssertTrue(textRangeIsValid);
|
||||
if (!textRangeIsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Used for block measure option
|
||||
__block CGRect firstRect = CGRectNull;
|
||||
__block CGRect lastRect = CGRectNull;
|
||||
__block CGRect blockRect = CGRectNull;
|
||||
NSMutableArray *mutableTextRects = [NSMutableArray array];
|
||||
|
||||
NSString *string = textStorage.string;
|
||||
|
||||
NSRange totalGlyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
|
||||
|
||||
[layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect,
|
||||
CGRect usedRect,
|
||||
NSTextContainer *innerTextContainer,
|
||||
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 _internalRectForGlyphAtIndex:i
|
||||
measureOption:measureOption
|
||||
layoutManager:layoutManager
|
||||
textContainer:textContainer
|
||||
textStorage:textStorage];
|
||||
|
||||
// Don't count empty glyphs towards our line rect.
|
||||
if (!CGRectIsEmpty(properGlyphRect)) {
|
||||
lineRect = CGRectIsNull(lineRect) ? properGlyphRect
|
||||
: CGRectUnion(lineRect, properGlyphRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!CGRectIsNull(lineRect)) {
|
||||
if (measureOption == ASTextKitRendererMeasureOptionBlock) {
|
||||
// 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.
|
||||
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lineRect]]];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (measureOption == ASTextKitRendererMeasureOptionBlock) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:firstRect]]];
|
||||
}
|
||||
if (!CGRectIsNull(blockRect)) {
|
||||
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:blockRect]]];
|
||||
}
|
||||
if (!CGRectIsNull(lastRect)) {
|
||||
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lastRect]]];
|
||||
}
|
||||
}
|
||||
textRects = mutableTextRects;
|
||||
}];
|
||||
|
||||
return textRects;
|
||||
}
|
||||
|
||||
- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position
|
||||
{
|
||||
// Check in a 9-point region around the actual touch point so we make sure
|
||||
// we get the best attribute for the touch.
|
||||
__block CGFloat minimumGlyphDistance = CGFLOAT_MAX;
|
||||
__block NSUInteger minimumGlyphCharacterIndex = NSNotFound;
|
||||
|
||||
[self enumerateTextIndexesAtPosition:position usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) {
|
||||
CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect));
|
||||
CGFloat currentDistance = sqrtf(powf(position.x - glyphLocation.x, 2.f) + powf(position.y - glyphLocation.y, 2.f));
|
||||
if (currentDistance < minimumGlyphDistance) {
|
||||
minimumGlyphDistance = currentDistance;
|
||||
minimumGlyphCharacterIndex = characterIndex;
|
||||
}
|
||||
}];
|
||||
return minimumGlyphCharacterIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
Measured from the internal coordinate space of the context, not accounting for shadow offsets. Actually uses CoreText
|
||||
as an approximation to work around problems in TextKit's glyph sizing.
|
||||
*/
|
||||
- (CGRect)_internalRectForGlyphAtIndex:(NSUInteger)glyphIndex
|
||||
measureOption:(ASTextKitRendererMeasureOption)measureOption
|
||||
layoutManager:(NSLayoutManager *)layoutManager
|
||||
textContainer:(NSTextContainer *)textContainer
|
||||
textStorage:(NSTextStorage *)textStorage
|
||||
{
|
||||
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 == ASTextKitRendererMeasureOptionCapHeight
|
||||
|| measureOption == ASTextKitRendererMeasureOptionBlock) {
|
||||
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 = ASTextKitRendererTextCapHeightPadding * 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)externalPosition usingBlock:(ck_text_component_index_block_t)block
|
||||
{
|
||||
// This method is a little complex because it has to call out to client code from inside an enumeration that needs
|
||||
// to achieve a lock on the textkit components. It cannot call out to client code from within that lock so we just
|
||||
// perform the textkit-locked ops inside the locked context.
|
||||
ASTextKitContext *lockingContext = self.context;
|
||||
CGPoint internalPosition = [self.shadower offsetPointWithExternalPoint:externalPosition];
|
||||
__block BOOL invalidPosition = NO;
|
||||
[lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
|
||||
invalidPosition = internalPosition.x > textContainer.size.width
|
||||
|| internalPosition.y > textContainer.size.height
|
||||
|| block == NULL;
|
||||
}];
|
||||
if (invalidPosition) {
|
||||
// Short circuit if the position is outside the size of this renderer, or if the block is null.
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(internalPosition.x + i * pointSeparation,
|
||||
internalPosition.y + j * pointSeparation);
|
||||
|
||||
__block NSUInteger characterIndex = NSNotFound;
|
||||
__block BOOL isValidGlyph = NO;
|
||||
__block CGRect glyphRect = CGRectNull;
|
||||
|
||||
[lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
|
||||
// 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.
|
||||
|
||||
[layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph];
|
||||
if (!isValidGlyph) {
|
||||
return;
|
||||
}
|
||||
|
||||
characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
|
||||
|
||||
glyphRect = [self _internalRectForGlyphAtIndex:glyphIndex
|
||||
measureOption:ASTextKitRendererMeasureOptionLineHeight
|
||||
layoutManager:layoutManager
|
||||
textContainer:textContainer
|
||||
textStorage:textStorage];
|
||||
}];
|
||||
|
||||
// Sometimes TextKit plays jokes on us and returns glyphs that really aren't close to the point in question.
|
||||
// Silly TextKit...
|
||||
if (!isValidGlyph || !CGRectContainsPoint(CGRectInset(glyphRect, -ASTextKitRendererGlyphTouchHitSlop, -ASTextKitRendererGlyphTouchHitSlop), currentPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
block(characterIndex, [self.shadower offsetRectWithInternalRect:glyphRect], &stop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)trailingRect
|
||||
{
|
||||
__block CGRect trailingRect = CGRectNull;
|
||||
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
|
||||
CGSize calculatedSize = textContainer.size;
|
||||
// If have an empty string, then our whole bounds constitute trailing space.
|
||||
if ([textStorage length] == 0) {
|
||||
trailingRect = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Take everything after our final character as trailing space.
|
||||
NSArray *finalRects = [self rectsForTextRange:NSMakeRange([textStorage length] - 1, 1) measureOption:ASTextKitRendererMeasureOptionLineHeight];
|
||||
CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue];
|
||||
CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect));
|
||||
CGSize size = CGSizeMake(calculatedSize.width - origin.x, calculatedSize.height - origin.y);
|
||||
trailingRect = (CGRect){origin, size};
|
||||
}];
|
||||
return trailingRect;
|
||||
}
|
||||
|
||||
- (CGRect)frameForTextRange:(NSRange)textRange
|
||||
{
|
||||
__block CGRect textRect = CGRectNull;
|
||||
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
|
||||
// Bail on invalid range.
|
||||
if (NSMaxRange(textRange) > [textStorage length]) {
|
||||
ASDisplayNodeCFailAssert(@"Invalid range");
|
||||
return;
|
||||
}
|
||||
|
||||
// Force glyph generation and layout.
|
||||
[layoutManager ensureLayoutForTextContainer:textContainer];
|
||||
|
||||
NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
|
||||
textRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
|
||||
}];
|
||||
return textRect;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user