From 46f391ae63c4a8b379eeda04182a0b2da2550c6d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 May 2024 12:58:35 +0530 Subject: [PATCH] CoreText: Fix selection of font file with multi-file variant font Also prefer semibold and use similar scorer as fontconfig --- kitty/fonts/__init__.py | 3 +-- kitty/fonts/common.py | 4 ++++ kitty/fonts/core_text.py | 34 +++++++++++++++++++++++++--------- kitty/fonts/fontconfig.py | 8 ++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index c5a9b8eb8..fc81704b5 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -156,10 +156,9 @@ class FontSpec(NamedTuple): class Score(NamedTuple): variable_score: int - style_score: int + style_score: Union[int, float] monospace_score: int width_score: int - weight_distance_from_medium: float = 0 Descriptor = Union[FontConfigPattern, CoreTextFont] diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index 0f59c005e..15ec17c94 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: def is_variable(descriptor: Descriptor) -> bool: ... def set_named_style(name: str, font: Descriptor, vd: VariableData) -> bool: ... def set_axis_values(tag_map: Dict[str, float], font: Descriptor, vd: VariableData) -> bool: ... + def dump_sorted_candidates(bold: bool, italic: bool, candidates: List[Descriptor], scorer: Scorer) -> None: ... else: FontCollectionMapType = FontMap = None if is_macos: @@ -33,6 +34,7 @@ else: from kitty.fonts.core_text import ( all_fonts_map, create_scorer, + dump_sorted_candidates, find_best_match, find_last_resort_text_font, is_monospace, @@ -45,6 +47,7 @@ else: from kitty.fonts.fontconfig import ( all_fonts_map, create_scorer, + dump_sorted_candidates, find_best_match, find_last_resort_text_font, is_monospace, @@ -55,6 +58,7 @@ else: def face_from_descriptor(descriptor: Descriptor) -> Face: return Face(descriptor=descriptor) +dump_sorted_candidates cache_for_variable_data_by_path: Dict[str, VariableData] = {} attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'} diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 8bb57c922..8940a9ff2 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -79,15 +79,22 @@ def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = T def score(candidate: Descriptor) -> Score: assert candidate['descriptor_type'] == 'core_text' variable_score = 0 if prefer_variable and candidate['variation'] is not None else 1 - style_match = 1 if candidate['bold'] == bold and candidate[ - 'italic' - ] == italic else 0 - monospace_match = 1 if candidate['monospace'] else 0 + bold_score = candidate['weight'] # -1 to 1 with 0 being normal + if bold_score < 0: # thinner than normal, reject + bold_score = 2.0 + else: + if bold: + # prefer semibold=0.3 to full bold = 0.4 + bold_score = abs(bold_score - 0.3) + italic_score = candidate['slant'] # -1 to 1 with 0 being upright < 0 being backward slant, abs(slant) == 1 implies 30 deg rotation + if italic: + if italic_score < 0: + italic_score = 2.0 + else: + italic_score = abs(1 - italic_score) + monospace_match = 0 if candidate['monospace'] else 1 is_regular_width = not candidate['expanded'] and not candidate['condensed'] - # prefer semi-bold to bold to heavy, less bold means less chance of - # overflow - weight_distance_from_medium = abs(candidate['weight']) - return Score(variable_score, 1 - style_match, 1 - monospace_match, 1 - is_regular_width, weight_distance_from_medium) + return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1) return score @@ -99,6 +106,14 @@ def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospa return sorted(candidates, key=scorer)[0] +def dump_sorted_candidates(bold: bool, italic: bool, candidates: List[CoreTextFont], scorer: Scorer) -> None: + print(f'{bold=} {italic=}') + for x in candidates: + print(x['postscript_name'], f'bold={x["bold"]}', f'italic={x["italic"]}', f'weight={x["weight"]:.2f}', f'slant={x["slant"]:.2f}') + print(scorer(x)) + print() + + def find_best_match( family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Optional[CoreTextFont] = None, prefer_variable: bool = False @@ -121,7 +136,8 @@ def find_best_match( log_error(f'The font {family} was not found, falling back to Menlo') q = 'menlo' candidates = font_map['family_map'][q] - return sorted(candidates, key=scorer)[0] + candidates.sort(key=scorer) + return candidates[0] def get_font_from_spec( diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 14356f40b..95dc9e099 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -97,6 +97,14 @@ def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = T return score +def dump_sorted_candidates(bold: bool, italic: bool, candidates: List[FontConfigPattern], scorer: Scorer) -> None: + print(f'{bold=} {italic=}') + for x in candidates: + print(x['postscript_name'], f'weight={x["weight"]}', f'slant={x["slant"]}') + print(scorer(x)) + print() + + def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern: # Use fc-match with a generic family family = 'monospace' if monospaced else 'sans-serif'