Handle variable fonts like cascadia code that dont have a postfix variation prefix name for some of their faces

This commit is contained in:
Kovid Goyal
2024-05-18 15:12:56 +05:30
parent 815df1e210
commit b017cc0c1e
7 changed files with 97 additions and 65 deletions

View File

@@ -20,7 +20,7 @@ from kitty.fonts.common import (
get_variable_data_for_descriptor, get_variable_data_for_descriptor,
get_variable_data_for_face, get_variable_data_for_face,
is_variable, is_variable,
spec_for_descriptor, spec_for_face,
) )
from kitty.fonts.list import create_family_groups from kitty.fonts.list import create_family_groups
from kitty.fonts.render import display_bitmap from kitty.fonts.render import display_bitmap
@@ -134,6 +134,11 @@ def render_family_sample(
ResolvedFace = Dict[Literal['family', 'spec'], str] ResolvedFace = Dict[Literal['family', 'spec'], str]
def spec_for_descriptor(d: Descriptor) -> str:
face = face_from_descriptor(d)
return spec_for_face(d['family'], face).as_setting
def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]: def resolved_faces(opts: Options) -> Dict[OptNames, ResolvedFace]:
font_files = get_font_files(opts) font_files = get_font_files(opts)
ans: Dict[OptNames, ResolvedFace] = {} ans: Dict[OptNames, ResolvedFace] = {}

View File

@@ -11,6 +11,7 @@ import (
"kitty/tools/tui" "kitty/tools/tui"
"kitty/tools/tui/loop" "kitty/tools/tui/loop"
"kitty/tools/utils" "kitty/tools/utils"
"kitty/tools/utils/shlex"
"kitty/tools/wcswidth" "kitty/tools/wcswidth"
) )
@@ -28,7 +29,8 @@ type face_panel struct {
} }
func (self *face_panel) variable_spec(named_style string, axis_overrides map[string]float64) string { func (self *face_panel) variable_spec(named_style string, axis_overrides map[string]float64) string {
ans := fmt.Sprintf(`family="%s" variable_name="%s"`, self.family, self.current_preview.Variable_data.Variations_postscript_name_prefix) vname := self.current_preview.Variable_data.Variations_postscript_name_prefix
ans := fmt.Sprintf(`family=%s variable_name=%s`, shlex.Quote(self.family), shlex.Quote(vname))
if axis_overrides != nil { if axis_overrides != nil {
axis_values := self.current_preview.current_axis_values() axis_values := self.current_preview.current_axis_values()
maps.Copy(axis_values, axis_overrides) maps.Copy(axis_values, axis_overrides)
@@ -36,7 +38,7 @@ func (self *face_panel) variable_spec(named_style string, axis_overrides map[str
ans += fmt.Sprintf(" %s=%g", tag, val) ans += fmt.Sprintf(" %s=%g", tag, val)
} }
} else if named_style != "" { } else if named_style != "" {
ans += fmt.Sprintf(" style=\"%s\"", named_style) ans += fmt.Sprintf(" style=%s", shlex.Quote(named_style))
} }
return ans return ans
} }

View File

@@ -1,5 +1,5 @@
from enum import Enum, IntEnum, auto from enum import Enum, IntEnum, auto
from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Sequence, Tuple, TypedDict, TypeVar, Union from typing import TYPE_CHECKING, Dict, List, Literal, NamedTuple, Optional, Sequence, Tuple, TypedDict, TypeVar, Union
from kitty.types import run_once from kitty.types import run_once
from kitty.typing import CoreTextFont, FontConfigPattern from kitty.typing import CoreTextFont, FontConfigPattern
@@ -136,13 +136,13 @@ class FontModification(NamedTuple):
class FontSpec(NamedTuple): class FontSpec(NamedTuple):
family: str = '' family: Optional[str] = None
style: str = '' style: Optional[str] = None
postscript_name: str = '' postscript_name: Optional[str] = None
full_name: str = '' full_name: Optional[str] = None
system: str = '' system: Optional[str] = None
axes: Tuple[Tuple[str, float], ...] = () axes: Tuple[Tuple[str, float], ...] = ()
variable_name: str = '' variable_name: Optional[str] = None
created_from_string: str = '' created_from_string: str = ''
@property @property
@@ -153,6 +153,33 @@ class FontSpec(NamedTuple):
def is_auto(self) -> bool: def is_auto(self) -> bool:
return self.system == 'auto' return self.system == 'auto'
@property
def as_setting(self) -> str:
if self.created_from_string:
return self.created_from_string
if self.system:
return self.system
ans = []
from shlex import quote
def a(key: str, val: str) -> None:
ans.append(f'{key}={quote(val)}')
if self.family is not None:
a('family', self.family)
if self.postscript_name is not None:
a('postscript_name', self.postscript_name)
if self.full_name is not None:
a('full_name', self.full_name)
if self.variable_name is not None:
a('variable_name', self.variable_name)
if self.style is not None:
a('style', self.style)
if self.axes:
for (key, val) in self.axes:
a(key, f'{val:g}')
return ' '.join(ans)
Descriptor = Union[FontConfigPattern, CoreTextFont] Descriptor = Union[FontConfigPattern, CoreTextFont]
DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor) DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor)

