Finished writing a NSLayoutManager-based text system. Not quite perfect yet, but we're getting somewhere!

This commit is contained in:
Pietro Gagliardi 2017-01-23 11:43:03 -05:00
parent 4e2dc90f4f
commit b19f4cf251
2 changed files with 133 additions and 195 deletions

View File

@ -2,16 +2,26 @@
#import "uipriv_darwin.h" #import "uipriv_darwin.h"
#import "draw.h" #import "draw.h"
// TODO what happens if nLines == 0 in any function? @interface lineInfo : NSObject
@property NSRange glyphRange;
@property NSRange characterRange;
@property NSRect lineRect;
@property CGFloat baselineOffset;
@end
@implementation lineInfo
@end
struct uiDrawTextLayout { struct uiDrawTextLayout {
CFAttributedStringRef attrstr; // NSTextStorage is subclassed from NSMutableAttributedString
NSTextStorage *attrstr;
NSTextContainer *container;
NSLayoutManager *layoutManager;
// the width as passed into uiDrawTextLayout constructors // the width as passed into uiDrawTextLayout constructors
double width; double width;
CTFramesetterRef framesetter; #if 0 /* TODO */
// the *actual* size of the frame // the *actual* size of the frame
// note: technically, metrics returned from frame are relative to CGPathGetPathBoundingBox(tl->path) // note: technically, metrics returned from frame are relative to CGPathGetPathBoundingBox(tl->path)
// however, from what I can gather, for a path created by CGPathCreateWithRect(), like we do (with a NULL transform), CGPathGetPathBoundingBox() seems to just return the standardized form of the rect used to create the path // however, from what I can gather, for a path created by CGPathCreateWithRect(), like we do (with a NULL transform), CGPathGetPathBoundingBox() seems to just return the standardized form of the rect used to create the path
@ -19,188 +29,101 @@ struct uiDrawTextLayout {
// so we can just use tl->size for adjustments // so we can just use tl->size for adjustments
// we don't need to adjust coordinates by any origin since our rect origin is (0, 0) // we don't need to adjust coordinates by any origin since our rect origin is (0, 0)
CGSize size; CGSize size;
#endif
CGPathRef path; NSMutableArray<lineInfo *> *lineInfo;
CTFrameRef frame;
CFArrayRef lines;
CFIndex nLines;
// we compute this once when first creating the layout
uiDrawTextLayoutLineMetrics *lineMetrics;
// for converting CFAttributedString indices to byte offsets // for converting CFAttributedString indices to byte offsets
size_t *u16tou8; size_t *u16tou8;
size_t nu16tou8; // TODO I don't like the casing of this name size_t nu16tou8; // TODO I don't like the casing of this name
}; };
static CTFontRef fontdescToCTFont(uiDrawFontDescriptor *fd) static NSFont *fontdescToNSFont(uiDrawFontDescriptor *fd)
{ {
CTFontDescriptorRef desc; NSFontDescriptor *desc;
CTFontRef font; NSFont *font;
desc = fontdescToCTFontDescriptor(fd); desc = fontdescToNSFontDescriptor(fd);
font = CTFontCreateWithFontDescriptor(desc, fd->Size, NULL); font = [NSFont fontWithDescriptor:desc size:fd->Size];
CFRelease(desc); // TODO correct? [desc release];
return font; return font;
} }
static CFAttributedStringRef attrstrToCoreFoundation(uiAttributedString *s, uiDrawFontDescriptor *defaultFont) static NSTextStorage *attrstrToTextStorage(uiAttributedString *s, uiDrawFontDescriptor *defaultFont)
{ {
CFStringRef cfstr; NSString *nsstr;
CFMutableDictionaryRef defaultAttrs; NSMutableDictionary *defaultAttrs;
CTFontRef defaultCTFont; NSTextStorage *attrstr;
CFAttributedStringRef base;
CFMutableAttributedStringRef mas;
cfstr = CFStringCreateWithCharacters(NULL, attrstrUTF16(s), attrstrUTF16Len(s)); nsstr = [[NSString alloc] initWithCharacters:attrstrUTF16(s)
if (cfstr == NULL) { length:attrstrUTF16Len(s)];
// TODO
}
defaultAttrs = CFDictionaryCreateMutable(NULL, 1,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
if (defaultAttrs == NULL) {
// TODO
}
defaultCTFont = fontdescToCTFont(defaultFont);
CFDictionaryAddValue(defaultAttrs, kCTFontAttributeName, defaultCTFont);
CFRelease(defaultCTFont);
base = CFAttributedStringCreate(NULL, cfstr, defaultAttrs); defaultAttrs = [NSMutableDictionary new];
if (base == NULL) { [defaultAttrs setObject:fontdescToNSFont(defaultFont)
// TODO forKey:NSFontAttributeName];
}
CFRelease(cfstr);
CFRelease(defaultAttrs);
mas = CFAttributedStringCreateMutableCopy(NULL, 0, base);
CFRelease(base);
CFAttributedStringBeginEditing(mas); attrstr = [[NSTextStorage alloc] initWithString:nsstr
attributes:defaultAttrs];
[defaultAttrs release];
[nsstr release];
[attrstr beginEditing];
// TODO copy in the attributes // TODO copy in the attributes
CFAttributedStringEndEditing(mas); [attrstr endEditing];
return mas; return attrstr;
}
// TODO this is wrong for our hit-test example's multiple combining character example
static uiDrawTextLayoutLineMetrics *computeLineMetrics(CTFrameRef frame, CGSize size)
{
uiDrawTextLayoutLineMetrics *metrics;
CFArrayRef lines;
CTLineRef line;
CFIndex i, n;
CGFloat ypos;
CGRect bounds, boundsNoLeading;
CGFloat ascent, descent, leading;
CGPoint *origins;
lines = CTFrameGetLines(frame);
n = CFArrayGetCount(lines);
metrics = (uiDrawTextLayoutLineMetrics *) uiAlloc(n * sizeof (uiDrawTextLayoutLineMetrics), "uiDrawTextLayoutLineMetrics[] (text layout)");
origins = (CGPoint *) uiAlloc(n * sizeof (CGPoint), "CGPoint[] (text layout)");
CTFrameGetLineOrigins(frame, CFRangeMake(0, n), origins);
ypos = size.height;
for (i = 0; i < n; i++) {
line = (CTLineRef) CFArrayGetValueAtIndex(lines, i);
bounds = CTLineGetBoundsWithOptions(line, 0);
boundsNoLeading = CTLineGetBoundsWithOptions(line, kCTLineBoundsExcludeTypographicLeading);
// this is equivalent to boundsNoLeading.size.height + boundsNoLeading.origin.y (manually verified)
ascent = bounds.size.height + bounds.origin.y;
descent = -boundsNoLeading.origin.y;
// TODO does this preserve leading sign?
leading = -bounds.origin.y - descent;
// Core Text always rounds these up for paragraph style calculations; there is a flag to control it but it's inaccessible (and this behavior is turned off for old versions of iPhoto)
ascent = floor(ascent + 0.5);
descent = floor(descent + 0.5);
if (leading > 0)
leading = floor(leading + 0.5);
metrics[i].X = origins[i].x;
metrics[i].Y = origins[i].y - descent - leading;
metrics[i].Width = bounds.size.width;
metrics[i].Height = ascent + descent + leading;
metrics[i].BaselineY = origins[i].y;
metrics[i].Ascent = ascent;
metrics[i].Descent = descent;
metrics[i].Leading = leading;
// TODO
metrics[i].ParagraphSpacingBefore = 0;
metrics[i].LineHeightSpace = 0;
metrics[i].LineSpacing = 0;
metrics[i].ParagraphSpacing = 0;
// and finally advance to the next line
ypos += metrics[i].Height;
}
// okay, but now all these metrics are unflipped
// we need to flip them
for (i = 0; i < n; i++) {
metrics[i].Y = size.height - metrics[i].Y;
// go from bottom-left corner to top-left
metrics[i].Y -= metrics[i].Height;
metrics[i].BaselineY = size.height - metrics[i].BaselineY;
// TODO also adjust by metrics[i].Height?
}
uiFree(origins);
return metrics;
} }
// TODO fine-tune all the properties
uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width) uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width)
{ {
uiDrawTextLayout *tl; uiDrawTextLayout *tl;
CGFloat cgwidth; CGFloat cgwidth;
CFRange range, unused; // TODO correct type?
CGRect rect; NSUInteger index;
tl = uiNew(uiDrawTextLayout); tl = uiNew(uiDrawTextLayout);
tl->attrstr = attrstrToCoreFoundation(s, defaultFont); tl->attrstr = attrstrToTextStorage(s, defaultFont);
range.location = 0;
range.length = CFAttributedStringGetLength(tl->attrstr);
tl->width = width; tl->width = width;
// TODO CTFrameProgression for RTL/LTR // TODO the documentation on the size property implies this might not be necessary?
// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing
tl->framesetter = CTFramesetterCreateWithAttributedString(tl->attrstr);
if (tl->framesetter == NULL) {
// TODO
}
cgwidth = (CGFloat) width; cgwidth = (CGFloat) width;
if (cgwidth < 0) if (cgwidth < 0)
cgwidth = CGFLOAT_MAX; cgwidth = CGFLOAT_MAX;
// TODO these seem to be floor()'d or truncated? // TODO rename to tl->textContainer
// TODO double check to make sure this TODO was right tl->container = [[NSTextContainer alloc] initWithSize:NSMakeSize(cgwidth, CGFLOAT_MAX)];
tl->size = CTFramesetterSuggestFrameSizeWithConstraints(tl->framesetter, // TODO pull the reference for this
range, [tl->container setLineFragmentPadding:0];
// TODO kCTFramePathWidthAttributeName?
NULL,
CGSizeMake(cgwidth, CGFLOAT_MAX),
&unused); // not documented as accepting NULL (TODO really?)
rect.origin = CGPointZero; tl->layoutManager = [[NSLayoutManager alloc] init];
rect.size = tl->size;
tl->path = CGPathCreateWithRect(rect, NULL); [tl->layoutManager addTextContainer:tl->container];
tl->frame = CTFramesetterCreateFrame(tl->framesetter, [tl->attrstr addLayoutManager:tl->layoutManager];
range, // and force a re-layout (TODO get source
tl->path, [tl->layoutManager glyphRangeForTextContainer:tl->container];
// TODO kCTFramePathWidthAttributeName?
NULL); // TODO equivalent of CTFrameProgression for RTL/LTR?
if (tl->frame == NULL) {
// TODO // now collect line information; see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html
tl->lineInfo = [NSMutableArray<lineInfo *> new];
index = 0;
while (index < [tl->layoutManager numberOfGlyphs]) {
NSRange glyphRange;
NSRect lineRect;
lineInfo *li;
lineRect = [tl->layoutManager lineFragmentRectForGlyphAtIndex:index effectiveRange:&glyphRange];
li = [lineInfo new];
li.glyphRange = glyphRange;
li.characterRange = [tl->layoutManager characterRangeForGlyphRange:li.glyphRange actualGlyphRange:NULL];
li.lineRect = lineRect;
// and this is from http://www.cocoabuilder.com/archive/cocoa/308568-how-to-get-baseline-info.html and http://www.cocoabuilder.com/archive/cocoa/199283-height-and-location-of-text-within-line-in-nslayoutmanager-ignoring-spacing.html
li.baselineOffset = [[tl->layoutManager typesetter] baselineOffsetInLayoutManager:tl->layoutManager glyphIndex:index];
[tl->lineInfo addObject:li];
[li release];
index = glyphRange.location + glyphRange.length;
} }
tl->lines = CTFrameGetLines(tl->frame);
tl->nLines = CFArrayGetCount(tl->lines);
tl->lineMetrics = computeLineMetrics(tl->frame, tl->size);
// and finally copy the UTF-16 to UTF-8 index conversion table // and finally copy the UTF-16 to UTF-8 index conversion table
tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8)); tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8));
@ -210,74 +133,91 @@ uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescripto
void uiDrawFreeTextLayout(uiDrawTextLayout *tl) void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{ {
uiFree(tl->u16tou8); uiFree(tl->u16tou8);
uiFree(tl->lineMetrics); [tl->lineInfo release];
// TODO release tl->lines? [tl->layoutManager release];
CFRelease(tl->frame); [tl->container release];
CFRelease(tl->path); [tl->attrstr release];
CFRelease(tl->framesetter);
CFRelease(tl->attrstr);
uiFree(tl); uiFree(tl);
} }
// TODO document that (x,y) is the top-left corner of the *entire frame* // TODO document that (x,y) is the top-left corner of the *entire frame*
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y) void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
{ {
CGContextSaveGState(c->c); NSGraphicsContext *gc;
// Core Text doesn't draw onto a flipped view correctly; we have to pretend it was unflipped CGContextFlush(c->c); // just to be safe
// see the iOS bits of the first example at https://developer.apple.com/library/mac/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW1 (iOS is naturally flipped) [NSGraphicsContext saveGraphicsState];
// TODO how is this affected by a non-identity CTM? gc = [NSGraphicsContext graphicsContextWithGraphicsPort:c->c flipped:YES];
CGContextTranslateCTM(c->c, 0, c->height); [NSGraphicsContext setCurrentContext:gc];
CGContextScaleCTM(c->c, 1.0, -1.0);
CGContextSetTextMatrix(c->c, CGAffineTransformIdentity);
// wait, that's not enough; we need to offset y values to account for our new flipping // TODO is this the right point?
// TODO explain this calculation // TODO does this draw with the proper default styles?
y = c->height - tl->size.height - y; [tl->layoutManager drawGlyphsForGlyphRange:[tl->layoutManager glyphRangeForTextContainer:tl->container]
atPoint:NSMakePoint(x, y)];
// CTFrameDraw() draws in the path we specified when creating the frame [gc flushGraphics]; // just to be safe
// this means that in our usage, CTFrameDraw() will draw at (0,0) [NSGraphicsContext restoreGraphicsState];
// so move the origin to be at (x,y) instead // TODO release gc?
// TODO are the signs correct?
CGContextTranslateCTM(c->c, x, y);
CTFrameDraw(tl->frame, c->c);
CGContextRestoreGState(c->c);
} }
// TODO update all of these {
// TODO document that the width and height of a layout is not necessarily the sum of the widths and heights of its constituent lines; this is definitely untrue on OS X, where lines are placed in such a way that the distance between baselines is always integral // TODO document that the width and height of a layout is not necessarily the sum of the widths and heights of its constituent lines; this is definitely untrue on OS X, where lines are placed in such a way that the distance between baselines is always integral
// TODO width doesn't include trailing whitespace... // TODO width doesn't include trailing whitespace...
// TODO figure out how paragraph spacing should play into this // TODO figure out how paragraph spacing should play into this
// }
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height) void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
{ {
*width = tl->size.width; NSRect r;
*height = tl->size.height;
// see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
r = [tl->layoutManager usedRectForTextContainer:tl->container];
*width = r.size.width;
*height = r.size.height;
} }
int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl) int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
{ {
return tl->nLines; return [tl->lineInfo count];
} }
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end) void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
{ {
CTLineRef lr; lineInfo *li;
CFRange range;
lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line); li = (lineInfo *) [tl->lineInfo objectAtIndex:line];
range = CTLineGetStringRange(lr); *start = tl->u16tou8[li.characterRange.location];
*start = tl->u16tou8[range.location]; *end = tl->u16tou8[li.characterRange.location + li.characterRange.length];
*end = tl->u16tou8[range.location + range.length];
} }
void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m) void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
{ {
*m = tl->lineMetrics[line]; lineInfo *li;
li = (lineInfo *) [tl->lineInfo objectAtIndex:line];
m->X = li.lineRect.origin.x;
m->Y = li.lineRect.origin.y;
m->Width = li.lineRect.size.width;
m->Height = li.lineRect.size.height;
// TODO is this correct?
m->BaselineY = (m->X + m->Height) - li.baselineOffset;
// TODO
m->Ascent = 10000;
m->Descent = 10000;
m->Leading = 10000;
// TODO
m->ParagraphSpacingBefore = 0;
m->LineHeightSpace = 0;
m->LineSpacing = 0;
m->ParagraphSpacing = 0;
} }
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result) void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result)
{ {
#if 0 /* TODO */
CFIndex i; CFIndex i;
CTLineRef line; CTLineRef line;
CFIndex pos; CFIndex pos;
@ -320,6 +260,7 @@ void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTex
// TODO // TODO
} }
result->Pos = tl->u16tou8[pos]; result->Pos = tl->u16tou8[pos];
#endif
} }
void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r) void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r)

