// 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]; grammarColors = nil; [spellingColor release]; spellingColor = nil; } // unlike the other systems, Core Text rolls family, size, weight, italic, width, AND opentype features into the "font" attribute // TODO opentype features are lost, 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 uiDrawFontDescriptor; 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; NSMutableDictionary *combinedFontAttrs; // keys are CFIndex in mas, values are uiprivCombinedFontAttr objects uiDrawFontDescriptor *defaultFont; NSMutableArray *backgroundBlocks; }; @interface uiprivCombinedFontAttr : NSObject @property const char *family; @property double size; @property uiDrawTextWeight weight; @property uiDrawTextItalic italic; @property uiDrawTextStretch stretch; @property const uiOpenTypeFeatures *features; - (id)initWithDefaultFont:(uiDrawFontDescriptor *)defaultFont; - (BOOL)same:(uiprivCombinedFontAttr *)b; - (CTFontRef)toCTFont; @end @implementation uiprivCombinedFontAttr - (id)initWithDefaultFont:(uiDrawFontDescriptor *)defaultFont { self = [super init]; if (self) { // TODO define behaviors if defaultFont->Family or any attribute Family is NULL, same with other invalid values self.family = defaultFont->Family; self.size = defaultFont->Size; self.weight = defaultFont->Weight; self.italic = defaultFont->Italic; self.stretch = defaultFont->Stretch; self.features = NULL; } return self; } // TODO deduplicate this with common/attrlist.c - (BOOL)same:(uiprivCombinedFontAttr *)b { // TODO should this be case-insensitive? if (strcmp(self.family, b.family) != 0) return NO; // TODO use a closest match? if (self.size != b.size) return NO; if (self.weight != b.weight) return NO; if (self.italic != b.italic) return NO; if (self.stretch != b.stretch) return NO; // this also handles NULL cases if (!uiOpenTypeFeaturesEqual(self.features, b.features)) return NO; return YES; } - (CTFontRef)toCTFont { uiDrawFontDescriptor uidesc; CTFontDescriptorRef desc; CTFontRef font; // TODO const-correct uiDrawFontDescriptor or change this function below uidesc.Family = (char *) (self.family); uidesc.Size = self.size; uidesc.Weight = self.weight; uidesc.Italic = self.italic; uidesc.Stretch = self.stretch; desc = fontdescToCTFontDescriptor(&uidesc); if (self.features != NULL) desc = fontdescAppendFeatures(desc, self.features); font = CTFontCreateWithFontDescriptor(desc, self.size, NULL); CFRelease(desc); // TODO correct? return font; } @end // TODO merge this with adjustFontInRange() (and TODO figure out why they were separate in the first place; probably Windows?) static void ensureFontInRange(struct foreachParams *p, size_t start, size_t end) { size_t i; NSNumber *n; uiprivCombinedFontAttr *new; for (i = start; i < end; i++) { n = [NSNumber numberWithInteger:i]; if ([p->combinedFontAttrs objectForKey:n] != nil) continue; new = [[uiprivCombinedFontAttr alloc] initWithDefaultFont:p->defaultFont]; [p->combinedFontAttrs setObject:new forKey:n]; } } static void adjustFontInRange(struct foreachParams *p, size_t start, size_t end, void (^adj)(uiprivCombinedFontAttr *cfa)) { size_t i; NSNumber *n; uiprivCombinedFontAttr *cfa; for (i = start; i < end; i++) { n = [NSNumber numberWithInteger:i]; cfa = (uiprivCombinedFontAttr *) [p->combinedFontAttrs objectForKey:n]; adj(cfa); } } static backgroundBlock mkBackgroundBlock(size_t start, size_t end, double r, double g, double b, double a) { return Block_copy(^(uiDrawContext *c, uiDrawTextLayout *layout, double x, double y) { uiDrawBrush brush; brush.Type = uiDrawBrushTypeSolid; brush.R = r; brush.G = g; brush.B = b; brush.A = a; drawTextBackground(c, x, y, layout, start, end, &brush, 0); }); } static CGColorRef mkcolor(uiAttributeSpec *spec) { CGColorSpaceRef colorspace; CGColorRef color; CGFloat components[4]; colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); if (colorspace == NULL) { // TODO } components[0] = spec->R; components[1] = spec->G; components[2] = spec->B; components[3] = spec->A; color = CGColorCreate(colorspace, components); CFRelease(colorspace); return color; } static uiForEach processAttribute(const uiAttributedString *s, const uiAttributeSpec *spec, size_t start, size_t end, void *data) { struct foreachParams *p = (struct foreachParams *) data; CFRange range; CGColorRef color; size_t ostart, oend; backgroundBlock block; int32_t us; CFNumberRef num; ostart = start; oend = end; start = attrstrUTF8ToUTF16(s, start); end = attrstrUTF8ToUTF16(s, end); range.location = start; range.length = end - start; switch (spec->Type) { case uiAttributeFamily: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.family = spec->Family; }); break; case uiAttributeSize: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.size = spec->Double; }); break; case uiAttributeWeight: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.weight = (uiDrawTextWeight) (spec->Value); }); break; case uiAttributeItalic: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.italic = (uiDrawTextItalic) (spec->Value); }); break; case uiAttributeStretch: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.stretch = (uiDrawTextStretch) (spec->Value); }); break; case uiAttributeColor: color = mkcolor(spec); CFAttributedStringSetAttribute(p->mas, range, kCTForegroundColorAttributeName, color); CFRelease(color); break; case uiAttributeBackground: block = mkBackgroundBlock(ostart, oend, spec->R, spec->G, spec->B, spec->A); [p->backgroundBlocks addObject:block]; Block_release(block); break; case uiAttributeUnderline: switch (spec->Value) { case uiDrawUnderlineStyleNone: us = kCTUnderlineStyleNone; break; case uiDrawUnderlineStyleSingle: us = kCTUnderlineStyleSingle; break; case uiDrawUnderlineStyleDouble: us = kCTUnderlineStyleDouble; break; case uiDrawUnderlineStyleSuggestion: // TODO incorrect if a solid color us = kCTUnderlineStyleThick; break; } num = CFNumberCreate(NULL, kCFNumberSInt32Type, &us); CFAttributedStringSetAttribute(p->mas, range, kCTUnderlineStyleAttributeName, num); CFRelease(num); break; case uiAttributeUnderlineColor: switch (spec->Value) { case uiDrawUnderlineColorCustom: color = mkcolor(spec); break; case uiDrawUnderlineColorSpelling: color = [spellingColor CGColor]; break; case uiDrawUnderlineColorGrammar: color = [grammarColor CGColor]; break; case uiDrawUnderlineColorAuxiliary: color = [auxiliaryColor CGColor]; break; } CFAttributedStringSetAttribute(p->mas, range, kCTUnderlineColorAttributeName, color); if (spec->Value == uiDrawUnderlineColorCustom) CFRelease(color); break; case uiAttributeFeatures: ensureFontInRange(p, start, end); adjustFontInRange(p, start, end, ^(uiprivCombinedFontAttr *cfa) { cfa.features = spec->Features; }); break; default: // TODO complain ; } return uiForEachContinue; } static BOOL cfaIsEqual(uiprivCombinedFontAttr *a, uiprivCombinedFontAttr *b) { if (a == nil && b == nil) return YES; if (a == nil || b == nil) return NO; return [a same:b]; } static void applyAndFreeFontAttributes(struct foreachParams *p) { CFIndex i, n; uiprivCombinedFontAttr *cfa, *cfab; CTFontRef defaultFont; CTFontRef font; CFRange range; // first get the default font as a CTFontRef cfa = [[uiprivCombinedFontAttr alloc] initWithDefaultFont:p->defaultFont]; defaultFont = [cfa toCTFont]; [cfa release]; // now go through, fililng in the font attribute for successive ranges of identical combinedFontAttrs // 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 // this also avoids breaking apart surrogate pairs (though IIRC OS X doing the something similar itself might make this a non-issue here) cfa = nil; n = CFAttributedStringGetLength(p->mas); range.location = 0; for (i = 0; i < n; i++) { NSNumber *nn; // TODO use NSValue or some other method of NSNumber nn = [NSNumber numberWithInteger:i]; cfab = (uiprivCombinedFontAttr *) [p->combinedFontAttrs objectForKey:nn]; if (cfaIsEqual(cfa, cfab)) continue; // the font has changed; write out the old one range.length = i - range.location; if (cfa == nil) { // TODO this isn't necessary because of default font shenanigans below font = defaultFont; CFRetain(font); } else font = [cfa toCTFont]; CFAttributedStringSetAttribute(p->mas, range, kCTFontAttributeName, font); CFRelease(font); // and start this run cfa = cfab; range.location = i; } // and finally, write out the last range range.length = i - range.location; if (cfa == nil) { // TODO likewise font = defaultFont; CFRetain(font); } else // note that this handles the difference between NULL and empty uiOpenTypeFeatures properly as far as conversion to native data formats is concerned (NULL does not generate a features dictionary; empty just produces an empty one) // TODO but what about from the OS's perspective on all OSs? font = [cfa toCTFont]; CFAttributedStringSetAttribute(p->mas, range, kCTFontAttributeName, font); CFRelease(font); CFRelease(defaultFont); } 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; } CFAttributedStringRef uiprivAttributedStringToCFAttributedString(uiDrawTextLayoutParams *p, NSArray **backgroundBlocks) { CFStringRef cfstr; CFMutableDictionaryRef defaultAttrs; CTFontRef defaultCTFont; CTParagraphStyleRef ps; CFAttributedStringRef base; CFMutableAttributedStringRef mas; struct foreachParams fep; cfstr = CFStringCreateWithCharacters(NULL, attrstrUTF16(p->String), attrstrUTF16Len(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.combinedFontAttrs = [NSMutableDictionary new]; fep.defaultFont = p->DefaultFont; fep.backgroundBlocks = [NSMutableArray new]; uiAttributedStringForEachAttribute(p->String, processAttribute, &fep); applyAndFreeFontAttributes(&fep); [fep.combinedFontAttrs release]; CFAttributedStringEndEditing(mas); *backgroundBlocks = fep.backgroundBlocks; return mas; }