use cosmic_text::{FontSystem, fontdb};
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FontFamily(pub(crate) u32);
#[derive(Default)]
pub(crate) struct FontFamilies {
known: Vec<(String, Option<&'static str>)>,
names: Vec<&'static str>,
}
impl FontFamilies {
pub(crate) fn name(&self, family: FontFamily) -> Option<&'static str> {
self.names.get(family.0 as usize).copied()
}
pub(crate) fn resolve(&mut self, fonts: &mut FontSystem, requested: &str) -> Option<FontFamily> {
let requested = requested.trim();
if requested.is_empty() {
return None;
}
let found = match self.known.iter().find(|(name, _)| name.eq_ignore_ascii_case(requested)) {
Some((_, found)) => *found,
None => {
let found = loaded_family(fonts.db(), requested).or_else(|| {
load_matching_files(fonts.db_mut(), requested);
loaded_family(fonts.db(), requested)
});
let found: Option<&'static str> = found.map(|name| Box::leak(name.into_boxed_str()) as &str);
self.known.push((requested.to_owned(), found));
found
}
}?;
let index = match self.names.iter().position(|name| *name == found) {
Some(index) => index,
None => {
self.names.push(found);
self.names.len() - 1
}
};
Some(FontFamily(index as u32))
}
}
fn loaded_family(database: &fontdb::Database, requested: &str) -> Option<String> {
database
.faces()
.flat_map(|face| face.families.iter())
.find(|(name, _)| name.eq_ignore_ascii_case(requested))
.map(|(name, _)| name.clone())
}
fn normalized(text: &str) -> String {
text.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect()
}
fn font_directories() -> Vec<PathBuf> {
let home = crate::core::platform::home_dir();
let mut directories: Vec<PathBuf> = if cfg!(target_os = "macos") {
["/System/Library/Fonts", "/System/Library/Fonts/Supplemental", "/Library/Fonts"].map(PathBuf::from).into()
} else if cfg!(target_os = "ios") {
["/System/Library/Fonts/Core", "/System/Library/Fonts/CoreUI"].map(crate::gpu::fonts::system_path).into()
} else if cfg!(target_os = "android") {
vec![PathBuf::from("/system/fonts")]
} else if cfg!(windows) {
let windir = std::env::var_os("WINDIR").map_or_else(|| PathBuf::from("C:\\Windows"), PathBuf::from);
let per_user = std::env::var_os("LOCALAPPDATA").map(|base| PathBuf::from(base).join("Microsoft/Windows/Fonts"));
std::iter::once(windir.join("Fonts")).chain(per_user).collect()
} else {
vec![PathBuf::from("/usr/share/fonts"), PathBuf::from("/usr/local/share/fonts")]
};
if let Some(home) = home {
directories.extend(["Library/Fonts", ".local/share/fonts", ".fonts"].map(|path| home.join(path)));
}
directories
}
fn load_matching_files(database: &mut fontdb::Database, requested: &str) {
let wanted = normalized(requested);
let mut pending: Vec<(PathBuf, u8)> = font_directories().into_iter().map(|path| (path, 0)).collect();
while let Some((directory, depth)) = pending.pop() {
let Ok(entries) = std::fs::read_dir(&directory) else { continue };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if depth < 2 {
pending.push((path, depth + 1));
}
} else if is_font_file(&path)
&& file_matches(&path, &wanted)
&& let Err(error) = database.load_font_file(&path)
{
eprintln!("no se pudo cargar la fuente {}: {error}", path.display());
}
}
}
}
fn is_font_file(path: &Path) -> bool {
let extension = path.extension().and_then(|extension| extension.to_str()).unwrap_or_default();
matches!(extension.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")
}
fn file_matches(path: &Path, wanted: &str) -> bool {
let stem = path.file_stem().and_then(|stem| stem.to_str()).unwrap_or_default();
!wanted.is_empty() && normalized(stem).starts_with(wanted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn el_nombre_del_archivo_se_compara_sin_espacios_ni_mayusculas() {
let wanted = normalized("JetBrains Mono");
assert!(file_matches(Path::new("/f/JetBrainsMono-Regular.ttf"), &wanted));
assert!(!file_matches(Path::new("/f/Menlo.ttc"), &wanted));
assert!(!file_matches(Path::new("/f/Menlo.ttc"), ""));
assert!(is_font_file(Path::new("a.TTF")) && !is_font_file(Path::new("a.txt")));
}
#[test]
fn una_familia_desconocida_no_se_registra_y_se_recuerda() {
let mut fonts = crate::gpu::fonts::build_font_system();
let mut families = FontFamilies::default();
assert_eq!(families.resolve(&mut fonts, " "), None);
assert_eq!(families.resolve(&mut fonts, "Fuente Que No Existe 123"), None);
assert_eq!(families.known.len(), 1, "el fallo se recuerda para no volver a recorrer carpetas");
let loaded = fonts.db().faces().next().and_then(|face| face.families.first()).map(|(name, _)| name.clone());
if let Some(name) = loaded {
let family = families.resolve(&mut fonts, &name.to_uppercase()).expect("familia cargada");
assert_eq!(families.name(family), Some(name.as_str()));
assert_eq!(families.resolve(&mut fonts, &name), Some(family));
}
}
}