// 12 february 2017 #import "uipriv_darwin.h" #import "attrstr.h" // this is what AppKit does internally // WebKit does this too; see https://github.com/adobe/webkit/blob/master/Source/WebCore/platform/graphics/mac/GraphicsContextMac.mm static NSColor *spellingColor = nil; static NSColor *grammarColor = nil; static NSColor *auxiliaryColor = nil; static NSColor *tryColorNamed(NSString *name) { NSImage *img; img = [NSImage imageNamed:name]; if (img == nil) return nil; return [NSColor colorWithPatternImage:img]; } void uiprivInitUnderlineColors(void) { spellingColor = tryColorNamed(@"NSSpellingDot"); if (spellingColor == nil) { // WebKit says this is needed for "older systems"; not sure how old, but 10.11 AppKit doesn't look for this spellingColor = tryColorNamed(@"SpellingDot"); if (spellingColor == nil) spellingColor = [NSColor redColor]; } [spellingColor retain]; // override autoreleasing grammarColor = tryColorNamed(@"NSGrammarDot"); if (grammarColor == nil) { // WebKit says this is needed for "older systems"; not sure how old, but 10.11 AppKit doesn't look for this grammarColor = tryColorNamed(@"GrammarDot"); if (grammarColor == nil) grammarColor = [NSColor greenColor]; } [grammarColor retain]; // override autoreleasing auxiliaryColor = tryColorNamed(@"NSCorrectionDot"); if (auxiliaryColor == nil) { // WebKit says this is needed for "older systems"; not sure how old, but 10.11 AppKit doesn't look for this auxiliaryColor = tryColorNamed(@"CorrectionDot"); if (auxiliaryColor == nil) auxiliaryColor = [NSColor blueColor]; } [auxiliaryColor retain]; // override autoreleasing } void uiprivUninitUnderlineColors(void) { [auxiliaryColor release]; auxiliaryColor = nil; [grammarColor release]; grammarColor = nil; [spellingColor release]; spellingColor = nil; } // TODO opentype features are lost when using uiFontDescriptor, so a handful of fonts in the font panel ("Titling" variants of some fonts and possibly others but those are the examples I know about) cannot be represented by uiFontDescriptor; what *can* we do about this since this info is NOT part of the font on other platforms? // TODO see if we could use NSAttributedString? // TODO consider renaming this struct and the fep variable(s) // TODO restructure all this so the important details at the top are below with the combined font attributes type? // TODO in fact I should just write something to explain everything in this file... struct foreachParams { CFMutableAttributedStringRef mas; NSMutableArray *backgroundParams; }; // unlike the other systems, Core Text rolls family, size, weight, italic, width, AND opentype features into the "font" attribute // instead of incrementally adjusting CTFontRefs (which, judging from NSFontManager, seems finicky and UI-centric), we use a custom class to incrementally store attributes that go into a CTFontRef, and then convert everything to CTFonts en masse later // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/AttributedStrings/Tasks/ChangingAttrStrings.html#//apple_ref/doc/uid/20000162-BBCBGCDG says we must have -hash and -isEqual: workign properly for this to work, so we must do that too, using a basic xor-based hash and leveraging Cocoa -hash implementations where useful and feasible (if not necessary) // TODO structure and rewrite this part // TODO re-find sources proving support of custom attributes // TODO what if this is NULL? static const CFStringRef combinedFontAttrName = CFSTR("libuiCombinedFontAttribute"); enum { cFamily, cSize, cWeight, cItalic, cStretch, cFeatures, nc, }; static const int toc[] = { [uiAttributeTypeFamily] = cFamily, [uiAttributeTypeSize] = cSize, [uiAttributeTypeWeight] = cWeight, [uiAttributeTypeItalic] = cItalic, [uiAttributeTypeStretch] = cStretch, [uiAttributeTypeFeatures] = cFeatures, }; static uiForEach featuresHash(const uiOpenTypeFeatures *otf, char a, char b, char c, char d, uint32_t value, void *data) { NSUInteger *hash = (NSUInteger *) data; uint32_t tag; tag = (((uint32_t) a) & 0xFF) << 24; tag |= (((uint32_t) b) & 0xFF) << 16; tag |= (((uint32_t) c) & 0xFF) << 8; tag |= ((uint32_t) d) & 0xFF; *hash ^= tag; *hash ^= value; return uiForEachContinue; } @interface uiprivCombinedFontAttr : NSObject { uiAttribute *attrs[nc]; BOOL hasHash; NSUInteger hash; } - (void)addAttribute:(uiAttribute *)attr; - (CTFontRef)toCTFontWithDefaultFont:(uiFontDescriptor *)defaultFont; @end @implementation uiprivCombinedFontAttr - (id)init { self = [super init]; if (self) { memset(self->attrs, 0, nc * sizeof (uiAttribute *)); self->hasHash = NO; } return self; } - (void)dealloc { int i; for (i = 0; i < nc; i++) if (self->attrs[i] != NULL) { uiprivAttributeRelease(self->attrs[i]); self->attrs[i] = NULL; } [super dealloc]; } - (id)copyWithZone:(NSZone *)zone { uiprivCombinedFontAttr *ret; int i; ret = [[uiprivCombinedFontAttr allocWithZone:zone] init]; for (i = 0; i < nc; i++) if (self->attrs[i] != NULL) ret->attrs[i] = uiprivAttributeRetain(self->attrs[i]); ret->hasHash = self->hasHash; ret->hash = self->hash; return ret; } - (void)addAttribute:(uiAttribute *)attr { int index; index = toc[uiAttributeGetType(attr)]; if (self->attrs[index] != NULL) uiprivAttributeRelease(self->attrs[index]); self->attrs[index] = uiprivAttributeRetain(attr); self->hasHash = NO; } - (BOOL)isEqual:(id)bb { uiprivCombinedFontAttr *b = (uiprivCombinedFontAttr *) bb; int i; if (b == nil) return NO; for (i = 0; i < nc; i++) { if (self->attrs[i] == NULL && b->attrs[i] == NULL) continue; if (self->attrs[i] == NULL || b->attrs[i] == NULL) return NO; if (!uiprivAttributeEqual(self->attrs[i], b->attrs[i])) return NO; } return YES; } - (NSUInteger)hash { if (self->hasHash) return self->hash; @autoreleasepool { NSString *family; NSNumber *size; self->hash = 0; if (self->attrs[cFamily] != NULL) { family = [NSString stringWithUTF8String:uiAttributeFamily(self->attrs[cFamily])]; // TODO make sure this aligns with case-insensitive compares when those are done in common/attribute.c self->hash ^= [[family uppercaseString] hash]; } if (self->attrs[cSize] != NULL) { size = [NSNumber numberWithDouble:uiAttributeSize(self->attrs[cSize])]; self->hash ^= [size hash]; } if (self->attrs[cWeight] != NULL) self->hash ^= (NSUInteger) uiAttributeWeight(self->attrs[cWeight]); if (self->attrs[cItalic] != NULL) self->hash ^= (NSUInteger) uiAttributeItalic(self->attrs[cItalic]); if (self->attrs[cStretch] != NULL) self->hash ^= (NSUInteger) uiAttributeStretch(self->attrs[cStretch]); if (self->attrs[cFeatures] != NULL) uiOpenTypeFeaturesForEach(uiAttributeFeatures(self->attrs[cFeatures]), featuresHash, &(self->hash)); self->hasHash = YES; } return self->hash; } - (CTFontRef)toCTFontWithDefaultFont:(uiFontDescriptor *)defaultFont { uiFontDescriptor uidesc; CTFontDescriptorRef desc; CTFontRef font; uidesc = *defaultFont; if (self->attrs[cFamily] != NULL) // TODO const-correct uiFontDescriptor or change this function below uidesc.Family = (char *) uiAttributeFamily(self->attrs[cFamily]); if (self->attrs[cSize] != NULL) uidesc.Size = uiAttributeSize(self->attrs[cSize]); if (self->attrs[cWeight] != NULL) uidesc.Weight = uiAttributeWeight(self->attrs[cWeight]); if (self->attrs[cItalic] != NULL) uidesc.Italic = uiAttributeItalic(self->attrs[cItalic]); if (self->attrs[cStretch] != NULL) uidesc.Stretch = uiAttributeStretch(self->attrs[cStretch]); desc = uiprivFontDescriptorToCTFontDescriptor(&uidesc); if (self->attrs[cFeatures] != NULL) desc = uiprivCTFontDescriptorAppendFeatures(desc, uiAttributeFeatures(self->attrs[cFeatures])); font = CTFontCreateWithFontDescriptor(desc, uidesc.Size, NULL); CFRelease(desc); // TODO correct? return font; } @end static void addFontAttributeToRange(struct foreachParams *p, size_t start, size_t end, uiAttribute *attr) { uiprivCombinedFontAttr *cfa; CFRange range; size_t diff; while (start < end) { cfa = (uiprivCombinedFontAttr *) CFAttributedStringGetAttribute(p->mas, start, combinedFontAttrName, &range); if (cfa == nil) cfa = [uiprivCombinedFontAttr new]; else cfa = [cfa copy]; [cfa addAttribute:attr]; // clamp range within [start, end) if ((size_t)range.location < start) { diff = start - range.location; range.location = start; range.length -= diff; } if ((size_t)(range.location + range.length) > end) range.length = end - range.location; CFAttributedStringSetAttribute(p->mas, range, combinedFontAttrName, cfa); [cfa release]; start += range.length; } } static CGColorRef mkcolor(double r, double g, double b, double a) { CGColorSpaceRef colorspace; CGColorRef color; CGFloat components[4]; // TODO we should probably just create this once and recycle it throughout program execution... colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); if (colorspace == NULL) { // TODO } components[0] = r; components[1] = g; components[2] = b; components[3] = a; color = CGColorCreate(colorspace, components); CFRelease(colorspace); return color; } static void addBackgroundAttribute(struct foreachParams *p, size_t start, size_t end, double r, double g, double b, double a) { uiprivDrawTextBackgroundParams *dtb; // TODO make sure this works properly with line paragraph spacings (after figuring out what that means, of course) if (uiprivFUTURE_kCTBackgroundColorAttributeName != NULL) { CGColorRef color; CFRange range; color = mkcolor(r, g, b, a); range.location = start; range.length = end - start; CFAttributedStringSetAttribute(p->mas, range, *uiprivFUTURE_kCTBackgroundColorAttributeName, color); CFRelease(color); return; } dtb = [[uiprivDrawTextBackgroundParams alloc] initWithStart:start end:end r:r g:g b:b a:a]; [p->backgroundParams addObject:dtb]; [dtb release]; } static uiForEach processAttribute(const uiAttributedString *s, const uiAttribute *attr, size_t start, size_t end, void *data) { struct foreachParams *p = (struct foreachParams *) data; CFRange range; CGColorRef color; int32_t us; CFNumberRef num; double r, g, b, a; uiUnderlineColor colorType; start = uiprivAttributedStringUTF8ToUTF16(s, start); end = uiprivAttributedStringUTF8ToUTF16(s, end); range.location = start; range.length = end - start; switch (uiAttributeGetType(attr)) { case uiAttributeTypeFamily: case uiAttributeTypeSize: case uiAttributeTypeWeight: case uiAttributeTypeItalic: case uiAttributeTypeStretch: case uiAttributeTypeFeatures: addFontAttributeToRange(p, start, end, attr); break; case uiAttributeTypeColor: uiAttributeColor(attr, &r, &g, &b, &a); color = mkcolor(r, g, b, a); CFAttributedStringSetAttribute(p->mas, range, kCTForegroundColorAttributeName, color); CFRelease(color); break; case uiAttributeTypeBackground: uiAttributeColor(attr, &r, &g, &b, &a); addBackgroundAttribute(p, start, end, r, g, b, a); break; // TODO turn into a class, like we did with the font attributes, or even integrate *into* the font attributes case uiAttributeTypeUnderline: switch (uiAttributeUnderline(attr)) { case uiUnderlineNone: us = kCTUnderlineStyleNone; break; case uiUnderlineSingle: us = kCTUnderlineStyleSingle; break; case uiUnderlineDouble: us = kCTUnderlineStyleDouble; break; case uiUnderlineSuggestion: // TODO incorrect if a solid color us = kCTUnderlineStyleThick; break; } num = CFNumberCreate(NULL, kCFNumberSInt32Type, &us); CFAttributedStringSetAttribute(p->mas, range, kCTUnderlineStyleAttributeName, num); CFRelease(num); break; case uiAttributeTypeUnderlineColor: uiAttributeUnderlineColor(attr, &colorType, &r, &g, &b, &a); switch (colorType) { case uiUnderlineColorCustom: color = mkcolor(r, g, b, a); break; case uiUnderlineColorSpelling: color = [spellingColor CGColor]; break; case uiUnderlineColorGrammar: color = [grammarColor CGColor]; break; case uiUnderlineColorAuxiliary: color = [auxiliaryColor CGColor]; break; } CFAttributedStringSetAttribute(p->mas, range, kCTUnderlineColorAttributeName, color); if (colorType == uiUnderlineColorCustom) CFRelease(color); break; } return uiForEachContinue; } static void applyFontAttributes(CFMutableAttributedStringRef mas, uiFontDescriptor *defaultFont) { uiprivCombinedFontAttr *cfa; CTFontRef font; CFRange range; CFIndex n; n = CFAttributedStringGetLength(mas); // first apply the default font to the entire string // TODO is this necessary given the #if 0'd code in uiprivAttributedStringToCFAttributedString()? cfa = [uiprivCombinedFontAttr new]; font = [cfa toCTFontWithDefaultFont:defaultFont]; [cfa release]; range.location = 0; range.length = n; CFAttributedStringSetAttribute(mas, range, kCTFontAttributeName, font); CFRelease(font); // now go through, replacing every uiprivCombinedFontAttr with the proper CTFontRef // we are best off treating series of identical fonts as single ranges ourselves for parity across platforms, even if OS X does something similar itself range.location = 0; while (range.location < n) { // TODO consider seeing if CFAttributedStringGetAttributeAndLongestEffectiveRange() can make things faster by reducing the number of potential iterations, either here or above cfa = (uiprivCombinedFontAttr *) CFAttributedStringGetAttribute(mas, range.location, combinedFontAttrName, &range); if (cfa != nil) { font = [cfa toCTFontWithDefaultFont:defaultFont]; CFAttributedStringSetAttribute(mas, range, kCTFontAttributeName, font); CFRelease(font); } range.location += range.length; } // and finally, get rid of all the uiprivCombinedFontAttrs as we won't need them anymore range.location = 0; range.length = 0; CFAttributedStringRemoveAttribute(mas, range, combinedFontAttrName); } static const CTTextAlignment ctaligns[] = { [uiDrawTextAlignLeft] = kCTTextAlignmentLeft, [uiDrawTextAlignCenter] = kCTTextAlignmentCenter, [uiDrawTextAlignRight] = kCTTextAlignmentRight, }; static CTParagraphStyleRef mkParagraphStyle(uiDrawTextLayoutParams *p) { CTParagraphStyleRef ps; CTParagraphStyleSetting settings[16]; size_t nSettings = 0; settings[nSettings].spec = kCTParagraphStyleSpecifierAlignment; settings[nSettings].valueSize = sizeof (CTTextAlignment); settings[nSettings].value = ctaligns + p->Align; nSettings++; ps = CTParagraphStyleCreate(settings, nSettings); if (ps == NULL) { // TODO } return ps; } // TODO either rename this (on all platforms) to uiprivDrawTextLayoutParams... or rename this file or both or split the struct or something else... CFAttributedStringRef uiprivAttributedStringToCFAttributedString(uiDrawTextLayoutParams *p, NSArray **backgroundParams) { CFStringRef cfstr; CFMutableDictionaryRef defaultAttrs; CTParagraphStyleRef ps; CFAttributedStringRef base; CFMutableAttributedStringRef mas; struct foreachParams fep; cfstr = CFStringCreateWithCharacters(NULL, uiprivAttributedStringUTF16String(p->String), uiprivAttributedStringUTF16Len(p->String)); if (cfstr == NULL) { // TODO } defaultAttrs = CFDictionaryCreateMutable(NULL, 0, &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (defaultAttrs == NULL) { // TODO } #if 0 /* TODO */ ffp.desc = *(p->DefaultFont); defaultCTFont = fontdescToCTFont(&ffp); CFDictionaryAddValue(defaultAttrs, kCTFontAttributeName, defaultCTFont); CFRelease(defaultCTFont); #endif ps = mkParagraphStyle(p); CFDictionaryAddValue(defaultAttrs, kCTParagraphStyleAttributeName, ps); CFRelease(ps); base = CFAttributedStringCreate(NULL, cfstr, defaultAttrs); if (base == NULL) { // TODO } CFRelease(cfstr); CFRelease(defaultAttrs); mas = CFAttributedStringCreateMutableCopy(NULL, 0, base); CFRelease(base); CFAttributedStringBeginEditing(mas); fep.mas = mas; fep.backgroundParams = [NSMutableArray new]; uiAttributedStringForEachAttribute(p->String, processAttribute, &fep); applyFontAttributes(mas, p->DefaultFont); CFAttributedStringEndEditing(mas); *backgroundParams = fep.backgroundParams; return mas; }