diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 9204fb8ea6..b43b53f7c2 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -1158,6 +1158,22 @@ Apple documentation for `NSFontWeight` for possible values): > :set guifont=-monospace-Light:h11 :set guifont=-monospace-Medium:h11 < +MacVim also supports the ":f" option for OpenType font features, using the +same syntax as the Win32 GUI (see above): > + :set guifont=Maple\ Mono:h14:fss19=1:fcalt=0 +Each ":f" entry specifies a single feature as tag=value, where tag is a +4-character OpenType feature tag and value is the parameter (0 to disable, 1 +or higher to enable/select a variant). A bare tag is equivalent to "tag=1". +This requires the font to support the given features (e.g. character variants +"cv01"-"cv99" or stylistic sets "ss01"-"ss20"); unsupported features are +ignored. Default features (calt, liga, etc.) are preserved unless explicitly +overridden. Features in 'guifontwide' are ignored; the 'guifont' features +apply to all rendered text. +Note: This requires macOS 10.10 or later, and currently only has an effect if +'Use Core Text renderer' is enabled in the GUI preferences pane. Standard +ligatures are controlled by 'macligatures', which takes precedence over a +"liga" entry here. + Mono-spaced fonts *E236* Note that the fonts must be mono-spaced (all characters have the same width). diff --git a/src/MacVim/MMBackend.h b/src/MacVim/MMBackend.h index 0ed690bd25..21338ac925 100644 --- a/src/MacVim/MMBackend.h +++ b/src/MacVim/MMBackend.h @@ -134,6 +134,7 @@ - (void)setFullScreenBackgroundColor:(int)color; - (void)setAntialias:(BOOL)antialias; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)ligatures; - (void)setThinStrokes:(BOOL)thinStrokes; - (void)setBlurRadius:(int)radius; diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index d58eae8476..5872e947ad 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -1196,6 +1196,19 @@ - (void)setAntialias:(BOOL)antialias [self queueMessage:msgid data:nil]; } +- (void)setFontFeatures:(NSString *)features +{ + // NOTE: Unlike setFont:wide: an empty string must still be sent since it + // means "reset to the font's default features". + int len = (int)[features lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *data = [NSMutableData data]; + [data appendBytes:&len length:sizeof(int)]; + if (len > 0) + [data appendBytes:[features UTF8String] length:len]; + + [self queueMessage:SetFontFeaturesMsgID data:data]; +} + - (void)setLigatures:(BOOL)ligatures { int msgid = ligatures ? EnableLigaturesMsgID : DisableLigaturesMsgID; diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index 15ec0b9c64..84c5ceb945 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -66,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN BOOL antialias; BOOL ligatures; BOOL thinStrokes; + NSString *fontFeatures; ///< Raw OpenType font features string + NSArray *fontFeatureSettings; ///< Parsed kCTFontFeatureSettingsAttribute array, nil if none BOOL forceRefreshFont; // when true, don't early out of setFont calls. @@ -138,6 +140,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setPreEditRow:(int)row column:(int)col; - (void)setMouseShape:(int)shape; - (void)setAntialias:(BOOL)state; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)state; - (void)setThinStrokes:(BOOL)state; - (void)setImControl:(BOOL)enable; diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 7a44da201c..4b58500efb 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -136,6 +136,23 @@ - (void)invertBlockFromRow:(int)row column:(int)col numRows:(int)nrows return [@"m" sizeWithAttributes:a].width; } +/// Returns a font derived from the supplied one with the given OpenType +/// feature settings (a kCTFontFeatureSettingsAttribute-style array) applied. + static NSFont * +fontWithFeatureSettings(NSFont *base, NSArray *settings) +{ + if (settings == nil || base == nil) + return base; + CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes( + (CFDictionaryRef)@{ (NSString *)kCTFontFeatureSettingsAttribute: settings }); + CTFontRef derived = CTFontCreateCopyWithAttributes( + (CTFontRef)base, 0.0, NULL, desc); + CFRelease(desc); + if (derived == NULL) + return base; + return [(NSFont *)derived autorelease]; +} + typedef struct { unsigned color; int shape; @@ -284,6 +301,8 @@ - (void)dealloc [characterStrings release]; characterStrings = nil; [fontVariants release]; fontVariants = nil; [characterLines release]; characterLines = nil; + [fontFeatures release]; fontFeatures = nil; + [fontFeatureSettings release]; fontFeatureSettings = nil; [helper setTextView:nil]; [helper release]; helper = nil; @@ -582,6 +601,43 @@ - (void)setAntialias:(BOOL)state antialias = state; } +- (void)setFontFeatures:(NSString *)features +{ + if (fontFeatures == features || [fontFeatures isEqualToString:features]) + return; + [fontFeatures release]; + fontFeatures = [features copy]; + [fontFeatureSettings release]; + fontFeatureSettings = nil; + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10 + if (features.length > 0) { + if (@available(macos 10.10, *)) { + // Parse the feature list ("tag" or "tag=N" entries, comma + // separated) into the array of dictionaries CoreText expects for + // kCTFontFeatureSettingsAttribute. + NSMutableArray *settings = [NSMutableArray array]; + for (NSString *entry in [features componentsSeparatedByString:@","]) { + NSArray *kv = [entry componentsSeparatedByString:@"="]; + NSString *tag = kv[0]; + if (tag.length != 4) + continue; // already validated by Vim; be defensive + int value = kv.count > 1 ? [kv[1] intValue] : 1; + [settings addObject:@{ + (NSString *)kCTFontOpenTypeFeatureTag: tag, + (NSString *)kCTFontOpenTypeFeatureValue: @(value), + }]; + } + if (settings.count > 0) + fontFeatureSettings = [settings retain]; + } + } +#endif + + [fontVariants removeAllObjects]; + [characterLines removeAllObjects]; +} + - (void)setLigatures:(BOOL)state { ligatures = state; @@ -2116,6 +2172,7 @@ - (NSFont *)fontVariantForTextFlags:(int)textFlags { fontRef = [NSFontManager.sharedFontManager convertFont:fontRef toHaveTrait:NSFontItalicTrait]; if (textFlags & DRAW_BOLD) fontRef = [NSFontManager.sharedFontManager convertFont:fontRef toHaveTrait:NSFontBoldTrait]; + fontRef = fontWithFeatureSettings(fontRef, fontFeatureSettings); fontVariants[cacheFlags] = fontRef; } diff --git a/src/MacVim/MMTextView.h b/src/MacVim/MMTextView.h index b18ee08203..c76af45f26 100644 --- a/src/MacVim/MMTextView.h +++ b/src/MacVim/MMTextView.h @@ -39,6 +39,7 @@ - (void)performBatchDrawWithData:(NSData *)data; - (void)setMouseShape:(int)shape; - (void)setAntialias:(BOOL)antialias; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)ligatures; - (void)setThinStrokes:(BOOL)thinStrokes; - (void)setImControl:(BOOL)enable; diff --git a/src/MacVim/MMTextView.m b/src/MacVim/MMTextView.m index 6de0e56478..fc4318fc1b 100644 --- a/src/MacVim/MMTextView.m +++ b/src/MacVim/MMTextView.m @@ -307,6 +307,11 @@ - (void)setAntialias:(BOOL)state antialias = state; } +- (void)setFontFeatures:(NSString *)features +{ + // Not supported by this renderer; only MMCoreTextView handles this. +} + - (void)setLigatures:(BOOL)state { ligatures = state; diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index e48f55f411..09c6a0e94f 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -1185,6 +1185,17 @@ - (void)handleMessage:(int)msgid data:(NSData *)data [[[windowController vimView] textView] setAntialias:NO]; } break; + case SetFontFeaturesMsgID: + { + const void *bytes = [data bytes]; + int len = *((int*)bytes); bytes += sizeof(int); + NSString *features = len > 0 + ? [[[NSString alloc] initWithBytes:(void*)bytes length:len + encoding:NSUTF8StringEncoding] autorelease] + : @""; + [[[windowController vimView] textView] setFontFeatures:features]; + } + break; case EnableLigaturesMsgID: { [[[windowController vimView] textView] setLigatures:YES]; diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index 6291531677..90926cfa11 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -359,6 +359,7 @@ extern const char * const MMVimMsgIDStrings[]; MSG(EnableThinStrokesMsgID) \ MSG(DisableThinStrokesMsgID) \ MSG(ShowDefinitionMsgID) \ + MSG(SetFontFeaturesMsgID) \ MSG(LoopBackMsgID) /* Simple message that Vim will reflect back to MacVim */ \ MSG(LastMsgID) \ diff --git a/src/MacVim/gui_macvim.m b/src/MacVim/gui_macvim.m index de46c923c9..2bf6d7b51f 100644 --- a/src/MacVim/gui_macvim.m +++ b/src/MacVim/gui_macvim.m @@ -42,6 +42,7 @@ static BOOL MMShareFindPboard = YES; static GuiFont gui_macvim_font_with_name(char_u *name); +static NSString *fontFeaturesFromFontString(NSString *font); static int specialKeyToNSKey(int key); static int vimModMaskToEventModifierFlags(int mods); @@ -1150,6 +1151,12 @@ : font) wide:YES]; + // Apply any OpenType font feature settings given as ":f" modifiers in + // the font description. An empty list resets the renderer to the font's + // default features. + [[MMBackend sharedInstance] setFontFeatures: + fontFeaturesFromFontString((NSString *)font)]; + return OK; } @@ -1164,6 +1171,51 @@ } +/* + * Check a single OpenType font feature setting from a ":f" 'guifont' + * modifier, e.g. "ss19=1": a four-character printable ASCII tag, optionally + * followed by "=" and a non-negative number (a bare tag is equivalent to + * "tag=1"). + */ + static BOOL +isValidFontFeature(NSString *feature) +{ + if ([feature length] < 4) + return NO; + for (NSUInteger i = 0; i < 4; ++i) { + unichar ch = [feature characterAtIndex:i]; + if (ch < 0x20 || ch > 0x7e || ch == ',' || ch == '=') + return NO; + } + if ([feature length] == 4) + return YES; + if ([feature characterAtIndex:4] != '=' || [feature length] == 5) + return NO; + for (NSUInteger i = 5; i < [feature length]; ++i) { + unichar ch = [feature characterAtIndex:i]; + if (ch < '0' || ch > '9') + return NO; + } + return YES; +} + +/* + * Extract the ":f" feature modifiers from a font description string + * ("Name:h12:fss19=1:fcalt=0") as a comma-separated list ("ss19=1,calt=0"). + */ + static NSString * +fontFeaturesFromFontString(NSString *font) +{ + NSArray *components = [font componentsSeparatedByString:@":"]; + NSMutableArray *features = [NSMutableArray array]; + for (NSUInteger i = 1; i < [components count]; ++i) { + NSString *c = [components objectAtIndex:i]; + if ([c length] >= 2 && [c characterAtIndex:0] == 'f') + [features addObject:[c substringFromIndex:1]]; + } + return [features componentsJoinedByString:@","]; +} + /* * Return GuiFont in allocated memory. The caller must free it using * gui_mch_free_font(). @@ -1179,22 +1231,25 @@ int size = MMDefaultFontSize; BOOL parseFailed = NO; + // The font description is the font name followed by optional + // colon-separated modifiers: "h" and "f" (an OpenType font + // feature setting, using the same syntax as the Win32 GUI, see + // |gui-font|). Example: "Menlo:h13:fss19=1:fcalt=0". NSArray *components = [fontName componentsSeparatedByString:@":"]; - if ([components count] == 2) { - NSString *sizeString = [components lastObject]; - if ([sizeString length] > 0 - && [sizeString characterAtIndex:0] == 'h') { - sizeString = [sizeString substringFromIndex:1]; - if ([sizeString length] > 0) { - size = (int)round([sizeString floatValue]); - fontName = [components objectAtIndex:0]; - } + NSString *featureSuffix = @""; + for (NSUInteger i = 1; i < [components count] && !parseFailed; ++i) { + NSString *c = [components objectAtIndex:i]; + if ([c length] >= 2 && [c characterAtIndex:0] == 'h') { + size = (int)round([[c substringFromIndex:1] floatValue]); + } else if ([c length] >= 2 && [c characterAtIndex:0] == 'f' + && isValidFontFeature([c substringFromIndex:1])) { + featureSuffix = [featureSuffix stringByAppendingFormat:@":%@", c]; } else { parseFailed = YES; } - } else if ([components count] > 2) { - parseFailed = YES; } + if ([components count] > 1) + fontName = [components objectAtIndex:0]; const BOOL isSystemFont = [fontName hasPrefix:MMSystemFontAlias]; if (isSystemFont) { @@ -1221,7 +1276,8 @@ if ([fontName isEqualToString:MMDefaultFontName] || isSystemFont || [NSFont fontWithName:fontName size:size]) - return [[NSString alloc] initWithFormat:@"%@:h%d", fontName, size]; + return [[NSString alloc] initWithFormat:@"%@:h%d%@", + fontName, size, featureSuffix]; // If font loading failed, try to replace underscores with spaces for // user convenience. This really only works if the name is a family @@ -1229,7 +1285,8 @@ if ([fontName rangeOfString:@"_"].location != NSNotFound) { fontName = [fontName stringByReplacingOccurrencesOfString:@"_" withString:@" "]; if ([NSFont fontWithName:fontName size:size]) { - return [[NSString alloc] initWithFormat:@"%@:h%d", fontName, size]; + return [[NSString alloc] initWithFormat:@"%@:h%d%@", + fontName, size, featureSuffix]; } } } diff --git a/src/testdir/test_macvim.vim b/src/testdir/test_macvim.vim index 6feb090b16..1a954bf9db 100644 --- a/src/testdir/test_macvim.vim +++ b/src/testdir/test_macvim.vim @@ -76,3 +76,30 @@ func Test_macvim_invalid_options() call assert_fails("let &fuoptions='abcdef'", 'E474:') endfunc + +" Test OpenType font feature settings (":f" modifiers) in 'guifont' +func Test_macvim_guifont_features() + " 'guifont' is only parsed and validated when the GUI is running. + if !has('gui_running') + throw 'Skipped: GUI not running' + endif + + let save_guifont = &guifont + + set guifont=Menlo:h13:fss01=1:fcalt=0 + call assert_match(':fss01=1:fcalt=0', getfontname()) + set guifont=Menlo:h13:fzero + call assert_match(':fzero', getfontname()) + + " Invalid feature settings must be rejected as invalid fonts. + call assert_fails("set guifont=Menlo:h13:fabc", 'E596:') + call assert_fails("set guifont=Menlo:h13:fabcde", 'E596:') + call assert_fails("set guifont=Menlo:h13:fss01=x", 'E596:') + call assert_fails("set guifont=Menlo:h13:fss01=", 'E596:') + + " A font without features resets to the font's defaults. + set guifont=Menlo:h13 + call assert_equal('Menlo:h13', getfontname()) + + let &guifont = save_guifont +endfunc