View File

@ -21,7 +21,6 @@ struct closeness {
static double doubleAttr(NSDictionary *traits, NSString *attr) static double doubleAttr(NSDictionary *traits, NSString *attr)
{ {
NSNumber *n; NSNumber *n;
double val;
n = (NSNumber *) [traits objectForKey:attr]; n = (NSNumber *) [traits objectForKey:attr];
return [n doubleValue]; return [n doubleValue];
@ -90,7 +89,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei
n = [matching count]; n = [matching count];
if (n == 0) { if (n == 0) {
// likewise // likewise
[matching release]; //TODO [matching release];
return against; return against;
} }
@ -100,7 +99,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei
closeness[i].index = i; closeness[i].index = i;
current = (NSFontDescriptor *) [matching objectAtIndex:i]; current = (NSFontDescriptor *) [matching objectAtIndex:i];
traits = (NSDictionary *) [current objectAtIndex:NSFontTraitsAttribute]; traits = (NSDictionary *) [current objectForKey:NSFontTraitsAttribute];
if (traits == nil) { if (traits == nil) {
// couldn't get traits; be safe by ranking it lowest // couldn't get traits; be safe by ranking it lowest
// LONGTERM figure out what the longest possible distances are // LONGTERM figure out what the longest possible distances are
@ -143,7 +142,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei
// release everything // release everything
uiFree(closeness); uiFree(closeness);
[matching release]; //TODO [matching release];
// and release the original descriptor since we no longer need it // and release the original descriptor since we no longer need it
[against release]; [against release];
@ -207,15 +206,13 @@ static const double stretchesToCTWidths[] = {
NSFontDescriptor *fontdescToNSFontDescriptor(uiDrawFontDescriptor *fd) NSFontDescriptor *fontdescToNSFontDescriptor(uiDrawFontDescriptor *fd)
{ {
NSMutableDictionary *attrs; NSMutableDictionary *attrs;
CFStringRef cffamily; NSFontDescriptor *basedesc;
CFNumberRef cfsize;
CTFontDescriptorRef basedesc;
attrs = [NSMutableDictionary new]; attrs = [NSMutableDictionary new];
[attrs setObject:[NSString stringWithUTF8String:fd->Family] [attrs setObject:[NSString stringWithUTF8String:fd->Family]
forKey:NSFontFamilyAttribute]; forKey:NSFontFamilyAttribute];
[attrs setObject:[NSNumber numberWithDouble:fd->Size] [attrs setObject:[NSNumber numberWithDouble:fd->Size]
forKye:NSFontSizeAttribute]; forKey:NSFontSizeAttribute];
basedesc = [[NSFontDescriptor alloc] initWithFontAttributes:attrs]; basedesc = [[NSFontDescriptor alloc] initWithFontAttributes:attrs];
[attrs release]; [attrs release];