libui/darwin/attrstr.m

506 lines
16 KiB
Objective-C

// 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<NSCopying> {
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 (range.location < start) {
diff = start - range.location;
range.location = start;
range.length -= diff;
}
if ((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;
}