From b19f4cf251c6424e95e141ccc87f98b2140bca01 Mon Sep 17 00:00:00 2001 From: Pietro Gagliardi Date: Mon, 23 Jan 2017 11:43:03 -0500 Subject: [PATCH] Finished writing a NSLayoutManager-based text system. Not quite perfect yet, but we're getting somewhere! --- darwin/drawtext.m | 315 ++++++++++++++++++--------------------------- darwin/fontmatch.m | 13 +- 2 files changed, 133 insertions(+), 195 deletions(-) diff --git a/darwin/drawtext.m b/darwin/drawtext.m index a84b68b5..6a16af1e 100644 --- a/darwin/drawtext.m +++ b/darwin/drawtext.m @@ -2,16 +2,26 @@ #import "uipriv_darwin.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 { - CFAttributedStringRef attrstr; + // NSTextStorage is subclassed from NSMutableAttributedString + NSTextStorage *attrstr; + NSTextContainer *container; + NSLayoutManager *layoutManager; // the width as passed into uiDrawTextLayout constructors double width; - CTFramesetterRef framesetter; - +#if 0 /* TODO */ // the *actual* size of the frame // 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 @@ -19,188 +29,101 @@ struct uiDrawTextLayout { // 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) CGSize size; +#endif - CGPathRef path; - CTFrameRef frame; - - CFArrayRef lines; - CFIndex nLines; - // we compute this once when first creating the layout - uiDrawTextLayoutLineMetrics *lineMetrics; + NSMutableArray *lineInfo; // for converting CFAttributedString indices to byte offsets size_t *u16tou8; 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; - CTFontRef font; + NSFontDescriptor *desc; + NSFont *font; - desc = fontdescToCTFontDescriptor(fd); - font = CTFontCreateWithFontDescriptor(desc, fd->Size, NULL); - CFRelease(desc); // TODO correct? + desc = fontdescToNSFontDescriptor(fd); + font = [NSFont fontWithDescriptor:desc size:fd->Size]; + [desc release]; return font; } -static CFAttributedStringRef attrstrToCoreFoundation(uiAttributedString *s, uiDrawFontDescriptor *defaultFont) +static NSTextStorage *attrstrToTextStorage(uiAttributedString *s, uiDrawFontDescriptor *defaultFont) { - CFStringRef cfstr; - CFMutableDictionaryRef defaultAttrs; - CTFontRef defaultCTFont; - CFAttributedStringRef base; - CFMutableAttributedStringRef mas; + NSString *nsstr; + NSMutableDictionary *defaultAttrs; + NSTextStorage *attrstr; - cfstr = CFStringCreateWithCharacters(NULL, attrstrUTF16(s), attrstrUTF16Len(s)); - if (cfstr == NULL) { - // TODO - } - defaultAttrs = CFDictionaryCreateMutable(NULL, 1, - &kCFCopyStringDictionaryKeyCallBacks, - &kCFTypeDictionaryValueCallBacks); - if (defaultAttrs == NULL) { - // TODO - } - defaultCTFont = fontdescToCTFont(defaultFont); - CFDictionaryAddValue(defaultAttrs, kCTFontAttributeName, defaultCTFont); - CFRelease(defaultCTFont); + nsstr = [[NSString alloc] initWithCharacters:attrstrUTF16(s) + length:attrstrUTF16Len(s)]; - base = CFAttributedStringCreate(NULL, cfstr, defaultAttrs); - if (base == NULL) { - // TODO - } - CFRelease(cfstr); - CFRelease(defaultAttrs); - mas = CFAttributedStringCreateMutableCopy(NULL, 0, base); - CFRelease(base); + defaultAttrs = [NSMutableDictionary new]; + [defaultAttrs setObject:fontdescToNSFont(defaultFont) + forKey:NSFontAttributeName]; - CFAttributedStringBeginEditing(mas); + attrstr = [[NSTextStorage alloc] initWithString:nsstr + attributes:defaultAttrs]; + [defaultAttrs release]; + [nsstr release]; + + [attrstr beginEditing]; // TODO copy in the attributes - CFAttributedStringEndEditing(mas); + [attrstr endEditing]; - return mas; -} - -// 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; + return attrstr; } +// TODO fine-tune all the properties uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width) { uiDrawTextLayout *tl; CGFloat cgwidth; - CFRange range, unused; - CGRect rect; + // TODO correct type? + NSUInteger index; tl = uiNew(uiDrawTextLayout); - tl->attrstr = attrstrToCoreFoundation(s, defaultFont); - range.location = 0; - range.length = CFAttributedStringGetLength(tl->attrstr); + tl->attrstr = attrstrToTextStorage(s, defaultFont); tl->width = width; - // TODO CTFrameProgression for RTL/LTR - // TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing - tl->framesetter = CTFramesetterCreateWithAttributedString(tl->attrstr); - if (tl->framesetter == NULL) { - // TODO - } - + // TODO the documentation on the size property implies this might not be necessary? cgwidth = (CGFloat) width; if (cgwidth < 0) cgwidth = CGFLOAT_MAX; - // TODO these seem to be floor()'d or truncated? - // TODO double check to make sure this TODO was right - tl->size = CTFramesetterSuggestFrameSizeWithConstraints(tl->framesetter, - range, - // TODO kCTFramePathWidthAttributeName? - NULL, - CGSizeMake(cgwidth, CGFLOAT_MAX), - &unused); // not documented as accepting NULL (TODO really?) + // TODO rename to tl->textContainer + tl->container = [[NSTextContainer alloc] initWithSize:NSMakeSize(cgwidth, CGFLOAT_MAX)]; + // TODO pull the reference for this + [tl->container setLineFragmentPadding:0]; - rect.origin = CGPointZero; - rect.size = tl->size; - tl->path = CGPathCreateWithRect(rect, NULL); - tl->frame = CTFramesetterCreateFrame(tl->framesetter, - range, - tl->path, - // TODO kCTFramePathWidthAttributeName? - NULL); - if (tl->frame == NULL) { - // TODO + tl->layoutManager = [[NSLayoutManager alloc] init]; + + [tl->layoutManager addTextContainer:tl->container]; + [tl->attrstr addLayoutManager:tl->layoutManager]; + // and force a re-layout (TODO get source + [tl->layoutManager glyphRangeForTextContainer:tl->container]; + + // TODO equivalent of CTFrameProgression for RTL/LTR? + + // now collect line information; see https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html + tl->lineInfo = [NSMutableArray 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 tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nu16tou8)); @@ -210,74 +133,91 @@ uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescripto void uiDrawFreeTextLayout(uiDrawTextLayout *tl) { uiFree(tl->u16tou8); - uiFree(tl->lineMetrics); - // TODO release tl->lines? - CFRelease(tl->frame); - CFRelease(tl->path); - CFRelease(tl->framesetter); - CFRelease(tl->attrstr); + [tl->lineInfo release]; + [tl->layoutManager release]; + [tl->container release]; + [tl->attrstr release]; uiFree(tl); } // TODO document that (x,y) is the top-left corner of the *entire frame* 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 - // 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) - // TODO how is this affected by a non-identity CTM? - CGContextTranslateCTM(c->c, 0, c->height); - CGContextScaleCTM(c->c, 1.0, -1.0); - CGContextSetTextMatrix(c->c, CGAffineTransformIdentity); + CGContextFlush(c->c); // just to be safe + [NSGraphicsContext saveGraphicsState]; + gc = [NSGraphicsContext graphicsContextWithGraphicsPort:c->c flipped:YES]; + [NSGraphicsContext setCurrentContext:gc]; - // wait, that's not enough; we need to offset y values to account for our new flipping - // TODO explain this calculation - y = c->height - tl->size.height - y; + // TODO is this the right point? + // TODO does this draw with the proper default styles? + [tl->layoutManager drawGlyphsForGlyphRange:[tl->layoutManager glyphRangeForTextContainer:tl->container] + atPoint:NSMakePoint(x, y)]; - // CTFrameDraw() draws in the path we specified when creating the frame - // this means that in our usage, CTFrameDraw() will draw at (0,0) - // so move the origin to be at (x,y) instead - // TODO are the signs correct? - CGContextTranslateCTM(c->c, x, y); - - CTFrameDraw(tl->frame, c->c); - - CGContextRestoreGState(c->c); + [gc flushGraphics]; // just to be safe + [NSGraphicsContext restoreGraphicsState]; + // TODO release gc? } +// 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 width doesn't include trailing whitespace... // TODO figure out how paragraph spacing should play into this +// } void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height) { - *width = tl->size.width; - *height = tl->size.height; + NSRect r; + + // 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) { - return tl->nLines; + return [tl->lineInfo count]; } void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end) { - CTLineRef lr; - CFRange range; + lineInfo *li; - lr = (CTLineRef) CFArrayGetValueAtIndex(tl->lines, line); - range = CTLineGetStringRange(lr); - *start = tl->u16tou8[range.location]; - *end = tl->u16tou8[range.location + range.length]; + li = (lineInfo *) [tl->lineInfo objectAtIndex:line]; + *start = tl->u16tou8[li.characterRange.location]; + *end = tl->u16tou8[li.characterRange.location + li.characterRange.length]; } 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) { +#if 0 /* TODO */ CFIndex i; CTLineRef line; CFIndex pos; @@ -320,6 +260,7 @@ void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTex // TODO } result->Pos = tl->u16tou8[pos]; +#endif } void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r) diff --git a/darwin/fontmatch.m b/darwin/fontmatch.m index 27440841..8e81400f 100644 --- a/darwin/fontmatch.m +++ b/darwin/fontmatch.m @@ -21,7 +21,6 @@ struct closeness { static double doubleAttr(NSDictionary *traits, NSString *attr) { NSNumber *n; - double val; n = (NSNumber *) [traits objectForKey:attr]; return [n doubleValue]; @@ -90,7 +89,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei n = [matching count]; if (n == 0) { // likewise - [matching release]; +//TODO [matching release]; return against; } @@ -100,7 +99,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei closeness[i].index = i; current = (NSFontDescriptor *) [matching objectAtIndex:i]; - traits = (NSDictionary *) [current objectAtIndex:NSFontTraitsAttribute]; + traits = (NSDictionary *) [current objectForKey:NSFontTraitsAttribute]; if (traits == nil) { // couldn't get traits; be safe by ranking it lowest // LONGTERM figure out what the longest possible distances are @@ -143,7 +142,7 @@ static NSFontDescriptor *matchTraits(NSFontDescriptor *against, double targetWei // release everything uiFree(closeness); - [matching release]; +//TODO [matching release]; // and release the original descriptor since we no longer need it [against release]; @@ -207,15 +206,13 @@ static const double stretchesToCTWidths[] = { NSFontDescriptor *fontdescToNSFontDescriptor(uiDrawFontDescriptor *fd) { NSMutableDictionary *attrs; - CFStringRef cffamily; - CFNumberRef cfsize; - CTFontDescriptorRef basedesc; + NSFontDescriptor *basedesc; attrs = [NSMutableDictionary new]; [attrs setObject:[NSString stringWithUTF8String:fd->Family] forKey:NSFontFamilyAttribute]; [attrs setObject:[NSNumber numberWithDouble:fd->Size] - forKye:NSFontSizeAttribute]; + forKey:NSFontSizeAttribute]; basedesc = [[NSFontDescriptor alloc] initWithFontAttributes:attrs]; [attrs release];