diff --git a/kitty/core_text.m b/kitty/core_text.m index 14a0d6ce6..d5598b42e 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -953,6 +953,20 @@ static CTFontRef nerd_font(CGFloat sz) { return variation_to_python(src); } +static PyObject* +applied_features(CTFace *self, PyObject *a UNUSED) { + RAII_PyObject(ans, PyTuple_New(self->font_features.count)); + if (!ans) return NULL; + char buf[256]; + for (size_t i = 0; i < self->font_features.count; i++) { + hb_feature_to_string(&self->font_features.features[i], buf, arraysz(buf)); + PyObject *t = PyUnicode_FromString(buf); + if (!t) return NULL; + PyTuple_SET_ITEM(ans, i, t); + } + Py_INCREF(ans); return ans; +} + static PyObject* get_features(CTFace *self, PyObject *a UNUSED) { if (!ensure_name_table(self)) return NULL; @@ -1012,6 +1026,7 @@ static CTFontRef nerd_font(CGFloat sz) { METHODB(display_name, METH_NOARGS), METHODB(postscript_name, METH_NOARGS), METHODB(get_variable_data, METH_NOARGS), + METHODB(applied_features, METH_NOARGS), METHODB(get_features, METH_NOARGS), METHODB(get_variation, METH_NOARGS), METHODB(identify_for_debug, METH_NOARGS), diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 3605b3040..2b990e847 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -442,6 +442,7 @@ class Face: def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> bytes: ... def get_variation(self) -> Optional[Dict[str, float]]: ... def get_features(self) -> Dict[str, Optional[FeatureData]]: ... + def applied_features(self) -> Tuple[str, ...]: ... class CoreTextFont(TypedDict): @@ -478,6 +479,7 @@ class CTFace: def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> bytes: ... def get_variation(self) -> Optional[Dict[str, float]]: ... def get_features(self) -> Dict[str, Optional[FeatureData]]: ... + def applied_features(self) -> Tuple[str, ...]: ... def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]: diff --git a/kitty/fonts.c b/kitty/fonts.c index 7719c8f5a..54f0a7f12 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -290,6 +290,17 @@ desc_to_face(PyObject *desc, FONTS_DATA_HANDLE fg) { return ans; } +static void +add_feature(FontFeatures *output, const hb_feature_t *feature) { + for (size_t i = 0; i < output->count; i++) { + if (output->features[i].tag == feature->tag) { + output->features[i] = *feature; + return; + } + } + output->features[output->count++] = *feature; +} + bool create_features_for_face(const char *psname, PyObject *features, FontFeatures *output) { size_t count_from_descriptor = features ? PyTuple_GET_SIZE(features): 0; @@ -304,16 +315,16 @@ create_features_for_face(const char *psname, PyObject *features, FontFeatures *o output->features = calloc(MAX(2u, count_from_opts + count_from_descriptor), sizeof(output->features[0])); if (!output->features) { PyErr_NoMemory(); return false; } for (size_t i = 0; i < count_from_opts; i++) { - output->features[output->count++] = from_opts->features[i]; + add_feature(output, &from_opts->features[i]); } for (size_t i = 0; i < count_from_descriptor; i++) { ParsedFontFeature *f = (ParsedFontFeature*)PyTuple_GET_ITEM(features, i); - output->features[output->count++] = f->feature; + add_feature(output, &f->feature); } if (!output->count) { if (strstr(psname, "NimbusMonoPS-") == psname) { - output->features[output->count++] = hb_features[LIGA_FEATURE]; - output->features[output->count++] = hb_features[DLIG_FEATURE]; + add_feature(output, &hb_features[LIGA_FEATURE]); + add_feature(output, &hb_features[DLIG_FEATURE]); } } return true; diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index e59cefe12..c63e22d77 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -315,8 +315,11 @@ def get_font_from_spec( resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event() ) -> Descriptor: if not spec.is_system: - return get_fine_grained_font(spec, bold, italic, resolved_medium_font=resolved_medium_font, family_axis_values=family_axis_values, + ans = get_fine_grained_font(spec, bold, italic, resolved_medium_font=resolved_medium_font, family_axis_values=family_axis_values, match_is_more_specific_than_family=match_is_more_specific_than_family) + if spec.features: + ans['features'] = spec.features + return ans family = spec.system or '' if family == 'auto': if bold or italic: diff --git a/kitty/freetype.c b/kitty/freetype.c index 216a9d4b9..20873a575 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -847,6 +847,20 @@ get_variation(Face *self, PyObject *a UNUSED) { Py_INCREF(ans); return ans; } +static PyObject* +applied_features(Face *self, PyObject *a UNUSED) { + RAII_PyObject(ans, PyTuple_New(self->font_features.count)); + if (!ans) return NULL; + char buf[256]; + for (size_t i = 0; i < self->font_features.count; i++) { + hb_feature_to_string(&self->font_features.features[i], buf, arraysz(buf)); + PyObject *t = PyUnicode_FromString(buf); + if (!t) return NULL; + PyTuple_SET_ITEM(ans, i, t); + } + Py_INCREF(ans); return ans; +} + static PyObject* get_features(Face *self, PyObject *a UNUSED) { FT_Error err; @@ -1023,6 +1037,7 @@ static PyMethodDef methods[] = { METHODB(identify_for_debug, METH_NOARGS), METHODB(extra_data, METH_NOARGS), METHODB(get_variable_data, METH_NOARGS), + METHODB(applied_features, METH_NOARGS), METHODB(get_features, METH_NOARGS), METHODB(get_variation, METH_NOARGS), METHODB(get_best_name, METH_O), diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 835d1f2da..36ecf6443 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -10,6 +10,7 @@ from kitty.constants import is_macos, read_kitty_resource from kitty.fast_data_types import ( DECAWM, + ParsedFontFeature, get_fallback_font, sprite_map_set_layout, sprite_map_set_limits, @@ -33,6 +34,7 @@ def parse_font_spec(spec): class Selection(BaseTest): def test_font_selection(self): + self.set_options({'font_features': {'LiberationMono': (ParsedFontFeature('-dlig'),)}}) opts = Options() fonts_map = all_fonts_map(True) names = set(fonts_map['family_map']) | set(fonts_map['variable_map']) @@ -140,6 +142,17 @@ def t(x, **kw): t('bold', spec='variable_name=CascadiaCodeRoman wght=603') t('bi', spec='variable_name= wght=603') + # Test font features + if has('liberation mono'): + opts = Options() + opts.font_family = parse_font_spec('family="liberation mono"') + ff = get_font_files(opts) + self.ae(face_from_descriptor(ff['medium']).applied_features(), ('-dlig',)) + self.ae(face_from_descriptor(ff['bold']).applied_features(), ()) + opts.font_family = parse_font_spec('family="liberation mono" features="dlig test"') + ff = get_font_files(opts) + self.ae(face_from_descriptor(ff['medium']).applied_features(), ('dlig', 'test')) + class Rendering(BaseTest):