mirror of https://github.com/axmolengine/axmol.git
915 lines
42 KiB
Objective-C
915 lines
42 KiB
Objective-C
//
|
|
// FontLabelStringDrawing.m
|
|
// FontLabel
|
|
//
|
|
// Created by Kevin Ballard on 5/5/09.
|
|
// Copyright © 2009 Zynga Game Networks
|
|
// Copyright (c) 2011 cocos2d-x.org
|
|
//
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License 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 "FontLabelStringDrawing.h"
|
|
#import "ZFont.h"
|
|
#import "ZAttributedStringPrivate.h"
|
|
|
|
@interface ZFont (ZFontPrivate)
|
|
@property (nonatomic, readonly) CGFloat ratio;
|
|
@end
|
|
|
|
#define kUnicodeHighSurrogateStart 0xD800
|
|
#define kUnicodeHighSurrogateEnd 0xDBFF
|
|
#define kUnicodeHighSurrogateMask kUnicodeHighSurrogateStart
|
|
#define kUnicodeLowSurrogateStart 0xDC00
|
|
#define kUnicodeLowSurrogateEnd 0xDFFF
|
|
#define kUnicodeLowSurrogateMask kUnicodeLowSurrogateStart
|
|
#define kUnicodeSurrogateTypeMask 0xFC00
|
|
#define UnicharIsHighSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeHighSurrogateMask)
|
|
#define UnicharIsLowSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeLowSurrogateMask)
|
|
#define ConvertSurrogatePairToUTF32(high, low) ((UInt32)((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000))
|
|
|
|
typedef enum {
|
|
kFontTableFormat4 = 4,
|
|
kFontTableFormat12 = 12,
|
|
} FontTableFormat;
|
|
|
|
typedef struct fontTable {
|
|
NSUInteger retainCount;
|
|
CFDataRef cmapTable;
|
|
FontTableFormat format;
|
|
union {
|
|
struct {
|
|
UInt16 segCountX2;
|
|
UInt16 *endCodes;
|
|
UInt16 *startCodes;
|
|
UInt16 *idDeltas;
|
|
UInt16 *idRangeOffsets;
|
|
} format4;
|
|
struct {
|
|
UInt32 nGroups;
|
|
struct {
|
|
UInt32 startCharCode;
|
|
UInt32 endCharCode;
|
|
UInt32 startGlyphCode;
|
|
} *groups;
|
|
} format12;
|
|
} cmap;
|
|
} fontTable;
|
|
|
|
static FontTableFormat supportedFormats[] = { kFontTableFormat4, kFontTableFormat12 };
|
|
static size_t supportedFormatsCount = sizeof(supportedFormats) / sizeof(FontTableFormat);
|
|
|
|
static fontTable *newFontTable(CFDataRef cmapTable, FontTableFormat format) {
|
|
fontTable *table = (struct fontTable *)malloc(sizeof(struct fontTable));
|
|
table->retainCount = 1;
|
|
table->cmapTable = CFRetain(cmapTable);
|
|
table->format = format;
|
|
return table;
|
|
}
|
|
|
|
static fontTable *retainFontTable(fontTable *table) {
|
|
if (table != NULL) {
|
|
table->retainCount++;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
static void releaseFontTable(fontTable *table) {
|
|
if (table != NULL) {
|
|
if (table->retainCount <= 1) {
|
|
CFRelease(table->cmapTable);
|
|
free(table);
|
|
} else {
|
|
table->retainCount--;
|
|
}
|
|
}
|
|
}
|
|
|
|
static const void *fontTableRetainCallback(CFAllocatorRef allocator, const void *value) {
|
|
return retainFontTable((fontTable *)value);
|
|
}
|
|
|
|
static void fontTableReleaseCallback(CFAllocatorRef allocator, const void *value) {
|
|
releaseFontTable((fontTable *)value);
|
|
}
|
|
|
|
static const CFDictionaryValueCallBacks kFontTableDictionaryValueCallBacks = {
|
|
.version = 0,
|
|
.retain = &fontTableRetainCallback,
|
|
.release = &fontTableReleaseCallback,
|
|
.copyDescription = NULL,
|
|
.equal = NULL
|
|
};
|
|
|
|
// read the cmap table from the font
|
|
// we only know how to understand some of the table formats at the moment
|
|
static fontTable *readFontTableFromCGFont(CGFontRef font) {
|
|
CFDataRef cmapTable = CGFontCopyTableForTag(font, 'cmap');
|
|
NSCAssert1(cmapTable != NULL, @"CGFontCopyTableForTag returned NULL for 'cmap' tag in font %@",
|
|
(font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
|
|
const UInt8 * const bytes = CFDataGetBytePtr(cmapTable);
|
|
NSCAssert1(OSReadBigInt16(bytes, 0) == 0, @"cmap table for font %@ has bad version number",
|
|
(font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
|
|
UInt16 numberOfSubtables = OSReadBigInt16(bytes, 2);
|
|
const UInt8 *unicodeSubtable = NULL;
|
|
//UInt16 unicodeSubtablePlatformID;
|
|
UInt16 unicodeSubtablePlatformSpecificID;
|
|
FontTableFormat unicodeSubtableFormat;
|
|
const UInt8 * const encodingSubtables = &bytes[4];
|
|
for (UInt16 i = 0; i < numberOfSubtables; i++) {
|
|
const UInt8 * const encodingSubtable = &encodingSubtables[8 * i];
|
|
UInt16 platformID = OSReadBigInt16(encodingSubtable, 0);
|
|
UInt16 platformSpecificID = OSReadBigInt16(encodingSubtable, 2);
|
|
// find the best subtable
|
|
// best is defined by a combination of encoding and format
|
|
// At the moment we only support format 4, so ignore all other format tables
|
|
// We prefer platformID == 0, but we will also accept Microsoft's unicode format
|
|
if (platformID == 0 || (platformID == 3 && platformSpecificID == 1)) {
|
|
BOOL preferred = NO;
|
|
if (unicodeSubtable == NULL) {
|
|
preferred = YES;
|
|
} else if (platformID == 0 && platformSpecificID > unicodeSubtablePlatformSpecificID) {
|
|
preferred = YES;
|
|
}
|
|
if (preferred) {
|
|
UInt32 offset = OSReadBigInt32(encodingSubtable, 4);
|
|
const UInt8 *subtable = &bytes[offset];
|
|
UInt16 format = OSReadBigInt16(subtable, 0);
|
|
for (size_t i = 0; i < supportedFormatsCount; i++) {
|
|
if (format == supportedFormats[i]) {
|
|
if (format >= 8) {
|
|
// the version is a fixed-point
|
|
UInt16 formatFrac = OSReadBigInt16(subtable, 2);
|
|
if (formatFrac != 0) {
|
|
// all the current formats with a Fixed version are always *.0
|
|
continue;
|
|
}
|
|
}
|
|
unicodeSubtable = subtable;
|
|
//unicodeSubtablePlatformID = platformID;
|
|
unicodeSubtablePlatformSpecificID = platformSpecificID;
|
|
unicodeSubtableFormat = format;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fontTable *table = NULL;
|
|
if (unicodeSubtable != NULL) {
|
|
table = newFontTable(cmapTable, unicodeSubtableFormat);
|
|
switch (unicodeSubtableFormat) {
|
|
case kFontTableFormat4:
|
|
// subtable format 4
|
|
//UInt16 length = OSReadBigInt16(unicodeSubtable, 2);
|
|
//UInt16 language = OSReadBigInt16(unicodeSubtable, 4);
|
|
table->cmap.format4.segCountX2 = OSReadBigInt16(unicodeSubtable, 6);
|
|
//UInt16 searchRange = OSReadBigInt16(unicodeSubtable, 8);
|
|
//UInt16 entrySelector = OSReadBigInt16(unicodeSubtable, 10);
|
|
//UInt16 rangeShift = OSReadBigInt16(unicodeSubtable, 12);
|
|
table->cmap.format4.endCodes = (UInt16*)&unicodeSubtable[14];
|
|
table->cmap.format4.startCodes = (UInt16*)&((UInt8*)table->cmap.format4.endCodes)[table->cmap.format4.segCountX2+2];
|
|
table->cmap.format4.idDeltas = (UInt16*)&((UInt8*)table->cmap.format4.startCodes)[table->cmap.format4.segCountX2];
|
|
table->cmap.format4.idRangeOffsets = (UInt16*)&((UInt8*)table->cmap.format4.idDeltas)[table->cmap.format4.segCountX2];
|
|
//UInt16 *glyphIndexArray = &idRangeOffsets[segCountX2];
|
|
break;
|
|
case kFontTableFormat12:
|
|
table->cmap.format12.nGroups = OSReadBigInt32(unicodeSubtable, 12);
|
|
table->cmap.format12.groups = (void *)&unicodeSubtable[16];
|
|
break;
|
|
default:
|
|
releaseFontTable(table);
|
|
table = NULL;
|
|
}
|
|
}
|
|
CFRelease(cmapTable);
|
|
return table;
|
|
}
|
|
|
|
// outGlyphs must be at least size n
|
|
static void mapCharactersToGlyphsInFont(const fontTable *table, unichar characters[], size_t charLen, CGGlyph outGlyphs[], size_t *outGlyphLen) {
|
|
if (table != NULL) {
|
|
NSUInteger j = 0;
|
|
switch (table->format) {
|
|
case kFontTableFormat4: {
|
|
for (NSUInteger i = 0; i < charLen; i++, j++) {
|
|
unichar c = characters[i];
|
|
UInt16 segOffset;
|
|
BOOL foundSegment = NO;
|
|
for (segOffset = 0; segOffset < table->cmap.format4.segCountX2; segOffset += 2) {
|
|
UInt16 endCode = OSReadBigInt16(table->cmap.format4.endCodes, segOffset);
|
|
if (endCode >= c) {
|
|
foundSegment = YES;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundSegment) {
|
|
// no segment
|
|
// this is an invalid font
|
|
outGlyphs[j] = 0;
|
|
} else {
|
|
UInt16 startCode = OSReadBigInt16(table->cmap.format4.startCodes, segOffset);
|
|
if (!(startCode <= c)) {
|
|
// the code falls in a hole between segments
|
|
outGlyphs[j] = 0;
|
|
} else {
|
|
UInt16 idRangeOffset = OSReadBigInt16(table->cmap.format4.idRangeOffsets, segOffset);
|
|
if (idRangeOffset == 0) {
|
|
UInt16 idDelta = OSReadBigInt16(table->cmap.format4.idDeltas, segOffset);
|
|
outGlyphs[j] = (c + idDelta) % 65536;
|
|
} else {
|
|
// use the glyphIndexArray
|
|
UInt16 glyphOffset = idRangeOffset + 2 * (c - startCode);
|
|
outGlyphs[j] = OSReadBigInt16(&((UInt8*)table->cmap.format4.idRangeOffsets)[segOffset], glyphOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case kFontTableFormat12: {
|
|
UInt32 lastSegment = UINT32_MAX;
|
|
for (NSUInteger i = 0; i < charLen; i++, j++) {
|
|
unichar c = characters[i];
|
|
UInt32 c32 = c;
|
|
if (UnicharIsHighSurrogate(c)) {
|
|
if (i+1 < charLen) { // do we have another character after this one?
|
|
unichar cc = characters[i+1];
|
|
if (UnicharIsLowSurrogate(cc)) {
|
|
c32 = ConvertSurrogatePairToUTF32(c, cc);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
// Start the heuristic search
|
|
// If this is an ASCII char, just do a linear search
|
|
// Otherwise do a hinted, modified binary search
|
|
// Start the first pivot at the last range found
|
|
// And when moving the pivot, limit the movement by increasing
|
|
// powers of two. This should help with locality
|
|
__typeof__(table->cmap.format12.groups[0]) *foundGroup = NULL;
|
|
if (c32 <= 0x7F) {
|
|
// ASCII
|
|
for (UInt32 idx = 0; idx < table->cmap.format12.nGroups; idx++) {
|
|
__typeof__(table->cmap.format12.groups[idx]) *group = &table->cmap.format12.groups[idx];
|
|
if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
|
|
// we've fallen into a hole
|
|
break;
|
|
} else if (c32 <= OSSwapBigToHostInt32(group->endCharCode)) {
|
|
// this is the range
|
|
foundGroup = group;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// heuristic search
|
|
UInt32 maxJump = (lastSegment == UINT32_MAX ? UINT32_MAX / 2 : 8);
|
|
UInt32 lowIdx = 0, highIdx = table->cmap.format12.nGroups; // highIdx is the first invalid idx
|
|
UInt32 pivot = (lastSegment == UINT32_MAX ? lowIdx + (highIdx - lowIdx) / 2 : lastSegment);
|
|
while (highIdx > lowIdx) {
|
|
__typeof__(table->cmap.format12.groups[pivot]) *group = &table->cmap.format12.groups[pivot];
|
|
if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
|
|
highIdx = pivot;
|
|
} else if (c32 > OSSwapBigToHostInt32(group->endCharCode)) {
|
|
lowIdx = pivot + 1;
|
|
} else {
|
|
// we've hit the range
|
|
foundGroup = group;
|
|
break;
|
|
}
|
|
if (highIdx - lowIdx > maxJump * 2) {
|
|
if (highIdx == pivot) {
|
|
pivot -= maxJump;
|
|
} else {
|
|
pivot += maxJump;
|
|
}
|
|
maxJump *= 2;
|
|
} else {
|
|
pivot = lowIdx + (highIdx - lowIdx) / 2;
|
|
}
|
|
}
|
|
if (foundGroup != NULL) lastSegment = pivot;
|
|
}
|
|
if (foundGroup == NULL) {
|
|
outGlyphs[j] = 0;
|
|
} else {
|
|
outGlyphs[j] = (CGGlyph)(OSSwapBigToHostInt32(foundGroup->startGlyphCode) +
|
|
(c32 - OSSwapBigToHostInt32(foundGroup->startCharCode)));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (outGlyphLen != NULL) *outGlyphLen = j;
|
|
} else {
|
|
// we have no table, so just null out the glyphs
|
|
bzero(outGlyphs, charLen*sizeof(CGGlyph));
|
|
if (outGlyphLen != NULL) *outGlyphLen = 0;
|
|
}
|
|
}
|
|
|
|
static BOOL mapGlyphsToAdvancesInFont(ZFont *font, size_t n, CGGlyph glyphs[], CGFloat outAdvances[]) {
|
|
int advances[n];
|
|
if (CGFontGetGlyphAdvances(font.cgFont, glyphs, n, advances)) {
|
|
CGFloat ratio = font.ratio;
|
|
|
|
for (size_t i = 0; i < n; i++) {
|
|
outAdvances[i] = advances[i]*ratio;
|
|
}
|
|
return YES;
|
|
} else {
|
|
bzero(outAdvances, n*sizeof(CGFloat));
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
static id getValueOrDefaultForRun(ZAttributeRun *run, NSString *key) {
|
|
id value = [run.attributes objectForKey:key];
|
|
if (value == nil) {
|
|
static NSDictionary *defaultValues = nil;
|
|
if (defaultValues == nil) {
|
|
defaultValues = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
[ZFont fontWithUIFont:[UIFont systemFontOfSize:12]], ZFontAttributeName,
|
|
[UIColor blackColor], ZForegroundColorAttributeName,
|
|
[UIColor clearColor], ZBackgroundColorAttributeName,
|
|
[NSNumber numberWithInt:ZUnderlineStyleNone], ZUnderlineStyleAttributeName,
|
|
nil];
|
|
}
|
|
value = [defaultValues objectForKey:key];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
static void readRunInformation(NSArray *attributes, NSUInteger len, CFMutableDictionaryRef fontTableMap,
|
|
NSUInteger index, ZAttributeRun **currentRun, NSUInteger *nextRunStart,
|
|
ZFont **currentFont, fontTable **currentTable) {
|
|
*currentRun = [attributes objectAtIndex:index];
|
|
*nextRunStart = ([attributes count] > index+1 ? [[attributes objectAtIndex:index+1] index] : len);
|
|
*currentFont = getValueOrDefaultForRun(*currentRun, ZFontAttributeName);
|
|
if (!CFDictionaryGetValueIfPresent(fontTableMap, (*currentFont).cgFont, (const void **)currentTable)) {
|
|
*currentTable = readFontTableFromCGFont((*currentFont).cgFont);
|
|
CFDictionarySetValue(fontTableMap, (*currentFont).cgFont, *currentTable);
|
|
releaseFontTable(*currentTable);
|
|
}
|
|
}
|
|
|
|
static CGSize drawOrSizeTextConstrainedToSize(BOOL performDraw, NSString *string, NSArray *attributes, CGSize constrainedSize, NSUInteger maxLines,
|
|
UILineBreakMode lineBreakMode, UITextAlignment alignment, BOOL ignoreColor) {
|
|
NSUInteger len = [string length];
|
|
NSUInteger idx = 0;
|
|
CGPoint drawPoint = CGPointZero;
|
|
CGSize retValue = CGSizeZero;
|
|
CGContextRef ctx = (performDraw ? UIGraphicsGetCurrentContext() : NULL);
|
|
|
|
BOOL convertNewlines = (maxLines == 1);
|
|
|
|
// Extract the characters from the string
|
|
// Convert newlines to spaces if necessary
|
|
unichar *characters = (unichar *)malloc(sizeof(unichar) * len);
|
|
if (convertNewlines) {
|
|
NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
|
|
NSRange range = NSMakeRange(0, len);
|
|
size_t cIdx = 0;
|
|
while (range.length > 0) {
|
|
NSRange newlineRange = [string rangeOfCharacterFromSet:charset options:0 range:range];
|
|
if (newlineRange.location == NSNotFound) {
|
|
[string getCharacters:&characters[cIdx] range:range];
|
|
cIdx += range.length;
|
|
break;
|
|
} else {
|
|
NSUInteger delta = newlineRange.location - range.location;
|
|
if (newlineRange.location > range.location) {
|
|
[string getCharacters:&characters[cIdx] range:NSMakeRange(range.location, delta)];
|
|
}
|
|
cIdx += delta;
|
|
characters[cIdx] = (unichar)' ';
|
|
cIdx++;
|
|
delta += newlineRange.length;
|
|
range.location += delta, range.length -= delta;
|
|
if (newlineRange.length == 1 && range.length >= 1 &&
|
|
[string characterAtIndex:newlineRange.location] == (unichar)'\r' &&
|
|
[string characterAtIndex:range.location] == (unichar)'\n') {
|
|
// CRLF sequence, skip the LF
|
|
range.location += 1, range.length -= 1;
|
|
}
|
|
}
|
|
}
|
|
len = cIdx;
|
|
} else {
|
|
[string getCharacters:characters range:NSMakeRange(0, len)];
|
|
}
|
|
|
|
// Create storage for glyphs and advances
|
|
CGGlyph *glyphs;
|
|
CGFloat *advances;
|
|
{
|
|
NSUInteger maxRunLength = 0;
|
|
ZAttributeRun *a = [attributes objectAtIndex:0];
|
|
for (NSUInteger i = 1; i < [attributes count]; i++) {
|
|
ZAttributeRun *b = [attributes objectAtIndex:i];
|
|
maxRunLength = MAX(maxRunLength, b.index - a.index);
|
|
a = b;
|
|
}
|
|
maxRunLength = MAX(maxRunLength, len - a.index);
|
|
maxRunLength++; // for a potential ellipsis
|
|
glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * maxRunLength);
|
|
advances = (CGFloat *)malloc(sizeof(CGFloat) * maxRunLength);
|
|
}
|
|
|
|
// Use this table to cache all fontTable objects
|
|
CFMutableDictionaryRef fontTableMap = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks,
|
|
&kFontTableDictionaryValueCallBacks);
|
|
|
|
// Fetch initial style values
|
|
NSUInteger currentRunIdx = 0;
|
|
ZAttributeRun *currentRun;
|
|
NSUInteger nextRunStart;
|
|
ZFont *currentFont;
|
|
fontTable *currentTable;
|
|
|
|
#define READ_RUN() readRunInformation(attributes, len, fontTableMap, \
|
|
currentRunIdx, ¤tRun, &nextRunStart, \
|
|
¤tFont, ¤tTable)
|
|
|
|
READ_RUN();
|
|
|
|
// fetch the glyphs for the first run
|
|
size_t glyphCount;
|
|
NSUInteger glyphIdx;
|
|
|
|
#define READ_GLYPHS() do { \
|
|
mapCharactersToGlyphsInFont(currentTable, &characters[currentRun.index], (nextRunStart - currentRun.index), glyphs, &glyphCount); \
|
|
mapGlyphsToAdvancesInFont(currentFont, (nextRunStart - currentRun.index), glyphs, advances); \
|
|
glyphIdx = 0; \
|
|
} while (0)
|
|
|
|
READ_GLYPHS();
|
|
|
|
NSMutableCharacterSet *alphaCharset = [NSMutableCharacterSet alphanumericCharacterSet];
|
|
[alphaCharset addCharactersInString:@"([{'\"\u2019\u02BC"];
|
|
|
|
// scan left-to-right looking for newlines or until we hit the width constraint
|
|
// When we hit a wrapping point, calculate truncation as follows:
|
|
// If we have room to draw at least one more character on the next line, no truncation
|
|
// Otherwise apply the truncation algorithm to the current line.
|
|
// After calculating any truncation, draw.
|
|
// Each time we hit the end of an attribute run, calculate the new font and make sure
|
|
// it fits (vertically) within the size constraint. If not, truncate this line.
|
|
// When we draw, iterate over the attribute runs for this line and draw each run separately
|
|
BOOL lastLine = NO; // used to indicate truncation and to stop the iterating
|
|
NSUInteger lineCount = 1;
|
|
while (idx < len && !lastLine) {
|
|
if (maxLines > 0 && lineCount == maxLines) {
|
|
lastLine = YES;
|
|
}
|
|
// scan left-to-right
|
|
struct {
|
|
NSUInteger index;
|
|
NSUInteger glyphIndex;
|
|
NSUInteger currentRunIdx;
|
|
} indexCache = { idx, glyphIdx, currentRunIdx };
|
|
CGSize lineSize = CGSizeMake(0, currentFont.leading);
|
|
CGFloat lineAscender = currentFont.ascender;
|
|
struct {
|
|
NSUInteger index;
|
|
NSUInteger glyphIndex;
|
|
NSUInteger currentRunIdx;
|
|
CGSize lineSize;
|
|
} lastWrapCache = {0, 0, 0, CGSizeZero};
|
|
BOOL inAlpha = NO; // used for calculating wrap points
|
|
|
|
BOOL finishLine = NO;
|
|
for (;idx <= len && !finishLine;) {
|
|
NSUInteger skipCount = 0;
|
|
if (idx == len) {
|
|
finishLine = YES;
|
|
lastLine = YES;
|
|
} else {
|
|
if (idx >= nextRunStart) {
|
|
// cycle the font and table and grab the next set of glyphs
|
|
do {
|
|
currentRunIdx++;
|
|
READ_RUN();
|
|
} while (idx >= nextRunStart);
|
|
READ_GLYPHS();
|
|
// re-scan the characters to synchronize the glyph index
|
|
for (NSUInteger j = currentRun.index; j < idx; j++) {
|
|
if (UnicharIsHighSurrogate(characters[j]) && j+1<len && UnicharIsLowSurrogate(characters[j+1])) {
|
|
j++;
|
|
}
|
|
glyphIdx++;
|
|
}
|
|
if (currentFont.leading > lineSize.height) {
|
|
lineSize.height = currentFont.leading;
|
|
if (retValue.height + currentFont.ascender > constrainedSize.height) {
|
|
lastLine = YES;
|
|
finishLine = YES;
|
|
}
|
|
}
|
|
lineAscender = MAX(lineAscender, currentFont.ascender);
|
|
}
|
|
unichar c = characters[idx];
|
|
// Mark a wrap point before spaces and after any stretch of non-alpha characters
|
|
BOOL markWrap = NO;
|
|
if (c == (unichar)' ') {
|
|
markWrap = YES;
|
|
} else if ([alphaCharset characterIsMember:c]) {
|
|
if (!inAlpha) {
|
|
markWrap = YES;
|
|
inAlpha = YES;
|
|
}
|
|
} else {
|
|
inAlpha = NO;
|
|
}
|
|
if (markWrap) {
|
|
lastWrapCache = (__typeof__(lastWrapCache)){
|
|
.index = idx,
|
|
.glyphIndex = glyphIdx,
|
|
.currentRunIdx = currentRunIdx,
|
|
.lineSize = lineSize
|
|
};
|
|
}
|
|
// process the line
|
|
if (c == (unichar)'\n' || c == 0x0085) { // U+0085 is the NEXT_LINE unicode character
|
|
finishLine = YES;
|
|
skipCount = 1;
|
|
} else if (c == (unichar)'\r') {
|
|
finishLine = YES;
|
|
// check for CRLF
|
|
if (idx+1 < len && characters[idx+1] == (unichar)'\n') {
|
|
skipCount = 2;
|
|
} else {
|
|
skipCount = 1;
|
|
}
|
|
} else if (lineSize.width + advances[glyphIdx] > constrainedSize.width) {
|
|
finishLine = YES;
|
|
if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
|
|
lastLine = YES;
|
|
}
|
|
// walk backwards if wrapping is necessary
|
|
if (lastWrapCache.index > indexCache.index && lineBreakMode != UILineBreakModeCharacterWrap &&
|
|
(!lastLine || lineBreakMode != UILineBreakModeClip)) {
|
|
// we're doing some sort of word wrapping
|
|
idx = lastWrapCache.index;
|
|
lineSize = lastWrapCache.lineSize;
|
|
if (!lastLine) {
|
|
// re-check if this is the last line
|
|
if (lastWrapCache.currentRunIdx != currentRunIdx) {
|
|
currentRunIdx = lastWrapCache.currentRunIdx;
|
|
READ_RUN();
|
|
READ_GLYPHS();
|
|
}
|
|
if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
|
|
lastLine = YES;
|
|
}
|
|
}
|
|
glyphIdx = lastWrapCache.glyphIndex;
|
|
// skip any spaces
|
|
for (NSUInteger j = idx; j < len && characters[j] == (unichar)' '; j++) {
|
|
skipCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (finishLine) {
|
|
// TODO: support head/middle truncation
|
|
if (lastLine && idx < len && lineBreakMode == UILineBreakModeTailTruncation) {
|
|
// truncate
|
|
unichar ellipsis = 0x2026; // ellipsis (…)
|
|
CGGlyph ellipsisGlyph;
|
|
mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
|
|
CGFloat ellipsisWidth;
|
|
mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
|
|
while ((idx - indexCache.index) > 1 && lineSize.width + ellipsisWidth > constrainedSize.width) {
|
|
// we have more than 1 character and we're too wide, so back up
|
|
idx--;
|
|
if (UnicharIsHighSurrogate(characters[idx]) && UnicharIsLowSurrogate(characters[idx+1])) {
|
|
idx--;
|
|
}
|
|
if (idx < currentRun.index) {
|
|
ZFont *oldFont = currentFont;
|
|
do {
|
|
currentRunIdx--;
|
|
READ_RUN();
|
|
} while (idx < currentRun.index);
|
|
READ_GLYPHS();
|
|
glyphIdx = glyphCount-1;
|
|
if (oldFont != currentFont) {
|
|
mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
|
|
mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
|
|
}
|
|
} else {
|
|
glyphIdx--;
|
|
}
|
|
lineSize.width -= advances[glyphIdx];
|
|
}
|
|
// skip any spaces before truncating
|
|
while ((idx - indexCache.index) > 1 && characters[idx-1] == (unichar)' ') {
|
|
idx--;
|
|
if (idx < currentRun.index) {
|
|
currentRunIdx--;
|
|
READ_RUN();
|
|
READ_GLYPHS();
|
|
glyphIdx = glyphCount-1;
|
|
} else {
|
|
glyphIdx--;
|
|
}
|
|
lineSize.width -= advances[glyphIdx];
|
|
}
|
|
lineSize.width += ellipsisWidth;
|
|
glyphs[glyphIdx] = ellipsisGlyph;
|
|
idx++;
|
|
glyphIdx++;
|
|
}
|
|
retValue.width = MAX(retValue.width, lineSize.width);
|
|
retValue.height += lineSize.height;
|
|
|
|
// draw
|
|
if (performDraw) {
|
|
switch (alignment) {
|
|
case UITextAlignmentLeft:
|
|
drawPoint.x = 0;
|
|
break;
|
|
case UITextAlignmentCenter:
|
|
drawPoint.x = (constrainedSize.width - lineSize.width) / 2.0f;
|
|
break;
|
|
case UITextAlignmentRight:
|
|
drawPoint.x = constrainedSize.width - lineSize.width;
|
|
break;
|
|
}
|
|
NSUInteger stopGlyphIdx = glyphIdx;
|
|
NSUInteger lastRunIdx = currentRunIdx;
|
|
NSUInteger stopCharIdx = idx;
|
|
idx = indexCache.index;
|
|
if (currentRunIdx != indexCache.currentRunIdx) {
|
|
currentRunIdx = indexCache.currentRunIdx;
|
|
READ_RUN();
|
|
READ_GLYPHS();
|
|
}
|
|
glyphIdx = indexCache.glyphIndex;
|
|
for (NSUInteger drawIdx = currentRunIdx; drawIdx <= lastRunIdx; drawIdx++) {
|
|
if (drawIdx != currentRunIdx) {
|
|
currentRunIdx = drawIdx;
|
|
READ_RUN();
|
|
READ_GLYPHS();
|
|
}
|
|
NSUInteger numGlyphs;
|
|
if (drawIdx == lastRunIdx) {
|
|
numGlyphs = stopGlyphIdx - glyphIdx;
|
|
idx = stopCharIdx;
|
|
} else {
|
|
numGlyphs = glyphCount - glyphIdx;
|
|
idx = nextRunStart;
|
|
}
|
|
CGContextSetFont(ctx, currentFont.cgFont);
|
|
CGContextSetFontSize(ctx, currentFont.pointSize);
|
|
// calculate the fragment size
|
|
CGFloat fragmentWidth = 0;
|
|
for (NSUInteger g = 0; g < numGlyphs; g++) {
|
|
fragmentWidth += advances[glyphIdx + g];
|
|
}
|
|
|
|
if (!ignoreColor) {
|
|
UIColor *foregroundColor = getValueOrDefaultForRun(currentRun, ZForegroundColorAttributeName);
|
|
UIColor *backgroundColor = getValueOrDefaultForRun(currentRun, ZBackgroundColorAttributeName);
|
|
if (backgroundColor != nil && ![backgroundColor isEqual:[UIColor clearColor]]) {
|
|
[backgroundColor setFill];
|
|
UIRectFillUsingBlendMode((CGRect){ drawPoint, { fragmentWidth, lineSize.height } }, kCGBlendModeNormal);
|
|
}
|
|
[foregroundColor setFill];
|
|
}
|
|
|
|
CGContextShowGlyphsAtPoint(ctx, drawPoint.x, drawPoint.y + lineAscender, &glyphs[glyphIdx], numGlyphs);
|
|
NSNumber *underlineStyle = getValueOrDefaultForRun(currentRun, ZUnderlineStyleAttributeName);
|
|
if ([underlineStyle integerValue] & ZUnderlineStyleMask) {
|
|
// we only support single for the time being
|
|
UIRectFill(CGRectMake(drawPoint.x, drawPoint.y + lineAscender, fragmentWidth, 1));
|
|
}
|
|
drawPoint.x += fragmentWidth;
|
|
glyphIdx += numGlyphs;
|
|
}
|
|
drawPoint.y += lineSize.height;
|
|
}
|
|
idx += skipCount;
|
|
glyphIdx += skipCount;
|
|
lineCount++;
|
|
} else {
|
|
lineSize.width += advances[glyphIdx];
|
|
glyphIdx++;
|
|
idx++;
|
|
if (idx < len && UnicharIsHighSurrogate(characters[idx-1]) && UnicharIsLowSurrogate(characters[idx])) {
|
|
// skip the second half of the surrogate pair
|
|
idx++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CFRelease(fontTableMap);
|
|
free(glyphs);
|
|
free(advances);
|
|
free(characters);
|
|
|
|
#undef READ_GLYPHS
|
|
#undef READ_RUN
|
|
|
|
return retValue;
|
|
}
|
|
|
|
static NSArray *attributeRunForFont(ZFont *font) {
|
|
return [NSArray arrayWithObject:[ZAttributeRun attributeRunWithIndex:0
|
|
attributes:[NSDictionary dictionaryWithObject:font
|
|
forKey:ZFontAttributeName]]];
|
|
}
|
|
|
|
static CGSize drawTextInRect(CGRect rect, NSString *text, NSArray *attributes, UILineBreakMode lineBreakMode,
|
|
UITextAlignment alignment, NSUInteger numberOfLines, BOOL ignoreColor) {
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
|
|
CGContextSaveGState(ctx);
|
|
|
|
// flip it upside-down because our 0,0 is upper-left, whereas ttfs are for screens where 0,0 is lower-left
|
|
CGAffineTransform textTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
|
|
CGContextSetTextMatrix(ctx, textTransform);
|
|
|
|
CGContextTranslateCTM(ctx, rect.origin.x, rect.origin.y);
|
|
|
|
CGContextSetTextDrawingMode(ctx, kCGTextFill);
|
|
CGSize size = drawOrSizeTextConstrainedToSize(YES, text, attributes, rect.size, numberOfLines, lineBreakMode, alignment, ignoreColor);
|
|
|
|
CGContextRestoreGState(ctx);
|
|
|
|
return size;
|
|
}
|
|
|
|
@implementation NSString (FontLabelStringDrawing)
|
|
// CGFontRef-based methods
|
|
- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
|
|
return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize]];
|
|
}
|
|
|
|
- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size {
|
|
return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size];
|
|
}
|
|
|
|
- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size
|
|
lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size lineBreakMode:lineBreakMode];
|
|
}
|
|
|
|
- (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
|
|
return [self drawAtPoint:point withZFont:[ZFont fontWithCGFont:font size:pointSize]];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
|
|
return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize]];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
|
|
lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
|
|
return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode alignment:alignment];
|
|
}
|
|
|
|
// ZFont-based methods
|
|
- (CGSize)sizeWithZFont:(ZFont *)font {
|
|
CGSize size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
|
|
UILineBreakModeClip, UITextAlignmentLeft, YES);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size {
|
|
return [self sizeWithZFont:font constrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
|
|
}
|
|
|
|
/*
|
|
According to experimentation with UIStringDrawing, this can actually return a CGSize whose height is greater
|
|
than the one passed in. The two cases are as follows:
|
|
1. If the given size parameter's height is smaller than a single line, the returned value will
|
|
be the height of one line.
|
|
2. If the given size parameter's height falls between multiples of a line height, and the wrapped string
|
|
actually extends past the size.height, and the difference between size.height and the previous multiple
|
|
of a line height is >= the font's ascender, then the returned size's height is extended to the next line.
|
|
To put it simply, if the baseline point of a given line falls in the given size, the entire line will
|
|
be present in the output size.
|
|
*/
|
|
- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, 0, lineBreakMode, UITextAlignmentLeft, YES);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
|
|
numberOfLines:(NSUInteger)numberOfLines {
|
|
size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, numberOfLines, lineBreakMode, UITextAlignmentLeft, YES);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font {
|
|
return [self drawAtPoint:point forWidth:CGFLOAT_MAX withZFont:font lineBreakMode:UILineBreakModeClip];
|
|
}
|
|
|
|
- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self, attributeRunForFont(font), lineBreakMode, UITextAlignmentLeft, 1, YES);
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font {
|
|
return [self drawInRect:rect withZFont:font lineBreakMode:UILineBreakModeWordWrap];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return [self drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
|
|
alignment:(UITextAlignment)alignment {
|
|
return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, 0, YES);
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
|
|
alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines {
|
|
return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, numberOfLines, YES);
|
|
}
|
|
@end
|
|
|
|
@implementation ZAttributedString (ZAttributedStringDrawing)
|
|
- (CGSize)size {
|
|
CGSize size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
|
|
UILineBreakModeClip, UITextAlignmentLeft, NO);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)sizeConstrainedToSize:(CGSize)size {
|
|
return [self sizeConstrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
|
|
}
|
|
|
|
- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, 0, lineBreakMode, UITextAlignmentLeft, NO);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
|
|
numberOfLines:(NSUInteger)numberOfLines {
|
|
size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, numberOfLines, lineBreakMode, UITextAlignmentLeft, NO);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
- (CGSize)drawAtPoint:(CGPoint)point {
|
|
return [self drawAtPoint:point forWidth:CGFLOAT_MAX lineBreakMode:UILineBreakModeClip];
|
|
}
|
|
|
|
- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self.string, self.attributes, lineBreakMode, UITextAlignmentLeft, 1, NO);
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect {
|
|
return [self drawInRect:rect withLineBreakMode:UILineBreakModeWordWrap];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode {
|
|
return [self drawInRect:rect withLineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
|
|
return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, 0, NO);
|
|
}
|
|
|
|
- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment
|
|
numberOfLines:(NSUInteger)numberOfLines {
|
|
return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, numberOfLines, NO);
|
|
}
|
|
@end
|
|
|
|
@implementation FontLabelStringDrawingHelper
|
|
+ (CGSize)sizeWithZFont:(NSString*)string zfont:(ZFont *)font {
|
|
CGSize size = drawOrSizeTextConstrainedToSize(NO, string, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
|
|
UILineBreakModeClip, UITextAlignmentLeft, YES);
|
|
return CGSizeMake(ceilf(size.width), ceilf(size.height));
|
|
}
|
|
|
|
+ (CGSize)sizeWithZFont:(NSString *)string zfont:(ZFont *)font constrainedToSize:(CGSize)size {
|
|
CGSize s = drawOrSizeTextConstrainedToSize(NO, string, attributeRunForFont(font), size, 0, UILineBreakModeWordWrap, UITextAlignmentLeft, YES);
|
|
return CGSizeMake(ceilf(s.width), ceilf(s.height));
|
|
}
|
|
|
|
|
|
|
|
+ (CGSize)drawInRect:(NSString*)string rect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
|
|
alignment:(UITextAlignment)alignment {
|
|
return [string drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:alignment];
|
|
}
|
|
@end
|
|
|