1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-28 07:55:03 +03:00

fonts: adopt CSS Fonts Level 3 compatible font matching

This commit is contained in:
Wez Furlong 2021-04-09 14:05:41 -07:00
parent 99fc3ee3cd
commit b006ab923b
5 changed files with 173 additions and 81 deletions

View File

@ -2,7 +2,7 @@
use crate::ftwrap::Library;
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 config::{Config, FontAttributes};
use rangeset::RangeSet;
@ -50,14 +50,12 @@ impl Entry {
}
pub struct FontDatabase {
by_family: HashMap<String, Vec<Arc<Entry>>>,
by_full_name: HashMap<String, Arc<Entry>>,
}
impl FontDatabase {
pub fn new() -> Self {
Self {
by_family: HashMap::new(),
by_full_name: HashMap::new(),
}
}
@ -69,13 +67,6 @@ impl FontDatabase {
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
.entry(entry.parsed.names().full_name.clone())
.or_insert(entry);
@ -182,23 +173,20 @@ impl FontDatabase {
}
pub fn resolve(&self, font_attr: &FontAttributes) -> Option<&ParsedFont> {
if let Some(entry) = self.by_full_name.get(&font_attr.family) {
if entry.parsed.matches_attributes(font_attr) == FontMatch::FullName {
return Some(&entry.parsed);
}
}
if let Some(family) = self.by_family.get(&font_attr.family) {
let mut candidates = vec![];
for entry in family {
let res = entry.parsed.matches_attributes(font_attr);
if res != FontMatch::NoMatch {
candidates.push((res, entry));
let candidates: Vec<&ParsedFont> = self
.by_full_name
.values()
.filter_map(|entry| {
if entry.parsed.matches_name(font_attr) {
Some(&entry.parsed)
} else {
None
}
}
candidates.sort_by(|a, b| a.0.cmp(&b.0));
let best = candidates.first()?;
return Some(&best.1.parsed);
})
.collect();
if let Some(idx) = ParsedFont::best_matching_index(font_attr, &candidates) {
return candidates.get(idx).map(|&p| p);
}
None

View File

@ -89,8 +89,7 @@ impl FontLocator for CoreTextFontLocator {
for attr in fonts_selection {
if let Ok(descriptor) = descriptor_from_attr(attr) {
let handles = handles_from_descriptor(&descriptor);
let ranked = ParsedFont::rank_matches(attr, handles);
for parsed in ranked {
if let Some(parsed) = ParsedFont::best_match(attr, handles) {
fonts.push(parsed);
loaded.insert(attr.clone());
}

View File

@ -1,6 +1,6 @@
use crate::fcwrap;
use crate::locator::{FontDataHandle, FontDataSource, FontLocator};
use crate::parser::{FontMatch, ParsedFont};
use crate::parser::ParsedFont;
use anyhow::Context;
use config::FontAttributes;
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
// here to see if we got what we asked for.
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());
fonts.push(parsed);
loaded.insert(attr.clone());

View File

@ -1,7 +1,7 @@
#![cfg(windows)]
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 dwrote::{FontDescriptor, FontStretch, FontStyle, FontWeight};
use std::borrow::Cow;
@ -63,7 +63,7 @@ fn extract_font_data(font: HFONT, attr: &FontAttributes) -> anyhow::Result<Parse
let mut font_info = vec![];
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 {
return Ok(m);
@ -145,11 +145,9 @@ fn handle_from_descriptor(
log::debug!("{} -> {}", family_name, path.display());
let source = FontDataSource::OnDisk(path);
match rank_matching_fonts(&source, attr) {
Ok(matches) => {
for p in matches {
return Some(p);
}
match best_matching_font(&source, attr) {
Ok(parsed) => {
return Some(parsed);
}
Err(err) => log::warn!("While parsing: {:?}: {:#}", source, err),
}
@ -176,7 +174,7 @@ impl FontLocator for GdiFontLocator {
fonts: &mut Vec<ParsedFont>,
loaded: &mut HashSet<FontAttributes>,
) -> bool {
if parsed.matches_attributes(font_attr) != FontMatch::NoMatch {
if parsed.matches_name(font_attr) {
fonts.push(parsed);
loaded.insert(font_attr.clone());
true

View File

@ -87,57 +87,164 @@ impl ParsedFont {
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 attr.family == *fam {
if attr.stretch == self.stretch {
let wanted_weight = attr.weight.to_opentype_weight();
let weight = self.weight.to_opentype_weight();
return true;
}
}
self.matches_full_or_ps_name(attr)
}
if weight >= wanted_weight {
if attr.italic == self.italic {
return FontMatch::Weight(weight - wanted_weight);
}
}
pub fn matches_full_or_ps_name(&self, attr: &FontAttributes) -> bool {
if attr.family == self.names.full_name {
return true;
}
if let Some(ps) = self.names.postscript_name.as_ref() {
if attr.family == *ps {
return true;
}
}
false
}
if attr.family == self.names.full_name {
return FontMatch::FullName;
}
/// Perform CSS Fonts Level 3 font matching.
/// This implementation is derived from the `find_best_match` function
/// in the font-kit crate which is
/// Copyright © 2018 The Pathfinder Project Developers.
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching says
pub fn best_matching_index<P: std::ops::Deref<Target = Self> + std::fmt::Debug>(
attr: &FontAttributes,
fonts: &[P],
) -> Option<usize> {
if fonts.is_empty() {
return None;
}
let mut candidates: Vec<usize> = (0..fonts.len()).collect();
// First, filter by stretch
let stretch_value = attr.stretch.to_opentype_stretch();
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
}
}
};
if attr.family == self.names.full_name {
FontMatch::FullName
} else if let Some(ps) = self.names.postscript_name.as_ref() {
if attr.family == *ps {
FontMatch::FullName
} else {
FontMatch::NoMatch
// 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 {
FontMatch::NoMatch
}
}
pub fn rank_matches(attr: &FontAttributes, fonts: Vec<Self>) -> Vec<Self> {
let mut candidates = vec![];
for p in fonts {
let res = p.matches_attributes(attr);
if res != FontMatch::NoMatch {
candidates.push((res, p));
// 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
}
}
}
candidates.sort_by(|a, b| a.0.cmp(&b.0));
candidates.into_iter().map(|(_, p)| p).collect()
}
}
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
pub enum FontMatch {
Weight(u16),
FullName,
NoMatch,
// 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,
@ -186,13 +293,13 @@ pub(crate) fn load_built_in_fonts(font_info: &mut Vec<ParsedFont>) -> anyhow::Re
Ok(())
}
pub fn rank_matching_fonts(
pub fn best_matching_font(
source: &FontDataSource,
font_attr: &FontAttributes,
) -> anyhow::Result<Vec<ParsedFont>> {
) -> anyhow::Result<Option<ParsedFont>> {
let mut font_info = vec![];
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(