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:
parent
99fc3ee3cd
commit
b006ab923b
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user