mirror of
https://github.com/wez/wezterm.git
synced 2024-12-20 20:01:38 +03:00
6ccef46989
This commit adds support for computing the codepoint coverage for fonts loaded from font-dirs and the built-in, in-memory fonts. What this means is that if you have eg: a font with chinese glyphs in your font-dirs but not explicitly listed in your wezterm config, if chinese text is rendered and no match from your config is found, wezterm will be able to find the font from your font-dirs and use that implicitly. Computing the codepoint coverage is relatively expensive so we defer it until we need to perform it, and cache it.
423 lines
15 KiB
Rust
423 lines
15 KiB
Rust
use crate::db::FontDatabase;
|
|
use crate::locator::{new_locator, FontDataHandle, FontLocator, FontLocatorSelection};
|
|
use crate::rasterizer::{new_rasterizer, FontRasterizer};
|
|
use crate::shaper::{new_shaper, FontShaper, FontShaperSelection};
|
|
use anyhow::Error;
|
|
use config::{configuration, ConfigHandle, FontRasterizerSelection, TextStyle};
|
|
use std::cell::RefCell;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::rc::{Rc, Weak};
|
|
use wezterm_term::CellAttributes;
|
|
|
|
mod hbwrap;
|
|
|
|
pub mod db;
|
|
pub mod ftwrap;
|
|
pub mod locator;
|
|
pub mod parser;
|
|
pub mod rasterizer;
|
|
pub mod shaper;
|
|
pub mod units;
|
|
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
|
pub mod fcwrap;
|
|
|
|
pub use crate::rasterizer::RasterizedGlyph;
|
|
pub use crate::shaper::{FallbackIdx, FontMetrics, GlyphInfo};
|
|
|
|
pub struct LoadedFont {
|
|
rasterizers: RefCell<HashMap<FallbackIdx, Box<dyn FontRasterizer>>>,
|
|
handles: RefCell<Vec<FontDataHandle>>,
|
|
shaper: RefCell<Box<dyn FontShaper>>,
|
|
metrics: FontMetrics,
|
|
font_size: f64,
|
|
dpi: u32,
|
|
font_config: Weak<FontConfigInner>,
|
|
}
|
|
|
|
impl LoadedFont {
|
|
pub fn metrics(&self) -> FontMetrics {
|
|
self.metrics
|
|
}
|
|
|
|
fn insert_fallback_handles(&self, extra_handles: Vec<FontDataHandle>) -> anyhow::Result<bool> {
|
|
let mut loaded = false;
|
|
{
|
|
let mut handles = self.handles.borrow_mut();
|
|
for h in extra_handles {
|
|
if !handles.iter().any(|existing| *existing == h) {
|
|
match crate::parser::ParsedFont::from_locator(&h) {
|
|
Ok(_parsed) => {
|
|
let idx = handles.len() - 1;
|
|
handles.insert(idx, h);
|
|
loaded = true;
|
|
}
|
|
Err(err) => {
|
|
log::error!("Failed to parse font from {:?}: {:?}", h, err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if loaded {
|
|
*self.shaper.borrow_mut() =
|
|
new_shaper(FontShaperSelection::get_default(), &self.handles.borrow())?;
|
|
}
|
|
Ok(loaded)
|
|
}
|
|
|
|
pub fn shape(&self, text: &str) -> anyhow::Result<Vec<GlyphInfo>> {
|
|
let mut no_glyphs = vec![];
|
|
let result = self
|
|
.shaper
|
|
.borrow()
|
|
.shape(text, self.font_size, self.dpi, &mut no_glyphs);
|
|
|
|
if !no_glyphs.is_empty() {
|
|
no_glyphs.sort();
|
|
no_glyphs.dedup();
|
|
if let Some(font_config) = self.font_config.upgrade() {
|
|
let mut extra_handles = vec![];
|
|
let fallback_str = no_glyphs.iter().collect::<String>();
|
|
|
|
match font_config
|
|
.font_dirs
|
|
.borrow()
|
|
.locate_fallback_for_codepoints(&no_glyphs)
|
|
{
|
|
Ok(ref mut handles) => extra_handles.append(handles),
|
|
Err(err) => log::error!(
|
|
"Error: {} while resolving fallback for {} from font_dirs",
|
|
err,
|
|
fallback_str.escape_debug()
|
|
),
|
|
}
|
|
match font_config
|
|
.locator
|
|
.locate_fallback_for_codepoints(&no_glyphs)
|
|
{
|
|
Ok(ref mut handles) => extra_handles.append(handles),
|
|
Err(err) => log::error!(
|
|
"Error: {} while resolving fallback for {} from font-locator",
|
|
err,
|
|
fallback_str.escape_debug()
|
|
),
|
|
}
|
|
|
|
match font_config
|
|
.built_in
|
|
.borrow()
|
|
.locate_fallback_for_codepoints(&no_glyphs)
|
|
{
|
|
Ok(ref mut handles) => extra_handles.append(handles),
|
|
Err(err) => log::error!(
|
|
"Error: {} while resolving fallback for {} for built-in fonts",
|
|
err,
|
|
fallback_str.escape_debug()
|
|
),
|
|
}
|
|
|
|
if extra_handles.is_empty() {
|
|
log::error!("No fonts have glyphs for {}", fallback_str.escape_debug());
|
|
} else {
|
|
let loaded = self.insert_fallback_handles(extra_handles)?;
|
|
if loaded {
|
|
log::trace!("handles is now: {:#?}", self.handles);
|
|
return self.shape(text);
|
|
} else {
|
|
log::error!(
|
|
"No fonts have glyphs for {}, even though fallback suggested some.",
|
|
fallback_str.escape_debug()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
pub fn metrics_for_idx(&self, font_idx: usize) -> anyhow::Result<FontMetrics> {
|
|
self.shaper
|
|
.borrow()
|
|
.metrics_for_idx(font_idx, self.font_size, self.dpi)
|
|
}
|
|
|
|
pub fn rasterize_glyph(
|
|
&self,
|
|
glyph_pos: u32,
|
|
fallback: FallbackIdx,
|
|
) -> anyhow::Result<RasterizedGlyph> {
|
|
let mut rasterizers = self.rasterizers.borrow_mut();
|
|
if let Some(raster) = rasterizers.get(&fallback) {
|
|
raster.rasterize_glyph(glyph_pos, self.font_size, self.dpi)
|
|
} else {
|
|
let raster = new_rasterizer(
|
|
FontRasterizerSelection::get_default(),
|
|
&(self.handles.borrow())[fallback],
|
|
)?;
|
|
let result = raster.rasterize_glyph(glyph_pos, self.font_size, self.dpi);
|
|
rasterizers.insert(fallback, raster);
|
|
result
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FontConfigInner {
|
|
fonts: RefCell<HashMap<TextStyle, Rc<LoadedFont>>>,
|
|
metrics: RefCell<Option<FontMetrics>>,
|
|
dpi_scale: RefCell<f64>,
|
|
font_scale: RefCell<f64>,
|
|
config_generation: RefCell<usize>,
|
|
locator: Box<dyn FontLocator>,
|
|
font_dirs: RefCell<FontDatabase>,
|
|
built_in: RefCell<FontDatabase>,
|
|
}
|
|
|
|
/// Matches and loads fonts for a given input style
|
|
pub struct FontConfiguration {
|
|
inner: Rc<FontConfigInner>,
|
|
}
|
|
|
|
impl FontConfigInner {
|
|
/// Create a new empty configuration
|
|
pub fn new() -> anyhow::Result<Self> {
|
|
let locator = new_locator(FontLocatorSelection::get_default());
|
|
let config = configuration();
|
|
Ok(Self {
|
|
fonts: RefCell::new(HashMap::new()),
|
|
locator,
|
|
metrics: RefCell::new(None),
|
|
font_scale: RefCell::new(1.0),
|
|
dpi_scale: RefCell::new(1.0),
|
|
config_generation: RefCell::new(config.generation()),
|
|
font_dirs: RefCell::new(FontDatabase::with_font_dirs(&config)?),
|
|
built_in: RefCell::new(FontDatabase::with_built_in()?),
|
|
})
|
|
}
|
|
|
|
/// Given a text style, load (with caching) the font that best
|
|
/// matches according to the fontconfig pattern.
|
|
fn resolve_font(&self, myself: &Rc<Self>, style: &TextStyle) -> anyhow::Result<Rc<LoadedFont>> {
|
|
let mut fonts = self.fonts.borrow_mut();
|
|
|
|
let config = configuration();
|
|
let current_generation = config.generation();
|
|
if current_generation != *self.config_generation.borrow() {
|
|
// Config was reloaded, invalidate our caches
|
|
fonts.clear();
|
|
self.metrics.borrow_mut().take();
|
|
*self.font_dirs.borrow_mut() = FontDatabase::with_font_dirs(&config)?;
|
|
*self.config_generation.borrow_mut() = current_generation;
|
|
}
|
|
|
|
if let Some(entry) = fonts.get(style) {
|
|
return Ok(Rc::clone(entry));
|
|
}
|
|
|
|
let attributes = style.font_with_fallback();
|
|
let preferred_attributes = attributes
|
|
.iter()
|
|
.filter(|a| !a.is_fallback)
|
|
.map(|a| a.clone())
|
|
.collect::<Vec<_>>();
|
|
let fallback_attributes = attributes
|
|
.iter()
|
|
.filter(|a| a.is_fallback)
|
|
.map(|a| a.clone())
|
|
.collect::<Vec<_>>();
|
|
let mut loaded = HashSet::new();
|
|
|
|
let mut handles = vec![];
|
|
for attrs in &[&preferred_attributes, &fallback_attributes] {
|
|
self.font_dirs
|
|
.borrow()
|
|
.resolve_multiple(attrs, &mut handles, &mut loaded);
|
|
handles.append(&mut self.locator.load_fonts(attrs, &mut loaded)?);
|
|
self.built_in
|
|
.borrow()
|
|
.resolve_multiple(attrs, &mut handles, &mut loaded);
|
|
}
|
|
|
|
for attr in &attributes {
|
|
if !attr.is_fallback && !loaded.contains(attr) {
|
|
let styled_extra = if attr.bold || attr.italic {
|
|
". A bold or italic variant of the font was requested; \
|
|
TrueType and OpenType fonts don't have an automatic way to \
|
|
produce these font variants, so a separate font file containing \
|
|
the bold or italic variant must be installed"
|
|
} else {
|
|
""
|
|
};
|
|
|
|
let is_primary = config.font.font.iter().any(|a| a == attr);
|
|
let derived_from_primary = config.font.font.iter().any(|a| a.family == attr.family);
|
|
|
|
let explanation = if is_primary {
|
|
// This is the primary font selection
|
|
format!(
|
|
"Unable to load a font specified by your font={} configuration",
|
|
attr
|
|
)
|
|
} else if derived_from_primary {
|
|
// it came from font_rules and may have been derived from
|
|
// their primary font (we can't know for sure)
|
|
format!(
|
|
"Unable to load a font matching one of your font_rules: {}. \
|
|
Note that wezterm will synthesize font_rules to select bold \
|
|
and italic fonts based on your primary font configuration",
|
|
attr
|
|
)
|
|
} else {
|
|
format!(
|
|
"Unable to load a font matching one of your font_rules: {}",
|
|
attr
|
|
)
|
|
};
|
|
|
|
mux::connui::show_configuration_error_message(&format!(
|
|
"{}. Fallback(s) are being used instead, and the terminal \
|
|
may not render as intended{}. See \
|
|
https://wezfurlong.org/wezterm/config/fonts.html for more information",
|
|
explanation, styled_extra
|
|
));
|
|
}
|
|
}
|
|
|
|
let shaper = new_shaper(FontShaperSelection::get_default(), &handles)?;
|
|
|
|
let config = configuration();
|
|
let font_size = config.font_size * *self.font_scale.borrow();
|
|
let dpi = *self.dpi_scale.borrow() as u32 * config.dpi as u32;
|
|
let metrics = shaper.metrics(font_size, dpi)?;
|
|
|
|
let loaded = Rc::new(LoadedFont {
|
|
rasterizers: RefCell::new(HashMap::new()),
|
|
handles: RefCell::new(handles),
|
|
shaper: RefCell::new(shaper),
|
|
metrics,
|
|
font_size,
|
|
dpi,
|
|
font_config: Rc::downgrade(myself),
|
|
});
|
|
|
|
fonts.insert(style.clone(), Rc::clone(&loaded));
|
|
|
|
Ok(loaded)
|
|
}
|
|
|
|
pub fn change_scaling(&self, font_scale: f64, dpi_scale: f64) {
|
|
*self.dpi_scale.borrow_mut() = dpi_scale;
|
|
*self.font_scale.borrow_mut() = font_scale;
|
|
self.fonts.borrow_mut().clear();
|
|
self.metrics.borrow_mut().take();
|
|
}
|
|
|
|
/// Returns the baseline font specified in the configuration
|
|
pub fn default_font(&self, myself: &Rc<Self>) -> anyhow::Result<Rc<LoadedFont>> {
|
|
self.resolve_font(myself, &configuration().font)
|
|
}
|
|
|
|
pub fn get_font_scale(&self) -> f64 {
|
|
*self.font_scale.borrow()
|
|
}
|
|
|
|
pub fn default_font_metrics(&self, myself: &Rc<Self>) -> Result<FontMetrics, Error> {
|
|
{
|
|
let metrics = self.metrics.borrow();
|
|
if let Some(metrics) = metrics.as_ref() {
|
|
return Ok(*metrics);
|
|
}
|
|
}
|
|
|
|
let font = self.default_font(myself)?;
|
|
let metrics = font.metrics();
|
|
|
|
*self.metrics.borrow_mut() = Some(metrics);
|
|
|
|
Ok(metrics)
|
|
}
|
|
|
|
/// Apply the defined font_rules from the user configuration to
|
|
/// produce the text style that best matches the supplied input
|
|
/// cell attributes.
|
|
pub fn match_style<'a>(
|
|
&self,
|
|
config: &'a ConfigHandle,
|
|
attrs: &CellAttributes,
|
|
) -> &'a TextStyle {
|
|
// a little macro to avoid boilerplate for matching the rules.
|
|
// If the rule doesn't specify a value for an attribute then
|
|
// it will implicitly match. If it specifies an attribute
|
|
// then it has to have the same value as that in the input attrs.
|
|
macro_rules! attr_match {
|
|
($ident:ident, $rule:expr) => {
|
|
if let Some($ident) = $rule.$ident {
|
|
if $ident != attrs.$ident() {
|
|
// Does not match
|
|
continue;
|
|
}
|
|
}
|
|
// matches so far...
|
|
};
|
|
};
|
|
|
|
for rule in &config.font_rules {
|
|
attr_match!(intensity, &rule);
|
|
attr_match!(underline, &rule);
|
|
attr_match!(italic, &rule);
|
|
attr_match!(blink, &rule);
|
|
attr_match!(reverse, &rule);
|
|
attr_match!(strikethrough, &rule);
|
|
attr_match!(invisible, &rule);
|
|
|
|
// If we get here, then none of the rules didn't match,
|
|
// so we therefore assume that it did match overall.
|
|
return &rule.font;
|
|
}
|
|
&config.font
|
|
}
|
|
}
|
|
|
|
impl FontConfiguration {
|
|
/// Create a new empty configuration
|
|
pub fn new() -> anyhow::Result<Self> {
|
|
let inner = Rc::new(FontConfigInner::new()?);
|
|
Ok(Self { inner })
|
|
}
|
|
|
|
/// Given a text style, load (with caching) the font that best
|
|
/// matches according to the fontconfig pattern.
|
|
pub fn resolve_font(&self, style: &TextStyle) -> anyhow::Result<Rc<LoadedFont>> {
|
|
self.inner.resolve_font(&self.inner, style)
|
|
}
|
|
|
|
pub fn change_scaling(&self, font_scale: f64, dpi_scale: f64) {
|
|
self.inner.change_scaling(font_scale, dpi_scale)
|
|
}
|
|
|
|
/// Returns the baseline font specified in the configuration
|
|
pub fn default_font(&self) -> anyhow::Result<Rc<LoadedFont>> {
|
|
self.inner.default_font(&self.inner)
|
|
}
|
|
|
|
pub fn get_font_scale(&self) -> f64 {
|
|
self.inner.get_font_scale()
|
|
}
|
|
|
|
pub fn default_font_metrics(&self) -> Result<FontMetrics, Error> {
|
|
self.inner.default_font_metrics(&self.inner)
|
|
}
|
|
|
|
/// Apply the defined font_rules from the user configuration to
|
|
/// produce the text style that best matches the supplied input
|
|
/// cell attributes.
|
|
pub fn match_style<'a>(
|
|
&self,
|
|
config: &'a ConfigHandle,
|
|
attrs: &CellAttributes,
|
|
) -> &'a TextStyle {
|
|
self.inner.match_style(config, attrs)
|
|
}
|
|
}
|