/* vi:set ts=8 sts=4 sw=4 ft=objc: * * VIM - Vi IMproved by Bram Moolenaar * MacVim GUI port by Bjorn Winckler * * Do ":help uganda" in Vim to read copying and usage conditions. * Do ":help credits" in Vim to see a list of people who contributed. * See README.txt for an overview of the Vim source code. */ /** * Extracted from Snapshot-104 of MacVim * https://github.com/macvim-dev/macvim * See VIM.LICENSE */ #import "MMCoreTextView.h" static CTFontRef lookupFont(NSMutableArray *fontCache, const unichar *chars, UniCharCount count, CTFontRef currFontRef) { CGGlyph glyphs[count]; // See if font in cache can draw at least one character NSUInteger i; for (i = 0; i < [fontCache count]; ++i) { NSFont *font = [fontCache objectAtIndex:i]; if (CTFontGetGlyphsForCharacters((CTFontRef)font, chars, glyphs, count)) return (CTFontRef)[font retain]; } // Ask Core Text for a font (can be *very* slow, which is why we cache // fonts in the first place) CFRange r = { 0, count }; CFStringRef strRef = CFStringCreateWithCharacters(NULL, chars, count); CTFontRef newFontRef = CTFontCreateForString(currFontRef, strRef, r); CFRelease(strRef); // Verify the font can actually convert all the glyphs. if (!CTFontGetGlyphsForCharacters(newFontRef, chars, glyphs, count)) return nil; if (newFontRef) [fontCache addObject:(NSFont *)newFontRef]; return newFontRef; } static CFAttributedStringRef attributedStringForString(NSString *string, const CTFontRef font, BOOL useLigatures) { NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys: (id)font, kCTFontAttributeName, // 2 - full ligatures including rare // 1 - basic ligatures // 0 - no ligatures [NSNumber numberWithInteger:(useLigatures ? 1 : 0)], kCTLigatureAttributeName, nil ]; return CFAttributedStringCreate(NULL, (CFStringRef)string, (CFDictionaryRef)attrs); } static UniCharCount fetchGlyphsAndAdvances(const CTLineRef line, CGGlyph *glyphs, CGSize *advances, UniCharCount length) { NSArray *glyphRuns = (NSArray*)CTLineGetGlyphRuns(line); // get a hold on the actual character widths and glyphs in line UniCharCount offset = 0; for (id item in glyphRuns) { CTRunRef run = (CTRunRef)item; CFIndex count = CTRunGetGlyphCount(run); if (count > 0 && count - offset > length) count = length - offset; CFRange range = CFRangeMake(0, count); if (glyphs != NULL) CTRunGetGlyphs(run, range, &glyphs[offset]); if (advances != NULL) CTRunGetAdvances(run, range, &advances[offset]); offset += count; if (offset >= length) break; } return offset; } static UniCharCount gatherGlyphs(CGGlyph glyphs[], UniCharCount count) { // Gather scattered glyphs that was happended by Surrogate pair chars UniCharCount glyphCount = 0; NSUInteger pos = 0; NSUInteger i; for (i = 0; i < count; ++i) { if (glyphs[i] != 0) { ++glyphCount; glyphs[pos++] = glyphs[i]; } } return glyphCount; } static UniCharCount ligatureGlyphsForChars(const unichar *chars, CGGlyph *glyphs, CGPoint *positions, UniCharCount length, CTFontRef font) { // CoreText has no simple wait of retrieving a ligature for a set of // UniChars. The way proposed on the CoreText ML is to convert the text to // an attributed string, create a CTLine from it and retrieve the Glyphs // from the CTRuns in it. CGGlyph refGlyphs[length]; CGPoint refPositions[length]; memcpy(refGlyphs, glyphs, sizeof(CGGlyph) * length); memcpy(refPositions, positions, sizeof(CGSize) * length); memset(glyphs, 0, sizeof(CGGlyph) * length); NSString *plainText = [NSString stringWithCharacters:chars length:length]; CFAttributedStringRef ligatureText = attributedStringForString(plainText, font, YES); CTLineRef ligature = CTLineCreateWithAttributedString(ligatureText); CGSize ligatureRanges[length], regularRanges[length]; // get the (ligature)glyphs and advances for the new text UniCharCount offset = fetchGlyphsAndAdvances(ligature, glyphs, ligatureRanges, length); // fetch the advances for the base text CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, refGlyphs, regularRanges, length); CFRelease(ligatureText); CFRelease(ligature); // tricky part: compare both advance ranges and chomp positions which are // covered by a single ligature while keeping glyphs not in the ligature // font. #define fequal(a, b) (fabs((a) - (b)) < FLT_EPSILON) #define fless(a, b)((a) - (b) < FLT_EPSILON) && (fabs((a) - (b)) > FLT_EPSILON) CFIndex skip = 0; CFIndex i; for (i = 0; i < offset && skip + i < length; ++i) { memcpy(&positions[i], &refPositions[skip + i], sizeof(CGSize)); if (fequal(ligatureRanges[i].width, regularRanges[skip + i].width)) { // [mostly] same width continue; } else if (fless(ligatureRanges[i].width, regularRanges[skip + i].width)) { // original is wider than our result - use the original glyph // FIXME: this is currently the only way to detect emoji (except // for 'glyph[i] == 5') glyphs[i] = refGlyphs[skip + i]; continue; } // no, that's a ligature // count how many positions this glyph would take up in the base text CFIndex j = 0; float width = ceil(regularRanges[skip + i].width); while ((int)width < (int)ligatureRanges[i].width && skip + i + j < length) { width += ceil(regularRanges[++j + skip + i].width); } skip += j; } #undef fless #undef fequal // as ligatures combine characters it is required to adjust the // original length value return offset; } void recurseDraw(const unichar *chars, CGGlyph *glyphs, CGPoint *positions, UniCharCount length, CGContextRef context, CTFontRef fontRef, NSMutableArray *fontCache, BOOL useLigatures) { if (CTFontGetGlyphsForCharacters(fontRef, chars, glyphs, length)) { // All chars were mapped to glyphs, so draw all at once and return. if (useLigatures) { length = ligatureGlyphsForChars(chars, glyphs, positions, length, fontRef); } else { // only fixup surrogate pairs if we're not using ligatures length = gatherGlyphs(glyphs, length); } CTFontDrawGlyphs(fontRef, glyphs, positions, length, context); return; } CGGlyph *glyphsEnd = glyphs+length, *g = glyphs; CGPoint *p = positions; const unichar *c = chars; while (glyphs < glyphsEnd) { if (*g) { // Draw as many consecutive glyphs as possible in the current font // (if a glyph is 0 that means it does not exist in the current // font). BOOL surrogatePair = NO; while (*g && g < glyphsEnd) { if (CFStringIsSurrogateHighCharacter(*c)) { surrogatePair = YES; g += 2; c += 2; } else { ++g; ++c; } ++p; } int count = g-glyphs; if (surrogatePair) count = gatherGlyphs(glyphs, count); CTFontDrawGlyphs(fontRef, glyphs, positions, count, context); } else { // Skip past as many consecutive chars as possible which cannot be // drawn in the current font. while (0 == *g && g < glyphsEnd) { if (CFStringIsSurrogateHighCharacter(*c)) { g += 2; c += 2; } else { ++g; ++c; } ++p; } // Try to find a fallback font that can render the entire // invalid range. If that fails, repeatedly halve the attempted // range until a font is found. UniCharCount count = c - chars; UniCharCount attemptedCount = count; CTFontRef fallback = nil; while (fallback == nil && attemptedCount > 0) { fallback = lookupFont(fontCache, chars, attemptedCount, fontRef); if (!fallback) attemptedCount /= 2; } if (!fallback) return; recurseDraw(chars, glyphs, positions, attemptedCount, context, fallback, fontCache, useLigatures); // If only a portion of the invalid range was rendered above, // the remaining range needs to be attempted by subsequent // iterations of the draw loop. c -= count - attemptedCount; g -= count - attemptedCount; p -= count - attemptedCount; CFRelease(fallback); } if (glyphs == g) { // No valid chars in the glyphs. Exit from the possible infinite // recursive call. break; } chars = c; glyphs = g; positions = p; } }