// SPDX-License-Identifier: MIT OR Apache-2.0 use std::{ collections::HashMap, sync::{Arc, Mutex}, }; use crate::{Attrs, AttrsOwned, Font, FontMatches}; #[ouroboros::self_referencing] struct FontSystemInner { locale: String, db: fontdb::Database, #[borrows(db)] #[not_covariant] font_cache: Mutex>>>>, #[borrows(locale, db)] #[not_covariant] font_matches_cache: Mutex>>>, } /// Access system fonts pub struct FontSystem(FontSystemInner); impl FontSystem { /// Create a new [`FontSystem`], that allows access to any installed system fonts pub fn new() -> Self { let locale = sys_locale::get_locale().unwrap_or_else(|| { log::warn!("failed to get system locale, falling back to en-US"); String::from("en-US") }); log::info!("Locale: {}", locale); let mut db = fontdb::Database::new(); { let now = std::time::Instant::now(); db.load_system_fonts(); //TODO: configurable default fonts db.set_monospace_family("Fira Mono"); db.set_sans_serif_family("Fira Sans"); db.set_serif_family("DejaVu Serif"); log::info!( "Parsed {} font faces in {}ms.", db.len(), now.elapsed().as_millis() ); } Self::new_with_locale_and_db(&locale, db) } /// Create a new [`FontSystem`], manually specifying the current locale and font database. pub fn new_with_locale_and_db(locale: &str, mut db: fontdb::Database) -> Self { { let now = std::time::Instant::now(); //TODO only do this on demand! for i in 0..db.faces().len() { let id = db.faces()[i].id; unsafe { db.make_shared_face_data(id); } } log::info!( "Mapped {} font faces in {}ms.", db.len(), now.elapsed().as_millis() ); } Self(FontSystemInnerBuilder { locale, db, font_cache_builder: |_| Mutex::new(HashMap::new()), font_matches_cache_builder: |_, _| Mutex::new(HashMap::new()) }.build()) } pub fn locale(&self) -> &str { self.0.borrow_locale() } pub fn db(&self) -> &fontdb::Database { self.0.borrow_db() } // Clippy false positive #[allow(clippy::needless_lifetimes)] pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option>> { self.0.with(|fields| { get_font(&fields, id) }) } pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc> { self.0.with(|fields| { let mut font_matches_cache = fields.font_matches_cache.lock().expect("failed to lock font matches cache"); //TODO: do not create AttrsOwned unless entry does not already exist font_matches_cache.entry(AttrsOwned::new(attrs)).or_insert_with(|| { let now = std::time::Instant::now(); let mut fonts = Vec::new(); for face in fields.db.faces() { if !attrs.matches(face) { continue; } if let Some(font) = get_font(&fields, face.id) { fonts.push(font); } } let font_matches = Arc::new(FontMatches { locale: fields.locale, default_family: fields.db.family_name(&attrs.family).to_string(), fonts }); let elapsed = now.elapsed(); log::debug!("font matches for {:?} in {:?}", attrs, elapsed); font_matches }).clone() }) } } fn get_font<'b>(fields: &ouroboros_impl_font_system_inner::BorrowedFields<'_, 'b>, id: fontdb::ID) -> Option>> { fields.font_cache.lock().expect("failed to lock font cache").entry(id).or_insert_with(|| { let face = fields.db.face(id)?; match Font::new(face) { Some(font) => Some(Arc::new(font)), None => { log::warn!("failed to load font '{}'", face.post_script_name); None } } }).clone() }