301 lines
11 KiB
Objective-C
301 lines
11 KiB
Objective-C
/*
|
|
Copyright 2011 Twitter, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this work except in compliance with the License.
|
|
You may obtain a copy of the License in the LICENSE file, or at:
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
#import "CoreText+Additions.h"
|
|
|
|
CGSize AB_CTLineGetSize(CTLineRef line)
|
|
{
|
|
CGFloat ascent, descent, leading;
|
|
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
CGFloat height = ascent + descent + leading;
|
|
return CGSizeMake(ceil(width), ceil(height));
|
|
}
|
|
|
|
CGSize AB_CTFrameGetSize(CTFrameRef frame)
|
|
{
|
|
CGFloat h = 0.0;
|
|
CGFloat w = 0.0;
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
for(id line in lines) {
|
|
CGSize s = AB_CTLineGetSize((__bridge CTLineRef)line);
|
|
if(s.width > w)
|
|
w = s.width;
|
|
}
|
|
|
|
// Mostly based off http://lists.apple.com/archives/quartz-dev/2008/Mar/msg00079.html
|
|
CTLineRef lastLine = (__bridge CTLineRef)[lines lastObject];
|
|
if(lastLine != NULL) {
|
|
// Get the origin of the last line. We add the descent to this
|
|
// (below) to get the bottom edge of the last line of text.
|
|
CGPoint lastLineOrigin;
|
|
CTFrameGetLineOrigins(frame, CFRangeMake(lines.count - 1, 0), &lastLineOrigin);
|
|
|
|
CGPathRef framePath = CTFrameGetPath(frame);
|
|
CGRect frameRect = CGPathGetBoundingBox(framePath);
|
|
// The height needed to draw the text is from the bottom of the last line
|
|
// to the top of the frame.
|
|
CGFloat ascent, descent, leading;
|
|
CTLineGetTypographicBounds(lastLine, &ascent, &descent, &leading);
|
|
h = CGRectGetMaxY(frameRect) - lastLineOrigin.y + descent;
|
|
}
|
|
|
|
return CGSizeMake(ceil(w), ceil(h));
|
|
}
|
|
|
|
CGFloat AB_CTFrameGetHeight(CTFrameRef f)
|
|
{
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(f);
|
|
NSInteger n = (NSInteger)[lines count];
|
|
CGPoint *lineOrigins = (CGPoint *) malloc(sizeof(CGPoint) * n);
|
|
CTFrameGetLineOrigins(f, CFRangeMake(0, n), lineOrigins);
|
|
|
|
CGPoint first, last;
|
|
|
|
CGFloat h = 0.0;
|
|
for(int i = 0; i < n; ++i) {
|
|
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
|
|
CGFloat ascent, descent, leading;
|
|
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
if(i == 0) {
|
|
first = lineOrigins[i];
|
|
h += ascent;
|
|
h += descent;
|
|
}
|
|
if(i == n-1) {
|
|
last = lineOrigins[i];
|
|
h += first.y - last.y;
|
|
h += descent;
|
|
free(lineOrigins);
|
|
return ceil(h);
|
|
}
|
|
}
|
|
free(lineOrigins);
|
|
return 0.0;
|
|
}
|
|
|
|
CFIndex AB_CTFrameGetStringIndexForPosition(CTFrameRef frame, CGPoint p)
|
|
{
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
|
|
CFIndex linesCount = [lines count];
|
|
CGPoint *lineOrigins = (CGPoint *) malloc(sizeof(CGPoint) * linesCount);
|
|
CTFrameGetLineOrigins(frame, CFRangeMake(0, linesCount), lineOrigins);
|
|
|
|
CTLineRef line = NULL;
|
|
CGPoint lineOrigin = CGPointZero;
|
|
|
|
for(CFIndex i = 0; i < linesCount; ++i) {
|
|
line = (__bridge CTLineRef)[lines objectAtIndex:i];
|
|
lineOrigin = lineOrigins[i];
|
|
CGFloat descent, ascent;
|
|
CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
|
|
if(p.y > (floor(lineOrigin.y) - floor(descent))) { // above bottom of line
|
|
if(i == 0 && (p.y > (ceil(lineOrigin.y) + ceil(ascent)))) { // above top of first line
|
|
free(lineOrigins);
|
|
return 0;
|
|
} else {
|
|
goto found;
|
|
}
|
|
}
|
|
}
|
|
|
|
free(lineOrigins);
|
|
|
|
// didn't find a line, must be beneath the last line
|
|
return CTFrameGetStringRange(frame).length; // last character index
|
|
|
|
found:
|
|
|
|
p.x -= lineOrigin.x;
|
|
p.y -= lineOrigin.y;
|
|
|
|
if(line) {
|
|
CFIndex i = CTLineGetStringIndexForPosition(line, p);
|
|
free(lineOrigins);
|
|
return i;
|
|
}
|
|
|
|
free(lineOrigins);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline BOOL RangeContainsIndex(CFRange range, CFIndex index)
|
|
{
|
|
BOOL a = (index >= range.location);
|
|
BOOL b = (index <= (range.location + range.length));
|
|
return (a && b);
|
|
}
|
|
|
|
void AB_CTFrameGetIndexForPositionInLine(NSString *string, CTFrameRef frame, CFIndex lineIndex, float xPosition, CFIndex *index)
|
|
{
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
CFIndex linesCount = [lines count];
|
|
|
|
if(lineIndex < linesCount) {
|
|
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:lineIndex];
|
|
*index = CTLineGetStringIndexForPosition(line, CGPointMake(xPosition, 0));
|
|
} else {
|
|
*index = 0;
|
|
}
|
|
}
|
|
|
|
void AB_CTFrameGetLinePositionOfIndex(NSString *string, CTFrameRef frame, CFIndex index, CFIndex *lineIndex, float *xPosition)
|
|
{
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
CFIndex linesCount = [lines count];
|
|
CFIndex charCount = 0;
|
|
CFIndex count = 0;
|
|
|
|
for(CFIndex i = 0; i < linesCount; ++i) {
|
|
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
|
|
count = CTLineGetGlyphCount(line);
|
|
|
|
if((index >= charCount && index < charCount + count) || i == linesCount - 1) {
|
|
CGFloat offset = CTLineGetOffsetForStringIndex(line, index, NULL);
|
|
*lineIndex = i;
|
|
*xPosition = offset;
|
|
return;
|
|
}
|
|
|
|
charCount += count;
|
|
}
|
|
|
|
*lineIndex = -1;
|
|
*xPosition = 0;
|
|
}
|
|
|
|
void AB_CTFrameGetRectsForRange(CTFrameRef frame, CFRange range, CGRect rects[], CFIndex *rectCount)
|
|
{
|
|
AB_CTFrameGetRectsForRangeWithAggregationType(frame, range, AB_CTLineRectAggregationTypeInline, rects, rectCount);
|
|
}
|
|
|
|
void AB_CTFrameGetRectsForRangeWithAggregationType(CTFrameRef frame, CFRange range, AB_CTLineRectAggregationType aggregationType, CGRect rects[], CFIndex *rectCount)
|
|
{
|
|
CGRect bounds;
|
|
CGPathIsRect(CTFrameGetPath(frame), &bounds);
|
|
|
|
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
|
|
CFIndex linesCount = [lines count];
|
|
CGPoint *lineOrigins = (CGPoint *) malloc(sizeof(CGPoint) * linesCount);
|
|
CTFrameGetLineOrigins(frame, CFRangeMake(0, linesCount), lineOrigins);
|
|
|
|
AB_CTLinesGetRectsForRangeWithAggregationType(lines, lineOrigins, bounds, range, aggregationType, rects, rectCount);
|
|
free(lineOrigins);
|
|
}
|
|
|
|
void AB_CTLinesGetRectsForRangeWithAggregationType(NSArray *lines, CGPoint *lineOrigins, CGRect bounds, CFRange range, AB_CTLineRectAggregationType aggregationType, CGRect rects[], CFIndex *rectCount)
|
|
{
|
|
CFIndex maxRects = *rectCount;
|
|
CFIndex rectIndex = 0;
|
|
|
|
CFIndex startIndex = range.location;
|
|
CFIndex endIndex = startIndex + range.length;
|
|
|
|
CFIndex linesCount = [lines count];
|
|
|
|
for(CFIndex i = 0; i < linesCount; ++i) {
|
|
CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
|
|
|
|
CFRange lineRange = CTLineGetStringRange(line);
|
|
CFIndex lineStartIndex = lineRange.location;
|
|
CFIndex lineEndIndex = lineStartIndex + lineRange.length;
|
|
BOOL containsStartIndex = RangeContainsIndex(lineRange, startIndex);
|
|
BOOL containsEndIndex = RangeContainsIndex(lineRange, endIndex);
|
|
|
|
if(containsStartIndex && containsEndIndex) {
|
|
CGPoint lineOrigin = lineOrigins[i];
|
|
CGFloat ascent, descent, leading;
|
|
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
lineWidth = lineWidth;
|
|
|
|
// If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it's only 1 line, then we'll guess the line's height.
|
|
BOOL useRealHeight = i < linesCount - 1;
|
|
CGFloat neighborLineY = i > 0 ? lineOrigins[i - 1].y : (linesCount - 1 > i ? lineOrigins[i + 1].y : 0.0f);
|
|
CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading);
|
|
CGFloat line_y = round(useRealHeight ? lineOrigin.y + bounds.origin.y - lineHeight/2 + descent : lineOrigin.y - descent + bounds.origin.y);
|
|
|
|
CGFloat startOffset = CTLineGetOffsetForStringIndex(line, startIndex, NULL);
|
|
CGFloat endOffset = CTLineGetOffsetForStringIndex(line, endIndex, NULL);
|
|
CGRect r = CGRectMake(bounds.origin.x + lineOrigin.x + startOffset, line_y, endOffset - startOffset, lineHeight);
|
|
if(aggregationType == AB_CTLineRectAggregationTypeBlock) {
|
|
r.size.width = bounds.size.width - startOffset;
|
|
}
|
|
|
|
if(rectIndex < maxRects)
|
|
rects[rectIndex++] = r;
|
|
goto end;
|
|
} else if(containsStartIndex) {
|
|
if(startIndex == lineEndIndex)
|
|
continue;
|
|
|
|
CGPoint lineOrigin = lineOrigins[i];
|
|
CGFloat ascent, descent, leading;
|
|
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
lineWidth = lineWidth;
|
|
|
|
// If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it's only 1 line, then we'll guess the line's height.
|
|
BOOL useRealHeight = i < linesCount - 1;
|
|
CGFloat neighborLineY = i > 0 ? lineOrigins[i - 1].y : (linesCount > i ? lineOrigins[i + 1].y : 0.0f);
|
|
CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading);
|
|
CGFloat line_y = round(useRealHeight ? lineOrigin.y + bounds.origin.y - lineHeight/2 + descent : lineOrigin.y - descent + bounds.origin.y);
|
|
|
|
CGFloat startOffset = CTLineGetOffsetForStringIndex(line, startIndex, NULL);
|
|
CGRect r = CGRectMake(bounds.origin.x + lineOrigin.x + startOffset, line_y, bounds.size.width - startOffset, lineHeight);
|
|
if(rectIndex < maxRects)
|
|
rects[rectIndex++] = r;
|
|
} else if(containsEndIndex) {
|
|
CGPoint lineOrigin = lineOrigins[i];
|
|
CGFloat ascent, descent, leading;
|
|
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
lineWidth = lineWidth;
|
|
|
|
// If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it's only 1 line, then we'll guess the line's height.
|
|
BOOL useRealHeight = i < linesCount - 1;
|
|
CGFloat neighborLineY = i > 0 ? lineOrigins[i - 1].y : (linesCount > i ? lineOrigins[i + 1].y : 0.0f);
|
|
CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading);
|
|
CGFloat line_y = round(useRealHeight ? lineOrigin.y + bounds.origin.y - lineHeight/2 + descent : lineOrigin.y - descent + bounds.origin.y);
|
|
|
|
CGFloat endOffset = CTLineGetOffsetForStringIndex(line, endIndex, NULL);
|
|
CGRect r = CGRectMake(bounds.origin.x + lineOrigin.x, line_y, endOffset, lineHeight);
|
|
if(aggregationType == AB_CTLineRectAggregationTypeBlock) {
|
|
r.size.width = bounds.size.width;
|
|
}
|
|
|
|
if(rectIndex < maxRects)
|
|
rects[rectIndex++] = r;
|
|
} else if(RangeContainsIndex(range, lineRange.location)) {
|
|
CGPoint lineOrigin = lineOrigins[i];
|
|
CGFloat ascent, descent, leading;
|
|
CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
lineWidth = lineWidth;
|
|
|
|
// If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it's only 1 line, then we'll guess the line's height.
|
|
BOOL useRealHeight = i < linesCount - 1;
|
|
CGFloat neighborLineY = i > 0 ? lineOrigins[i - 1].y : (linesCount > i ? lineOrigins[i + 1].y : 0.0f);
|
|
CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading);
|
|
CGFloat line_y = round(useRealHeight ? lineOrigin.y + bounds.origin.y - lineHeight/2 + descent : lineOrigin.y - descent + bounds.origin.y);
|
|
|
|
CGRect r = CGRectMake(bounds.origin.x + lineOrigin.x, line_y, bounds.size.width, lineHeight);
|
|
if(rectIndex < maxRects)
|
|
rects[rectIndex++] = r;
|
|
}
|
|
}
|
|
|
|
end:
|
|
*rectCount = rectIndex;
|
|
}
|