use std::path::Path;
use eframe::egui::{self, FontData, FontDefinitions, FontFamily};
const CANDIDATES: &[&str] = &[
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf",
"/Library/Fonts/Menlo.ttc",
"/System/Library/Fonts/Menlo.ttc",
"C:\\Windows\\Fonts\\consola.ttf",
];
pub struct Face {
pub name: String,
pub bytes: Vec<u8>,
pub index: u32,
}
pub fn install(ctx: &egui::Context, spec: Option<&str>) -> Option<String> {
let face = load(spec)?;
let name = face.name.clone();
let mut fonts = FontDefinitions::default();
let data = FontData {
index: face.index,
..FontData::from_owned(face.bytes)
};
fonts.font_data.insert(name.clone(), data.into());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.insert(0, name.clone());
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, name.clone());
ctx.set_fonts(fonts);
Some(name)
}
pub fn load(spec: Option<&str>) -> Option<Face> {
let spec = spec.map(str::trim).filter(|s| !s.is_empty());
if let Some(spec) = spec {
if let Some(face) = from_file(Path::new(spec)) {
return Some(face);
}
if let Some(face) = by_family(spec) {
return Some(face);
}
}
CANDIDATES
.iter()
.find_map(|path| from_file(Path::new(path)))
}
fn from_file(path: &Path) -> Option<Face> {
let bytes = std::fs::read(path).ok()?;
let name = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "system-mono".to_string());
Some(Face {
name,
bytes,
index: 0,
})
}
fn by_family(family: &str) -> Option<Face> {
let db = system_fonts();
let query = fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight: fontdb::Weight::NORMAL,
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
};
let id = db.query(&query)?;
let info = db.face(id)?;
if !info
.families
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(family))
{
return None;
}
let name = info.post_script_name.clone();
let index = info.index;
let bytes = db.with_face_data(id, |data, _| data.to_vec())?;
Some(Face { name, bytes, index })
}
pub fn families() -> Vec<String> {
let db = system_fonts();
let mut names: Vec<String> = db
.faces()
.filter(|face| face.monospaced)
.filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
.collect();
names.sort_by_key(|name| name.to_lowercase());
names.dedup();
names
}
fn system_fonts() -> fontdb::Database {
let mut db = fontdb::Database::new();
db.load_system_fonts();
db
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_override_that_is_not_there_falls_through() {
let _ = load(Some("/nonexistent/font/that/is/not/here.ttf"));
let _ = load(Some("No Such Family 3f9a"));
assert!(by_family("No Such Family 3f9a").is_none());
}
#[test]
fn a_listed_family_loads() {
let names = families();
let Some(name) = names.first() else {
return; };
let face = by_family(name).expect("listed family loads");
assert!(!face.bytes.is_empty());
assert!(names.windows(2).all(|w| w[0] != w[1]), "deduped");
}
}