View File

@@ -157,7 +157,7 @@ def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> De
def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: List[Descriptor]) -> Descriptor: def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: List[Descriptor]) -> Descriptor:
if spec.variable_name: if spec.variable_name is not None:
q = spec.variable_name.lower() q = spec.variable_name.lower()
for font in candidates: for font in candidates:
vd = get_variable_data_for_descriptor(font) vd = get_variable_data_for_descriptor(font)
@@ -177,7 +177,7 @@ def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced
def get_fine_grained_font( def get_fine_grained_font(
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), spec: FontSpec, bold: bool = False, italic: bool = False,
resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event() resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event()
) -> Descriptor: ) -> Descriptor:
font_map = all_fonts_map(monospaced) font_map = all_fonts_map(monospaced)
@@ -241,13 +241,13 @@ def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descrip
def get_font_from_spec( def get_font_from_spec(
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), spec: FontSpec, bold: bool = False, italic: bool = False,
resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event() resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event()
) -> Descriptor: ) -> Descriptor:
if not spec.is_system: if not spec.is_system:
return get_fine_grained_font(spec, bold, italic, medium_font_spec, resolved_medium_font, return get_fine_grained_font(spec, bold, italic, resolved_medium_font,
match_is_more_specific_than_family=match_is_more_specific_than_family) match_is_more_specific_than_family=match_is_more_specific_than_family)
family = spec.system family = spec.system or ''
if family == 'auto': if family == 'auto':
if bold or italic: if bold or italic:
assert resolved_medium_font is not None assert resolved_medium_font is not None
@@ -288,7 +288,7 @@ def get_font_files(opts: Options) -> FontFiles:
kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'} kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'}
for (bold, italic), attr in attr_map.items(): for (bold, italic), attr in attr_map.items():
if bold or italic: if bold or italic:
font = get_font_from_spec(getattr(opts, attr), bold, italic, medium_font_spec=opts.font_family, resolved_medium_font=medium_font) font = get_font_from_spec(getattr(opts, attr), bold, italic, resolved_medium_font=medium_font)
else: else:
font = medium_font font = medium_font
key = kd[(bold, italic)] key = kd[(bold, italic)]
@@ -382,21 +382,19 @@ def get_axis_map(face_or_descriptor: Union[Face, Descriptor]) -> Dict[str, float
return base_axis_map return base_axis_map
def spec_for_descriptor(descriptor: Descriptor) -> str: def spec_for_face(family: str, face: Face) -> FontSpec:
from shlex import quote as q v = face.get_variation()
if is_variable(descriptor): if v is None:
vd = get_variable_data_for_descriptor(descriptor) return FontSpec(family=family, postscript_name=face.postscript_name())
spec = f'family={q(descriptor["family"])}' vd = face.get_variable_data()
if vd['variations_postscript_name_prefix']: varname = vd['variations_postscript_name_prefix']
spec += f' variable_name={q(vd["variations_postscript_name_prefix"])}' ns = get_named_style(face)
ns = get_named_style(descriptor) if ns is None:
if ns is None: axes = []
for key, val in get_axis_map(descriptor).items(): for key, val in get_axis_map(face).items():
spec += f' {key}={val:g}' axes.append((key, val))
else: return FontSpec(family=family, variable_name=varname, axes=tuple(axes))
spec = f'{spec} style={q(ns["psname"])}' return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name'])
return spec
return descriptor['postscript_name']
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -8,11 +8,10 @@ from functools import lru_cache
from typing import Dict, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple from typing import Dict, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple
from kitty.fast_data_types import CTFace, coretext_all_fonts from kitty.fast_data_types import CTFace, coretext_all_fonts
from kitty.options.types import Options
from kitty.typing import CoreTextFont from kitty.typing import CoreTextFont
from kitty.utils import log_error from kitty.utils import log_error
from . import Descriptor, DescriptorVar, FontSpec, ListedFont, Score, Scorer, VariableData, family_name_to_key from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key
attr_map = {(False, False): 'font_family', attr_map = {(False, False): 'font_family',
(True, False): 'bold_font', (True, False): 'bold_font',
@@ -202,36 +201,6 @@ def find_best_match(
return candidates[0] return candidates[0]
def get_font_from_spec(
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(),
resolved_medium_font: Optional[CoreTextFont] = None
) -> CoreTextFont:
if not spec.is_system:
raise NotImplementedError('TODO: Implement me')
family = spec.system
if family == 'auto' and (bold or italic):
assert resolved_medium_font is not None
family = resolved_medium_font['family']
return find_best_match(family, bold, italic, ignore_face=resolved_medium_font)
def get_font_files(opts: Options) -> Dict[str, CoreTextFont]:
medium_font = get_font_from_spec(opts.font_family)
ans: Dict[str, CoreTextFont] = {}
kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'}
for (bold, italic) in sorted(attr_map):
attr = attr_map[(bold, italic)]
key = kd[(bold, italic)]
if bold or italic:
font = get_font_from_spec(getattr(opts, attr), bold, italic, medium_font_spec=opts.font_family, resolved_medium_font=medium_font)
else:
font = medium_font
ans[key] = font
if key == 'medium':
setattr(get_font_files, 'medium_family', font['family'])
return ans
def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]: def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]:
ans = find_best_match(family, monospaced=False) ans = find_best_match(family, monospaced=False)
return ans, ans['bold'], ans['italic'] return ans, ans['bold'], ans['italic']

View File

@@ -8,10 +8,18 @@ import unittest
from functools import partial from functools import partial
from kitty.constants import is_macos, read_kitty_resource from kitty.constants import is_macos, read_kitty_resource
from kitty.fast_data_types import DECAWM, get_fallback_font, sprite_map_set_layout, sprite_map_set_limits, test_render_line, test_sprite_position_for, wcwidth from kitty.fast_data_types import (
DECAWM,
get_fallback_font,
sprite_map_set_layout,
sprite_map_set_limits,
test_render_line,
test_sprite_position_for,
wcwidth,
)
from kitty.fonts import family_name_to_key from kitty.fonts import family_name_to_key
from kitty.fonts.box_drawing import box_chars from kitty.fonts.box_drawing import box_chars
from kitty.fonts.common import all_fonts_map, face_from_descriptor, get_font_files, get_named_style from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, get_font_files, get_named_style, spec_for_face
from kitty.fonts.render import coalesce_symbol_maps, render_string, setup_for_testing, shape_string from kitty.fonts.render import coalesce_symbol_maps, render_string, setup_for_testing, shape_string
from kitty.options.types import Options from kitty.options.types import Options
from kitty.options.utils import parse_font_spec from kitty.options.utils import parse_font_spec
@@ -93,6 +101,18 @@ class Selection(BaseTest):
d = get_font_files(opts)['medium'] d = get_font_files(opts)['medium']
face = face_from_descriptor(d) face = face_from_descriptor(d)
self.ae(get_named_style(face)['name'], 'Black') self.ae(get_named_style(face)['name'], 'Black')
if has('cascadia code'):
opts = Options()
opts.font_family = parse_font_spec('family="cascadia code"')
opts.italic_font = parse_font_spec('family="cascadia code" variable_name= style="Light Italic"')
ff = get_font_files(opts)
def t(x, **kw):
kw['family'] = 'Cascadia Code'
face = face_from_descriptor(ff[x])
self.ae(FontSpec(**kw).as_setting, spec_for_face('Cascadia Code', face).as_setting)
t('medium', variable_name='CascadiaCodeRoman', style='Regular')
t('italic', variable_name='', style='Light Italic')
class Rendering(BaseTest): class Rendering(BaseTest):

View File

@@ -17,6 +17,7 @@ package shlex
import ( import (
"fmt" "fmt"
"kitty/tools/utils"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
) )
@@ -182,6 +183,16 @@ func Split(s string) (ans []string, err error) {
return return
} }
func Quote(s string) string {
if s == "" {
return s
}
if utils.MustCompile(`[^\w@%+=:,./-]`).MatchString(s) {
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
}
return s
}
// SplitForCompletion partitions a string into a slice of strings. It differs from Split in being // SplitForCompletion partitions a string into a slice of strings. It differs from Split in being
// more relaxed about errors and also adding an empty string at the end if s ends with a Space. // more relaxed about errors and also adding an empty string at the end if s ends with a Space.
func SplitForCompletion(s string) (argv []string, position_of_last_arg int) { func SplitForCompletion(s string) (argv []string, position_of_last_arg int) {