patch 9.2.0321: MS-Windows: No OpenType font support
Commit:
https://github.com/vim/vim/commit/ea7bf9aa8a0e338ed9163c24ae43829885a97edb
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Tue Apr 7 21:07:46 2026 +0000
patch 9.2.0321: MS-Windows: No OpenType font support
Problem: MS-Windows: No OpenType font support
Solution: Allow specifying OpenType font features directly in 'guifont'
(Yasuhiro Matsumoto).
Allow specifying OpenType font features directly in 'guifont' using
the ':f' option (e.g., :set guifont=Cascadia_Code:h14:fss19=1:fcalt=0).
Each ':fXXXX=N' sets a single OpenType feature tag with a parameter
value. Multiple features can be specified by repeating the ':f' option.
This only takes effect when 'renderoptions' is set to use DirectWrite
(type:directx). Default features (calt, liga, clig, rlig, kern) are
preserved unless explicitly overridden.
closes: #19857
Signed-off-by: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt
index a255af48b..9965e3dd4 100644
--- a/runtime/doc/gui.txt
+++ b/runtime/doc/gui.txt
@@ -1,4 +1,4 @@
-*gui.txt* For Vim version 9.2. Last change: 2026 Feb 14
+*gui.txt* For Vim version 9.2. Last change: 2026 Apr 07
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -1150,11 +1150,22 @@ For the Win32 GUI *E244* *E245*
NONANTIALIASED, CLEARTYPE and DEFAULT. Normally you would use
"qDEFAULT".
Some quality values are not supported in legacy OSs.
+ fXX - OpenType font feature. Specify 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 variant). Multiple features
+ can be specified by repeating the ":f" option.
+ This only takes effect when 'renderoptions' is set to use
+ DirectWrite (type:directx). Default features (calt, liga,
+ etc.) are preserved unless explicitly overridden.
+ Example: ":fss19=1:fcalt=0" enables Stylistic Set 19
+ and disables Contextual Alternates.
- A '_' can be used in the place of a space, so you don't need to use
backslashes to escape the spaces.
Examples: >
:set guifont=courier_new:h12:w5:b:cRUSSIAN
:set guifont=Andale_Mono:h7.5:w4.5
+ :set guifont=Cascadia_Code:h14:fss19=1:fcalt=1:fliga=1
See also |font-sizes|.
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 8adff066f..acce37226 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -52614,12 +52614,16 @@ Other ~
- |system()| and |systemlist()| functions accept a list as first argument,
bypassing the shell completely.
+Platform specific ~
+-----------------
+- support OpenType font features in 'guifont' for DirectWrite (Win32)
+
xxd ~
---
Add "-t" option to append a terminating NUL byte to C include output (-i).
*changed-9.3*
-Changed~
+Changed ~
-------
- Support for NeXTStep was dropped with patch v9.2.0122
- |json_decode()| is stricter: keywords must be lowercase, lone surrogates are
diff --git a/src/gui_dwrite.cpp b/src/gui_dwrite.cpp
index c71358717..673b6d539 100644
--- a/src/gui_dwrite.cpp
+++ b/src/gui_dwrite.cpp
@@ -313,6 +313,9 @@ struct DWriteContext {
D2D1_TEXT_ANTIALIAS_MODE mTextAntialiasMode;
+ DWriteFontFeature mFontFeatures[DWRITE_MAX_FONT_FEATURES];
+ int mFontFeatureCount;
+
// METHODS
DWriteContext();
@@ -357,6 +360,8 @@ struct DWriteContext {
DWriteRenderingParams *GetRenderingParams(
DWriteRenderingParams *params);
+
+ void SetFontFeatures(const DWriteFontFeature *features, int count);
};
class AdjustedGlyphRun : public DWRITE_GLYPH_RUN
@@ -648,8 +653,10 @@ DWriteContext::DWriteContext() :
mFontStyle(DWRITE_FONT_STYLE_NORMAL),
mFontSize(0.0f),
mFontAscent(0.0f),
- mTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_DEFAULT)
+ mTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_DEFAULT),
+ mFontFeatureCount(0)
{
+ ZeroMemory(mFontFeatures, sizeof(mFontFeatures));
HRESULT hr;
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
@@ -1086,6 +1093,56 @@ DWriteContext::DrawText(const WCHAR *text, int len,
textLayout->SetFontWeight(mFontWeight, textRange);
textLayout->SetFontStyle(mFontStyle, textRange);
+ if (mFontFeatureCount > 0)
+ {
+ // Default OpenType features that DirectWrite normally enables.
+ // SetTypography() overrides all defaults, so we must
+ // re-add them here explicitly.
+ static const DWRITE_FONT_FEATURE defaultFeatures[] = {
+ { DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1 },
+ { DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1 },
+ { DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1 },
+ { DWRITE_FONT_FEATURE_TAG_REQUIRED_LIGATURES, 1 },
+ { DWRITE_FONT_FEATURE_TAG_KERNING, 1 },
+ };
+ static const int numDefaults = sizeof(defaultFeatures)
+ / sizeof(defaultFeatures[0]);
+
+ IDWriteTypography *typography = NULL;
+ hr = mDWriteFactory->CreateTypography(&typography);
+ if (SUCCEEDED(hr))
+ {
+ // Add default features, skipping any that the user
+ // has explicitly specified (either + or -).
+ for (int d = 0; d < numDefaults; ++d)
+ {
+ int overridden = 0;
+ for (int u = 0; u < mFontFeatureCount; ++u)
+ {
+ if ((DWRITE_FONT_FEATURE_TAG)mFontFeatures[u].tag
+ == defaultFeatures[d].nameTag)
+ {
+ overridden = 1;
+ break;
+ }
+ }
+ if (!overridden)
+ typography->AddFontFeature(defaultFeatures[d]);
+ }
+ // Add user-specified features.
+ for (int i = 0; i < mFontFeatureCount; ++i)
+ {
+ DWRITE_FONT_FEATURE ff = {
+ (DWRITE_FONT_FEATURE_TAG)mFontFeatures[i].tag,
+ mFontFeatures[i].parameter
+ };
+ typography->AddFontFeature(ff);
+ }
+ textLayout->SetTypography(typography, textRange);
+ SafeRelease(&typography);
+ }
+ }
+
// Calculate baseline using font ascent from font metrics.
// Do NOT use GetLineMetrics() because it returns different values
// depending on text content (e.g., when CJK characters trigger
@@ -1413,3 +1470,24 @@ DWriteContext_GetRenderingParams(
else
return NULL;
}
+
+ void
+DWriteContext::SetFontFeatures(
+ const DWriteFontFeature *features, int count)
+{
+ if (count > DWRITE_MAX_FONT_FEATURES)
+ count = DWRITE_MAX_FONT_FEATURES;
+ mFontFeatureCount = count;
+ if (count > 0 && features != NULL)
+ memcpy(mFontFeatures, features, sizeof(DWriteFontFeature) * count);
+}
+
+ void
+DWriteContext_SetFontFeatures(
+ DWriteContext *ctx,
+ const DWriteFontFeature *features,
+ int count)
+{
+ if (ctx != NULL)
+ ctx->SetFontFeatures(features, count);
+}
diff --git a/src/gui_dwrite.h b/src/gui_dwrite.h
index 5de06f92b..4c4777764 100644
--- a/src/gui_dwrite.h
+++ b/src/gui_dwrite.h
@@ -51,6 +51,13 @@ typedef struct DWriteRenderingParams {
int textAntialiasMode;
} DWriteRenderingParams;
+#define DWRITE_MAX_FONT_FEATURES 32
+
+typedef struct DWriteFontFeature {
+ unsigned int tag; // OpenType feature tag (4 bytes)
+ unsigned int parameter; // Feature parameter (0 = disable, 1 = enable)
+} DWriteFontFeature;
+
void DWrite_Init(void);
void DWrite_Final(void);
@@ -86,6 +93,11 @@ DWriteRenderingParams *DWriteContext_GetRenderingParams(
DWriteContext *ctx,
DWriteRenderingParams *params);
+void DWriteContext_SetFontFeatures(
+ DWriteContext *ctx,
+ const DWriteFontFeature *features,
+ int count);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/gui_w32.c b/src/gui_w32.c
index 605897fb2..093c84176 100644
--- a/src/gui_w32.c
+++ b/src/gui_w32.c
@@ -145,7 +145,6 @@ gui_mch_set_rendering_options(char_u *s)
int dx_geom = 0;
int dx_renmode = 0;
int dx_taamode = 0;
-
// parse string as rendering options.
for (p = s; p != NULL && *p != NUL; )
{
@@ -3956,6 +3955,60 @@ gui_mch_init_font(char_u *font_name, int fontset UNUSED)
if (font == NOFONT)
return FAIL;
+#if defined(FEAT_DIRECTX)
+ // Parse font features from guifont (e.g., ":fss19=1:fcalt=0:fliga=1").
+ {
+ DWriteFontFeature features[DWRITE_MAX_FONT_FEATURES];
+ int feat_count = 0;
+ char_u *fp;
+
+ if (font_name != NULL)
+ {
+ // Find each ":f" option in font_name.
+ for (fp = font_name; *fp != NUL; fp++)
+ {
+ if (*fp == ':' && *(fp + 1) == 'f')
+ {
+ char_u tag[5];
+ int ti = 0;
+ unsigned int param = 1;
+
+ fp += 2; // skip ":f"
+ while (*fp != NUL && *fp != '=' && *fp != ':'
+ && ti < 4)
+ tag[ti++] = *fp++;
+ tag[ti] = NUL;
+
+ if (ti != 4)
+ continue; // invalid tag length
+
+ if (*fp == '=')
+ {
+ fp++;
+ param = (unsigned int)atoi((char *)fp);
+ while (*fp >= '0' && *fp <= '9')
+ fp++;
+ }
+
+ if (feat_count < DWRITE_MAX_FONT_FEATURES)
+ {
+ features[feat_count].tag =
+ ((unsigned int)tag[0])
+ | ((unsigned int)tag[1] << 8)
+ | ((unsigned int)tag[2] << 16)
+ | ((unsigned int)tag[3] << 24);
+ features[feat_count].parameter = param;
+ feat_count++;
+ }
+
+ fp--; // adjust for loop increment
+ }
+ }
+ }
+ DWriteContext_SetFontFeatures(s_dwc, features, feat_count);
+ }
+#endif
+
if (font_name == NULL)
font_name = (char_u *)"";
#ifdef FEAT_MBYTE_IME
diff --git a/src/os_mswin.c b/src/os_mswin.c
index be71fac73..6eaf84f6c 100644
--- a/src/os_mswin.c
+++ b/src/os_mswin.c
@@ -3210,6 +3210,12 @@ get_logfont(
}
}
break;
+ case L'f':
+ // Font features (e.g., "fss19=1").
+ // Parsed separately by gui_mch_init_font(); skip here.
+ while (*p && *p != L':')
+ p++;
+ break;
case L'q':
for (i = 0; i < (int)ARRAY_LENGTH(quality_pairs); ++i)
{
diff --git a/src/version.c b/src/version.c
index 81519ea17..baacac399 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 321,
/**/
320,
/**/