use crate::db::FontDatabase; use crate::locator::{new_locator, FontDataHandle, FontLocator}; use crate::rasterizer::{new_rasterizer, FontRasterizer}; use crate::shaper::{new_shaper, FontShaper}; use anyhow::{Context, 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; use window::default_dpi; 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>>, handles: RefCell>, shaper: RefCell>, metrics: FontMetrics, font_size: f64, dpi: u32, font_config: Weak, } impl LoadedFont { pub fn metrics(&self) -> FontMetrics { self.metrics } fn insert_fallback_handles(&self, extra_handles: Vec) -> anyhow::Result { 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 { if let Some(font_config) = self.font_config.upgrade() { *self.shaper.borrow_mut() = new_shaper(&*font_config.config.borrow(), &self.handles.borrow())?; } } Ok(loaded) } pub fn shape(&self, text: &str) -> anyhow::Result> { 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::(); 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_unicode() ), } if extra_handles.is_empty() { 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_unicode() ), } } if extra_handles.is_empty() { 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_unicode() ), } } if extra_handles.is_empty() { font_config.advise_no_glyphs(&fallback_str); } else { match self.insert_fallback_handles(extra_handles) { Ok(true) => { log::trace!("handles is now: {:#?}", self.handles); return self.shape(text); } Err(err) => { log::error!("Failed to insert fallback handles: {:#}", err); font_config.advise_no_glyphs(&fallback_str); } Ok(false) => { font_config.advise_no_glyphs(&fallback_str); } } } } } result } pub fn metrics_for_idx(&self, font_idx: usize) -> anyhow::Result { 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 { 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_selection = self .font_config .upgrade() .map_or(FontRasterizerSelection::default(), |c| { c.config.borrow().font_rasterizer }); let raster = new_rasterizer(raster_selection, &(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>>, metrics: RefCell>, dpi_scale: RefCell, font_scale: RefCell, config: RefCell, locator: Box, font_dirs: RefCell, built_in: RefCell, no_glyphs: RefCell>, } /// Matches and loads fonts for a given input style pub struct FontConfiguration { inner: Rc, } impl FontConfigInner { /// Create a new empty configuration pub fn new(config: Option) -> anyhow::Result { let config = config.unwrap_or_else(|| configuration()); let locator = new_locator(config.font_locator); 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: RefCell::new(config.clone()), font_dirs: RefCell::new(FontDatabase::with_font_dirs(&config)?), built_in: RefCell::new(FontDatabase::with_built_in()?), no_glyphs: RefCell::new(HashSet::new()), }) } fn config_changed(&self, config: &ConfigHandle) -> anyhow::Result<()> { let mut fonts = self.fonts.borrow_mut(); *self.config.borrow_mut() = config.clone(); // Config was reloaded, invalidate our caches fonts.clear(); self.metrics.borrow_mut().take(); self.no_glyphs.borrow_mut().clear(); *self.font_dirs.borrow_mut() = FontDatabase::with_font_dirs(config)?; Ok(()) } fn advise_no_glyphs(&self, fallback_str: &str) { let mut no_glyphs = self.no_glyphs.borrow_mut(); let mut notif = String::new(); for c in fallback_str.chars() { if !no_glyphs.contains(&c) { notif.push(c); no_glyphs.insert(c); } } if !notif.is_empty() { mux::connui::show_configuration_error_message(&format!( "Unable to resolve a font containing glyphs for {}. \ A glyph from the built-in \"last resort\" font is being \ used instead.", notif.escape_unicode() )); } } /// Given a text style, load (with caching) the font that best /// matches according to the fontconfig pattern. fn resolve_font(&self, myself: &Rc, style: &TextStyle) -> anyhow::Result> { let config = self.config.borrow(); let mut fonts = self.fonts.borrow_mut(); 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::>(); let fallback_attributes = attributes .iter() .filter(|a| a.is_fallback) .map(|a| a.clone()) .collect::>(); 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(&*config, &handles)?; let font_size = config.font_size * *self.font_scale.borrow(); let dpi = *self.dpi_scale.borrow() as u32 * config.dpi.unwrap_or_else(|| default_dpi()) as u32; let metrics = shaper.metrics(font_size, dpi).with_context(|| { format!( "obtaining metrics for font_size={} @ dpi {}", 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) -> (f64, f64) { let prior_font = *self.font_scale.borrow(); let prior_dpi = *self.dpi_scale.borrow(); *self.dpi_scale.borrow_mut() = dpi_scale; *self.font_scale.borrow_mut() = font_scale; self.fonts.borrow_mut().clear(); self.metrics.borrow_mut().take(); (prior_font, prior_dpi) } /// Returns the baseline font specified in the configuration pub fn default_font(&self, myself: &Rc) -> anyhow::Result> { self.resolve_font(myself, &self.config.borrow().font) } pub fn get_font_scale(&self) -> f64 { *self.font_scale.borrow() } pub fn default_font_metrics(&self, myself: &Rc) -> Result { { 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(config: Option) -> anyhow::Result { let inner = Rc::new(FontConfigInner::new(config)?); Ok(Self { inner }) } pub fn config_changed(&self, config: &ConfigHandle) -> anyhow::Result<()> { self.inner.config_changed(config) } /// 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> { self.inner.resolve_font(&self.inner, style) } pub fn change_scaling(&self, font_scale: f64, dpi_scale: f64) -> (f64, f64) { self.inner.change_scaling(font_scale, dpi_scale) } /// Returns the baseline font specified in the configuration pub fn default_font(&self) -> anyhow::Result> { 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 { 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) } }