use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FontWeight {
Light,
Normal,
Medium,
Bold,
Heavy,
}
impl FontWeight {
pub fn to_cairo_weight(self) -> cairo::FontWeight {
match self {
FontWeight::Light => cairo::FontWeight::Normal,
FontWeight::Normal => cairo::FontWeight::Normal,
FontWeight::Medium => cairo::FontWeight::Normal,
FontWeight::Bold => cairo::FontWeight::Bold,
FontWeight::Heavy => cairo::FontWeight::Bold,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FontSlant {
Normal,
Italic,
Oblique,
}
impl FontSlant {
pub fn to_cairo_slant(self) -> cairo::FontSlant {
match self {
FontSlant::Normal => cairo::FontSlant::Normal,
FontSlant::Italic => cairo::FontSlant::Italic,
FontSlant::Oblique => cairo::FontSlant::Oblique,
}
}
}
#[derive(Debug, Clone)]
pub struct FontDesc {
pub family: String,
pub weight: FontWeight,
pub slant: FontSlant,
pub size: f64,
}
impl FontDesc {
pub fn new(family: &str, size: f64) -> Self {
Self {
family: family.to_string(),
weight: FontWeight::Normal,
slant: FontSlant::Normal,
size,
}
}
pub fn with_weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
pub fn with_slant(mut self, slant: FontSlant) -> Self {
self.slant = slant;
self
}
}
pub struct FontManager {
font_dirs: Vec<PathBuf>,
font_cache: HashMap<String, Vec<PathBuf>>,
default_fonts: Vec<String>,
}
impl FontManager {
pub fn new() -> Result<Self> {
let mut manager = Self {
font_dirs: Vec::new(),
font_cache: HashMap::new(),
default_fonts: vec![
"DejaVu Sans".to_string(),
"Liberation Sans".to_string(),
"sans-serif".to_string(),
"Arial".to_string(),
],
};
manager.add_system_font_dirs()?;
manager.scan_fonts()?;
Ok(manager)
}
fn add_system_font_dirs(&mut self) -> Result<()> {
let system_dirs = [
"/usr/share/fonts",
"/usr/local/share/fonts",
"/opt/local/share/fonts",
"~/.fonts",
"~/.local/share/fonts",
];
for dir in &system_dirs {
let path = if dir.starts_with('~') {
if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home).join(&dir[2..])
} else {
continue;
}
} else {
PathBuf::from(dir)
};
if path.exists() && path.is_dir() {
self.font_dirs.push(path);
}
}
Ok(())
}
fn scan_fonts(&mut self) -> Result<()> {
let font_dirs = self.font_dirs.clone();
for font_dir in &font_dirs {
self.scan_directory(font_dir)?;
}
Ok(())
}
fn scan_directory(&mut self, dir: &Path) -> Result<()> {
if !dir.exists() || !dir.is_dir() {
return Ok(());
}
let entries = fs::read_dir(dir)
.with_context(|| format!("Failed to read font directory: {}", dir.display()))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
self.scan_directory(&path)?;
} else if self.is_font_file(&path) {
if let Some(family) = self.extract_font_family(&path) {
self.font_cache
.entry(family.to_lowercase())
.or_default()
.push(path);
}
}
}
Ok(())
}
fn is_font_file(&self, path: &Path) -> bool {
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
matches!(
ext.as_str(),
"ttf" | "otf" | "woff" | "woff2" | "pfb" | "pfa"
)
} else {
false
}
}
fn extract_font_family(&self, _path: &Path) -> Option<String> {
if let Some(stem) = _path.file_stem() {
let name = stem.to_string_lossy();
let family = name.split('-').next().unwrap_or(&name).replace('_', " ");
Some(family)
} else {
None
}
}
pub fn find_font(&self, family: &str) -> Option<&PathBuf> {
let family_lower = family.to_lowercase();
if let Some(fonts) = self.font_cache.get(&family_lower) {
return fonts.first();
}
for default_family in &self.default_fonts {
let default_lower = default_family.to_lowercase();
if let Some(fonts) = self.font_cache.get(&default_lower) {
return fonts.first();
}
}
None
}
pub fn get_available_families(&self) -> Vec<&String> {
self.font_cache.keys().collect()
}
pub fn has_font(&self, family: &str) -> bool {
self.font_cache.contains_key(&family.to_lowercase())
}
}
impl Default for FontManager {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
font_dirs: Vec::new(),
font_cache: HashMap::new(),
default_fonts: vec!["sans-serif".to_string()],
})
}
}