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
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 97 additions and 65 deletions

View File

@ -20,7 +20,7 @@
get_variable_data_for_descriptor,
get_variable_data_for_face,
is_variable,
spec_for_descriptor,
spec_for_face,
)
from kitty.fonts.list import create_family_groups
from kitty.fonts.render import display_bitmap
@ -134,6 +134,11 @@ def render_family_sample(
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]:
font_files = get_font_files(opts)
ans: Dict[OptNames, ResolvedFace] = {}

View File

@ -11,6 +11,7 @@ import (
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/shlex"
"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 {
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 {
axis_values := self.current_preview.current_axis_values()
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)
}
} else if named_style != "" {
ans += fmt.Sprintf(" style=\"%s\"", named_style)
ans += fmt.Sprintf(" style=%s", shlex.Quote(named_style))
}
return ans
}

View File

@ -1,5 +1,5 @@
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.typing import CoreTextFont, FontConfigPattern
@ -136,13 +136,13 @@ def __repr__(self) -> str:
class FontSpec(NamedTuple):
family: str = ''
style: str = ''
postscript_name: str = ''
full_name: str = ''
system: str = ''
family: Optional[str] = None
style: Optional[str] = None
postscript_name: Optional[str] = None
full_name: Optional[str] = None
system: Optional[str] = None
axes: Tuple[Tuple[str, float], ...] = ()
variable_name: str = ''
variable_name: Optional[str] = None
created_from_string: str = ''
@property
@ -153,6 +153,33 @@ def is_system(self) -> bool:
def is_auto(self) -> bool:
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]
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:
if spec.variable_name:
if spec.variable_name is not None:
q = spec.variable_name.lower()
for font in candidates:
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(
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()
) -> Descriptor:
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(
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()
) -> Descriptor:
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)
family = spec.system
family = spec.system or ''
if family == 'auto':
if bold or italic:
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'}
for (bold, italic), attr in attr_map.items():
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:
font = medium_font
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
def spec_for_descriptor(descriptor: Descriptor) -> str:
from shlex import quote as q
if is_variable(descriptor):
vd = get_variable_data_for_descriptor(descriptor)
spec = f'family={q(descriptor["family"])}'
if vd['variations_postscript_name_prefix']:
spec += f' variable_name={q(vd["variations_postscript_name_prefix"])}'
ns = get_named_style(descriptor)
if ns is None:
for key, val in get_axis_map(descriptor).items():
spec += f' {key}={val:g}'
else:
spec = f'{spec} style={q(ns["psname"])}'
return spec
return descriptor['postscript_name']
def spec_for_face(family: str, face: Face) -> FontSpec:
v = face.get_variation()
if v is None:
return FontSpec(family=family, postscript_name=face.postscript_name())
vd = face.get_variable_data()
varname = vd['variations_postscript_name_prefix']
ns = get_named_style(face)
if ns is None:
axes = []
for key, val in get_axis_map(face).items():
axes.append((key, val))
return FontSpec(family=family, variable_name=varname, axes=tuple(axes))
return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name'])
if __name__ == '__main__':

View File

@ -8,11 +8,10 @@
from typing import Dict, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple
from kitty.fast_data_types import CTFace, coretext_all_fonts
from kitty.options.types import Options
from kitty.typing import CoreTextFont
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',
(True, False): 'bold_font',
@ -202,36 +201,6 @@ def find_best_match(
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]:
ans = find_best_match(family, monospaced=False)
return ans, ans['bold'], ans['italic']

View File

@ -8,10 +8,18 @@
from functools import partial
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.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.options.types import Options
from kitty.options.utils import parse_font_spec
@ -93,6 +101,18 @@ def t(family, psprefix, bold='Bold', italic='Italic', bi='', reg='Regular', allo
d = get_font_files(opts)['medium']
face = face_from_descriptor(d)
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):

View File

@ -17,6 +17,7 @@ package shlex
import (
"fmt"
"kitty/tools/utils"
"strings"
"unicode/utf8"
)
@ -182,6 +183,16 @@ func Split(s string) (ans []string, err error) {
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
// 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) {