mirror of
https://github.com/wez/wezterm.git
synced 2024-12-29 16:42:13 +03:00
fonts: adopt CSS Fonts Level 3 compatible font matching
This commit is contained in:
parent
99fc3ee3cd
commit
b006ab923b
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::ftwrap::Library;
|
use crate::ftwrap::Library;
|
||||||
use crate::locator::FontDataSource;
|
use crate::locator::FontDataSource;
|
||||||
use crate::parser::{load_built_in_fonts, parse_and_collect_font_info, FontMatch, ParsedFont};
|
use crate::parser::{load_built_in_fonts, parse_and_collect_font_info, ParsedFont};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use config::{Config, FontAttributes};
|
use config::{Config, FontAttributes};
|
||||||
use rangeset::RangeSet;
|
use rangeset::RangeSet;
|
||||||
@ -50,14 +50,12 @@ impl Entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct FontDatabase {
|
pub struct FontDatabase {
|
||||||
by_family: HashMap<String, Vec<Arc<Entry>>>,
|
|
||||||
by_full_name: HashMap<String, Arc<Entry>>,
|
by_full_name: HashMap<String, Arc<Entry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontDatabase {
|
impl FontDatabase {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
by_family: HashMap::new(),
|
|
||||||
by_full_name: HashMap::new(),
|
by_full_name: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,13 +67,6 @@ impl FontDatabase {
|
|||||||
coverage: Mutex::new(None),
|
coverage: Mutex::new(None),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(family) = entry.parsed.names().family.as_ref() {
|
|
||||||
self.by_family
|
|
||||||
.entry(family.to_string())
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(Arc::clone(&entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.by_full_name
|
self.by_full_name
|
||||||
.entry(entry.parsed.names().full_name.clone())
|
.entry(entry.parsed.names().full_name.clone())
|
||||||
.or_insert(entry);
|
.or_insert(entry);
|
||||||
@ -182,23 +173,20 @@ impl FontDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve(&self, font_attr: &FontAttributes) -> Option<&ParsedFont> {
|
pub fn resolve(&self, font_attr: &FontAttributes) -> Option<&ParsedFont> {
|
||||||
if let Some(entry) = self.by_full_name.get(&font_attr.family) {
|
let candidates: Vec<&ParsedFont> = self
|
||||||
if entry.parsed.matches_attributes(font_attr) == FontMatch::FullName {
|
.by_full_name
|
||||||
return Some(&entry.parsed);
|
.values()
|
||||||
}
|
.filter_map(|entry| {
|
||||||
|
if entry.parsed.matches_name(font_attr) {
|
||||||
|
Some(&entry.parsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
if let Some(family) = self.by_family.get(&font_attr.family) {
|
if let Some(idx) = ParsedFont::best_matching_index(font_attr, &candidates) {
|
||||||
let mut candidates = vec![];
|
return candidates.get(idx).map(|&p| p);
|
||||||
for entry in family {
|
|
||||||
let res = entry.parsed.matches_attributes(font_attr);
|
|
||||||
if res != FontMatch::NoMatch {
|
|
||||||
candidates.push((res, entry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
candidates.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
let best = candidates.first()?;
|
|
||||||
return Some(&best.1.parsed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
@ -89,8 +89,7 @@ impl FontLocator for CoreTextFontLocator {
|
|||||||
for attr in fonts_selection {
|
for attr in fonts_selection {
|
||||||
if let Ok(descriptor) = descriptor_from_attr(attr) {
|
if let Ok(descriptor) = descriptor_from_attr(attr) {
|
||||||
let handles = handles_from_descriptor(&descriptor);
|
let handles = handles_from_descriptor(&descriptor);
|
||||||
let ranked = ParsedFont::rank_matches(attr, handles);
|
if let Some(parsed) = ParsedFont::best_match(attr, handles) {
|
||||||
for parsed in ranked {
|
|
||||||
fonts.push(parsed);
|
fonts.push(parsed);
|
||||||
loaded.insert(attr.clone());
|
loaded.insert(attr.clone());
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::fcwrap;
|
use crate::fcwrap;
|
||||||
use crate::locator::{FontDataHandle, FontDataSource, FontLocator};
|
use crate::locator::{FontDataHandle, FontDataSource, FontLocator};
|
||||||
use crate::parser::{FontMatch, ParsedFont};
|
use crate::parser::ParsedFont;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use config::FontAttributes;
|
use config::FontAttributes;
|
||||||
use fcwrap::{to_fc_weight, to_fc_width, CharSet, Pattern as FontPattern};
|
use fcwrap::{to_fc_weight, to_fc_width, CharSet, Pattern as FontPattern};
|
||||||
@ -98,7 +98,7 @@ impl FontLocator for FontConfigFontLocator {
|
|||||||
// so we need to parse the returned font
|
// so we need to parse the returned font
|
||||||
// here to see if we got what we asked for.
|
// here to see if we got what we asked for.
|
||||||
if let Ok(parsed) = crate::parser::ParsedFont::from_locator(&handle) {
|
if let Ok(parsed) = crate::parser::ParsedFont::from_locator(&handle) {
|
||||||
if parsed.matches_attributes(attr) != FontMatch::NoMatch {
|
if parsed.matches_name(attr) {
|
||||||
log::trace!("found font-config match for {:?}", parsed.names());
|
log::trace!("found font-config match for {:?}", parsed.names());
|
||||||
fonts.push(parsed);
|
fonts.push(parsed);
|
||||||
loaded.insert(attr.clone());
|
loaded.insert(attr.clone());
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#![cfg(windows)]
|
#![cfg(windows)]
|
||||||
|
|
||||||
use crate::locator::{FontDataSource, FontLocator};
|
use crate::locator::{FontDataSource, FontLocator};
|
||||||
use crate::parser::{parse_and_collect_font_info, rank_matching_fonts, FontMatch, ParsedFont};
|
use crate::parser::{best_matching_font, parse_and_collect_font_info, ParsedFont};
|
||||||
use config::{FontAttributes, FontStretch, FontWeight as WTFontWeight};
|
use config::{FontAttributes, FontStretch, FontWeight as WTFontWeight};
|
||||||
use dwrote::{FontDescriptor, FontStretch, FontStyle, FontWeight};
|
use dwrote::{FontDescriptor, FontStretch, FontStyle, FontWeight};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@ -63,7 +63,7 @@ fn extract_font_data(font: HFONT, attr: &FontAttributes) -> anyhow::Result<Parse
|
|||||||
|
|
||||||
let mut font_info = vec![];
|
let mut font_info = vec![];
|
||||||
parse_and_collect_font_info(&source, &mut font_info)?;
|
parse_and_collect_font_info(&source, &mut font_info)?;
|
||||||
let matches = ParsedFont::rank_matches(attr, font_info);
|
let matches = ParsedFont::best_match(attr, font_info);
|
||||||
|
|
||||||
for m in matches {
|
for m in matches {
|
||||||
return Ok(m);
|
return Ok(m);
|
||||||
@ -145,11 +145,9 @@ fn handle_from_descriptor(
|
|||||||
|
|
||||||
log::debug!("{} -> {}", family_name, path.display());
|
log::debug!("{} -> {}", family_name, path.display());
|
||||||
let source = FontDataSource::OnDisk(path);
|
let source = FontDataSource::OnDisk(path);
|
||||||
match rank_matching_fonts(&source, attr) {
|
match best_matching_font(&source, attr) {
|
||||||
Ok(matches) => {
|
Ok(parsed) => {
|
||||||
for p in matches {
|
return Some(parsed);
|
||||||
return Some(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => log::warn!("While parsing: {:?}: {:#}", source, err),
|
Err(err) => log::warn!("While parsing: {:?}: {:#}", source, err),
|
||||||
}
|
}
|
||||||
@ -176,7 +174,7 @@ impl FontLocator for GdiFontLocator {
|
|||||||
fonts: &mut Vec<ParsedFont>,
|
fonts: &mut Vec<ParsedFont>,
|
||||||
loaded: &mut HashSet<FontAttributes>,
|
loaded: &mut HashSet<FontAttributes>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if parsed.matches_attributes(font_attr) != FontMatch::NoMatch {
|
if parsed.matches_name(font_attr) {
|
||||||
fonts.push(parsed);
|
fonts.push(parsed);
|
||||||
loaded.insert(font_attr.clone());
|
loaded.insert(font_attr.clone());
|
||||||
true
|
true
|
||||||
|
@ -87,57 +87,164 @@ impl ParsedFont {
|
|||||||
self.italic
|
self.italic
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches_attributes(&self, attr: &FontAttributes) -> FontMatch {
|
pub fn matches_name(&self, attr: &FontAttributes) -> bool {
|
||||||
if let Some(fam) = self.names.family.as_ref() {
|
if let Some(fam) = self.names.family.as_ref() {
|
||||||
if attr.family == *fam {
|
if attr.family == *fam {
|
||||||
if attr.stretch == self.stretch {
|
return true;
|
||||||
let wanted_weight = attr.weight.to_opentype_weight();
|
|
||||||
let weight = self.weight.to_opentype_weight();
|
|
||||||
|
|
||||||
if weight >= wanted_weight {
|
|
||||||
if attr.italic == self.italic {
|
|
||||||
return FontMatch::Weight(weight - wanted_weight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.matches_full_or_ps_name(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches_full_or_ps_name(&self, attr: &FontAttributes) -> bool {
|
||||||
if attr.family == self.names.full_name {
|
if attr.family == self.names.full_name {
|
||||||
return FontMatch::FullName;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
if let Some(ps) = self.names.postscript_name.as_ref() {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if attr.family == self.names.full_name {
|
|
||||||
FontMatch::FullName
|
|
||||||
} else if let Some(ps) = self.names.postscript_name.as_ref() {
|
|
||||||
if attr.family == *ps {
|
if attr.family == *ps {
|
||||||
FontMatch::FullName
|
return true;
|
||||||
} else {
|
|
||||||
FontMatch::NoMatch
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
FontMatch::NoMatch
|
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rank_matches(attr: &FontAttributes, fonts: Vec<Self>) -> Vec<Self> {
|
/// Perform CSS Fonts Level 3 font matching.
|
||||||
let mut candidates = vec![];
|
/// This implementation is derived from the `find_best_match` function
|
||||||
for p in fonts {
|
/// in the font-kit crate which is
|
||||||
let res = p.matches_attributes(attr);
|
/// Copyright © 2018 The Pathfinder Project Developers.
|
||||||
if res != FontMatch::NoMatch {
|
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching says
|
||||||
candidates.push((res, p));
|
pub fn best_matching_index<P: std::ops::Deref<Target = Self> + std::fmt::Debug>(
|
||||||
}
|
attr: &FontAttributes,
|
||||||
}
|
fonts: &[P],
|
||||||
candidates.sort_by(|a, b| a.0.cmp(&b.0));
|
) -> Option<usize> {
|
||||||
candidates.into_iter().map(|(_, p)| p).collect()
|
if fonts.is_empty() {
|
||||||
}
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
|
let mut candidates: Vec<usize> = (0..fonts.len()).collect();
|
||||||
pub enum FontMatch {
|
|
||||||
Weight(u16),
|
// First, filter by stretch
|
||||||
FullName,
|
let stretch_value = attr.stretch.to_opentype_stretch();
|
||||||
NoMatch,
|
let stretch = if candidates
|
||||||
|
.iter()
|
||||||
|
.any(|&idx| fonts[idx].stretch == attr.stretch)
|
||||||
|
{
|
||||||
|
attr.stretch
|
||||||
|
} else if attr.stretch <= FontStretch::Normal {
|
||||||
|
// Find the closest stretch, looking at narrower first before
|
||||||
|
// looking at wider candidates
|
||||||
|
match candidates
|
||||||
|
.iter()
|
||||||
|
.filter(|&&idx| fonts[idx].stretch < attr.stretch)
|
||||||
|
.min_by_key(|&&idx| stretch_value - fonts[idx].stretch.to_opentype_stretch())
|
||||||
|
{
|
||||||
|
Some(&idx) => fonts[idx].stretch,
|
||||||
|
None => {
|
||||||
|
let idx = *candidates.iter().min_by_key(|&&idx| {
|
||||||
|
fonts[idx].stretch.to_opentype_stretch() - stretch_value
|
||||||
|
})?;
|
||||||
|
fonts[idx].stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Look at wider values, then narrower values
|
||||||
|
match candidates
|
||||||
|
.iter()
|
||||||
|
.filter(|&&idx| fonts[idx].stretch > attr.stretch)
|
||||||
|
.min_by_key(|&&idx| fonts[idx].stretch.to_opentype_stretch() - stretch_value)
|
||||||
|
{
|
||||||
|
Some(&idx) => fonts[idx].stretch,
|
||||||
|
None => {
|
||||||
|
let idx = *candidates.iter().min_by_key(|&&idx| {
|
||||||
|
stretch_value - fonts[idx].stretch.to_opentype_stretch()
|
||||||
|
})?;
|
||||||
|
fonts[idx].stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reduce to matching stretches
|
||||||
|
candidates.retain(|&idx| fonts[idx].stretch == stretch);
|
||||||
|
|
||||||
|
// Now match style: italics
|
||||||
|
let styles = [attr.italic, !attr.italic];
|
||||||
|
let italic = *styles
|
||||||
|
.iter()
|
||||||
|
.filter(|&&italic| candidates.iter().any(|&idx| fonts[idx].italic == italic))
|
||||||
|
.next()?;
|
||||||
|
|
||||||
|
// Reduce to matching italics
|
||||||
|
candidates.retain(|&idx| fonts[idx].italic == italic);
|
||||||
|
|
||||||
|
// And now match by font weight
|
||||||
|
let query_weight = attr.weight.to_opentype_weight();
|
||||||
|
let weight = if candidates
|
||||||
|
.iter()
|
||||||
|
.any(|&idx| fonts[idx].weight == attr.weight)
|
||||||
|
{
|
||||||
|
// Exact match for the requested weight
|
||||||
|
attr.weight
|
||||||
|
} else if attr.weight == FontWeight::Regular
|
||||||
|
&& candidates
|
||||||
|
.iter()
|
||||||
|
.any(|&idx| fonts[idx].weight == FontWeight::Medium)
|
||||||
|
{
|
||||||
|
// https://drafts.csswg.org/css-fonts-3/#font-style-matching says
|
||||||
|
// that if they want weight=400 and we don't have 400,
|
||||||
|
// look at weight 500 first
|
||||||
|
FontWeight::Medium
|
||||||
|
} else if attr.weight == FontWeight::Medium
|
||||||
|
&& candidates
|
||||||
|
.iter()
|
||||||
|
.any(|&idx| fonts[idx].weight == FontWeight::Regular)
|
||||||
|
{
|
||||||
|
// Similarly, look at regular before Medium if they wanted
|
||||||
|
// Medium and we didn't have it
|
||||||
|
FontWeight::Regular
|
||||||
|
} else if attr.weight <= FontWeight::Medium {
|
||||||
|
// Find best lighter weight, else best heavier weight
|
||||||
|
match candidates
|
||||||
|
.iter()
|
||||||
|
.filter(|&&idx| fonts[idx].weight <= attr.weight)
|
||||||
|
.min_by_key(|&&idx| query_weight - fonts[idx].weight.to_opentype_weight())
|
||||||
|
{
|
||||||
|
Some(&idx) => fonts[idx].weight,
|
||||||
|
None => {
|
||||||
|
let idx = *candidates.iter().min_by_key(|&&idx| {
|
||||||
|
fonts[idx].weight.to_opentype_weight() - query_weight
|
||||||
|
})?;
|
||||||
|
fonts[idx].weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find best heavier weight, else best lighter weight
|
||||||
|
match candidates
|
||||||
|
.iter()
|
||||||
|
.filter(|&&idx| fonts[idx].weight >= attr.weight)
|
||||||
|
.min_by_key(|&&idx| fonts[idx].weight.to_opentype_weight() - query_weight)
|
||||||
|
{
|
||||||
|
Some(&idx) => fonts[idx].weight,
|
||||||
|
None => {
|
||||||
|
let idx = *candidates.iter().min_by_key(|&&idx| {
|
||||||
|
query_weight - fonts[idx].weight.to_opentype_weight()
|
||||||
|
})?;
|
||||||
|
fonts[idx].weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reduce to matching weight
|
||||||
|
candidates.retain(|&idx| fonts[idx].weight == weight);
|
||||||
|
|
||||||
|
// The first one in this set is our best match
|
||||||
|
candidates.into_iter().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn best_match(attr: &FontAttributes, mut fonts: Vec<Self>) -> Option<Self> {
|
||||||
|
let refs: Vec<&Self> = fonts.iter().collect();
|
||||||
|
let idx = Self::best_matching_index(attr, &refs)?;
|
||||||
|
fonts.drain(idx..=idx).next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In case the user has a broken configuration, or no configuration,
|
/// In case the user has a broken configuration, or no configuration,
|
||||||
@ -186,13 +293,13 @@ pub(crate) fn load_built_in_fonts(font_info: &mut Vec<ParsedFont>) -> anyhow::Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rank_matching_fonts(
|
pub fn best_matching_font(
|
||||||
source: &FontDataSource,
|
source: &FontDataSource,
|
||||||
font_attr: &FontAttributes,
|
font_attr: &FontAttributes,
|
||||||
) -> anyhow::Result<Vec<ParsedFont>> {
|
) -> anyhow::Result<Option<ParsedFont>> {
|
||||||
let mut font_info = vec![];
|
let mut font_info = vec![];
|
||||||
parse_and_collect_font_info(source, &mut font_info)?;
|
parse_and_collect_font_info(source, &mut font_info)?;
|
||||||
Ok(ParsedFont::rank_matches(font_attr, font_info))
|
Ok(ParsedFont::best_match(font_attr, font_info))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_and_collect_font_info(
|
pub(crate) fn parse_and_collect_font_info(
|
||||||
|
Loading…
Reference in New Issue
Block a user