use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::OnceLock;
use fontdb::{Database, Family, Query, Source, Style, Weight};
use harfrust::{FontRef, ShaperData};
use crate::font::writing_system_index::{
FaceBytes, FaceRef, WritingSystemIndexBuilder, build_from_faces,
};
use crate::types::FontFaceId;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FontFamilyInfo {
pub name: String,
pub monospaced: bool,
}
pub type SharedFontData = Arc<dyn AsRef<[u8]> + Sync + Send>;
enum FontData {
Resident(SharedFontData),
Lazy {
path: PathBuf,
cell: OnceLock<SharedFontData>,
},
}
pub struct FontEntry {
pub fontdb_id: fontdb::ID,
pub face_index: u32,
data: FontData,
pub swash_cache_key: swash::CacheKey,
pub is_system: bool,
shaper_data: OnceLock<ShaperData>,
}
impl FontEntry {
pub fn bytes(&self) -> &[u8] {
match &self.data {
FontData::Resident(data) => (**data).as_ref(),
FontData::Lazy { path, cell } => {
let arc = cell.get_or_init(|| {
let bytes = std::fs::read(path).unwrap_or_default();
Arc::new(bytes) as SharedFontData
});
(**arc).as_ref()
}
}
}
pub fn shaper_data(&self, font: &FontRef) -> &ShaperData {
self.shaper_data.get_or_init(|| ShaperData::new(font))
}
}
pub struct FontRegistry {
fontdb: Database,
fonts: Vec<Option<FontEntry>>,
fontdb_index: HashMap<fontdb::ID, FontFaceId>,
generic_families: HashMap<String, String>,
default_font: Option<FontFaceId>,
default_size_px: f32,
}
impl Default for FontRegistry {
fn default() -> Self {
Self::new()
}
}
impl FontRegistry {
pub fn new() -> Self {
let mut registry = Self::new_without_system_fonts();
registry.load_system_fonts();
registry
}
pub fn new_without_system_fonts() -> Self {
Self {
fontdb: Database::new(),
fonts: Vec::new(),
fontdb_index: HashMap::new(),
generic_families: HashMap::new(),
default_font: None,
default_size_px: 16.0,
}
}
fn load_system_fonts(&mut self) {
self.fontdb.load_system_fonts();
let discovered: Vec<(fontdb::ID, u32, PathBuf)> = self
.fontdb
.faces()
.filter_map(|f| match &f.source {
Source::File(path) => Some((f.id, f.index, path.clone())),
Source::SharedFile(path, _) => Some((f.id, f.index, path.clone())),
Source::Binary(_) => None,
})
.collect();
for (fontdb_id, face_index, path) in discovered {
if self.fontdb_index.contains_key(&fontdb_id) {
continue;
}
let entry = FontEntry {
fontdb_id,
face_index,
data: FontData::Lazy {
path,
cell: OnceLock::new(),
},
swash_cache_key: swash::CacheKey::new(),
is_system: true,
shaper_data: OnceLock::new(),
};
let face_id = FontFaceId(self.fonts.len() as u32);
self.fonts.push(Some(entry));
self.fontdb_index.insert(fontdb_id, face_id);
}
}
pub fn register_font(&mut self, data: &[u8]) -> Vec<FontFaceId> {
let arc_data: SharedFontData = Arc::new(data.to_vec());
self.register_font_shared(arc_data)
}
pub fn register_font_shared(&mut self, data: SharedFontData) -> Vec<FontFaceId> {
let source = Source::Binary(data.clone());
let fontdb_ids = self.fontdb.load_font_source(source);
let mut face_ids = Vec::new();
for fontdb_id in fontdb_ids {
let face_index = self.fontdb.face(fontdb_id).map(|f| f.index).unwrap_or(0);
let swash_cache_key = swash::CacheKey::new();
let entry = FontEntry {
fontdb_id,
face_index,
data: FontData::Resident(data.clone()),
swash_cache_key,
is_system: false,
shaper_data: OnceLock::new(),
};
let face_id = FontFaceId(self.fonts.len() as u32);
self.fonts.push(Some(entry));
self.fontdb_index.insert(fontdb_id, face_id);
face_ids.push(face_id);
}
face_ids
}
pub fn register_font_as(
&mut self,
data: &[u8],
family: &str,
weight: u16,
italic: bool,
) -> Vec<FontFaceId> {
let arc_data: SharedFontData = Arc::new(data.to_vec());
self.register_font_shared_as(arc_data, family, weight, italic)
}
pub fn register_font_shared_as(
&mut self,
data: SharedFontData,
family: &str,
weight: u16,
italic: bool,
) -> Vec<FontFaceId> {
let source = Source::Binary(data.clone());
let fontdb_ids = self.fontdb.load_font_source(source);
let mut face_ids = Vec::new();
for fontdb_id in fontdb_ids {
if let Some(face_info) = self.fontdb.face(fontdb_id) {
let mut info = face_info.clone();
info.families = vec![(family.to_string(), fontdb::Language::English_UnitedStates)];
info.weight = Weight(weight);
info.style = if italic { Style::Italic } else { Style::Normal };
let face_index = info.index;
self.fontdb.remove_face(fontdb_id);
let new_id = self.fontdb.push_face_info(info);
let swash_cache_key = swash::CacheKey::new();
let entry = FontEntry {
fontdb_id: new_id,
face_index,
data: FontData::Resident(data.clone()),
swash_cache_key,
is_system: false,
shaper_data: OnceLock::new(),
};
let face_id = FontFaceId(self.fonts.len() as u32);
self.fonts.push(Some(entry));
self.fontdb_index.insert(new_id, face_id);
face_ids.push(face_id);
}
}
face_ids
}
pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
self.default_font = Some(face);
self.default_size_px = size_px;
}
pub fn default_font(&self) -> Option<FontFaceId> {
self.default_font
}
pub fn default_size_px(&self) -> f32 {
self.default_size_px
}
pub fn set_generic_family(&mut self, generic: &str, family: &str) {
self.generic_families
.insert(generic.to_string(), family.to_string());
}
pub fn resolve_family_name<'a>(&'a self, family: &'a str) -> &'a str {
self.generic_families
.get(family)
.map(|s| s.as_str())
.unwrap_or(family)
}
pub fn query_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontFaceId> {
let resolved = self.resolve_family_name(family);
let style = if italic { Style::Italic } else { Style::Normal };
let query = Query {
families: &[Family::Name(resolved)],
weight: Weight(weight),
style,
..Query::default()
};
let fontdb_id = self.fontdb.query(&query)?;
self.fontdb_id_to_face_id(fontdb_id)
}
pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
let entry = self.get(face_id)?;
let face_info = self.fontdb.face(entry.fontdb_id)?;
face_info.families.first().map(|(name, _)| name.clone())
}
pub fn get(&self, face_id: FontFaceId) -> Option<&FontEntry> {
self.fonts
.get(face_id.0 as usize)
.and_then(|opt| opt.as_ref())
}
pub fn query_variant(
&self,
base_face: FontFaceId,
weight: u16,
italic: bool,
) -> Option<FontFaceId> {
let entry = self.get(base_face)?;
let face_info = self.fontdb.face(entry.fontdb_id)?;
let family_name = face_info.families.first().map(|(name, _)| name.as_str())?;
self.query_font(family_name, weight, italic)
}
fn fontdb_id_to_face_id(&self, fontdb_id: fontdb::ID) -> Option<FontFaceId> {
self.fontdb_index.get(&fontdb_id).copied()
}
pub fn families(&self) -> Vec<FontFamilyInfo> {
let mut map: HashMap<String, (String, bool)> = HashMap::new();
for face in self.fontdb.faces() {
let Some((name, _)) = face.families.first() else {
continue;
};
let entry = map
.entry(name.to_lowercase())
.or_insert_with(|| (name.clone(), false));
entry.1 |= face.monospaced;
}
let mut out: Vec<FontFamilyInfo> = map
.into_values()
.map(|(name, monospaced)| FontFamilyInfo { name, monospaced })
.collect();
out.sort_by(|a, b| {
a.name
.to_lowercase()
.cmp(&b.name.to_lowercase())
.then_with(|| a.name.cmp(&b.name))
});
out
}
pub fn family_names(&self) -> Vec<String> {
self.families().into_iter().map(|f| f.name).collect()
}
pub fn family_is_monospaced(&self, family: &str) -> bool {
let target = family.to_lowercase();
self.fontdb.faces().any(|f| {
f.monospaced
&& f.families
.first()
.is_some_and(|(n, _)| n.to_lowercase() == target)
})
}
pub fn writing_system_index_builder(&self) -> WritingSystemIndexBuilder {
build_from_faces(self.fontdb.faces().filter_map(|face| {
let (family, _) = face.families.first()?;
let bytes = match &face.source {
Source::File(path) => FaceBytes::Path(path.clone()),
Source::SharedFile(_, data) => FaceBytes::Shared(data.clone()),
Source::Binary(data) => FaceBytes::Shared(data.clone()),
};
Some((
family.to_lowercase(),
FaceRef {
bytes,
index: face.index,
},
))
}))
}
pub fn all_entries(&self) -> impl Iterator<Item = (FontFaceId, &FontEntry)> {
self.fonts
.iter()
.enumerate()
.filter_map(|(i, opt)| opt.as_ref().map(|entry| (FontFaceId(i as u32), entry)))
}
}