use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum FontSource {
Builtin(&'static str),
System(String),
File(PathBuf),
Bytes(&'static [u8]),
}
impl FontSource {
pub fn system(name: impl Into<String>) -> Self {
FontSource::System(name.into())
}
pub fn file(path: impl Into<PathBuf>) -> Self {
FontSource::File(path.into())
}
pub fn bytes(data: &'static [u8]) -> Self {
FontSource::Bytes(data)
}
}
#[derive(Debug, Clone, Default)]
pub struct FontConfig {
pub default_font: Option<String>,
pub code_font: Option<String>,
pub default_font_source: Option<FontSource>,
pub code_font_source: Option<FontSource>,
pub enable_subsetting: bool,
}
impl FontConfig {
pub fn new() -> Self {
Self {
default_font: None,
code_font: None,
default_font_source: None,
code_font_source: None,
enable_subsetting: true,
}
}
pub fn with_default_font(mut self, font: impl Into<String>) -> Self {
self.default_font = Some(font.into());
self
}
pub fn with_code_font(mut self, font: impl Into<String>) -> Self {
self.code_font = Some(font.into());
self
}
pub fn with_default_font_source(mut self, source: FontSource) -> Self {
self.default_font_source = Some(source);
self
}
pub fn with_code_font_source(mut self, source: FontSource) -> Self {
self.code_font_source = Some(source);
self
}
pub fn with_subsetting(mut self, enabled: bool) -> Self {
self.enable_subsetting = enabled;
self
}
}
pub fn is_builtin_font_name(name: &str) -> bool {
matches!(
name.to_lowercase().as_str(),
"helvetica"
| "arial"
| "sans-serif"
| "times"
| "times new roman"
| "serif"
| "courier"
| "courier new"
| "monospace"
)
}
pub fn resolve_font_source(name: &str) -> FontSource {
if is_builtin_font_name(name) {
return FontSource::Builtin(match name.to_lowercase().as_str() {
"helvetica" | "arial" | "sans-serif" => "Helvetica",
"times" | "times new roman" | "serif" => "Times",
"courier" | "courier new" | "monospace" => "Courier",
_ => "Helvetica",
});
}
if name.contains('/') || name.contains('\\') || name.ends_with(".ttf") || name.ends_with(".otf")
{
return FontSource::File(PathBuf::from(name));
}
FontSource::System(name.to_string())
}
pub fn system_font_dirs() -> Vec<&'static str> {
if cfg!(target_os = "macos") {
vec![
"/System/Library/Fonts",
"/System/Library/Fonts/Supplemental",
"/Library/Fonts",
]
} else if cfg!(target_os = "linux") {
vec![
"/usr/share/fonts/truetype",
"/usr/share/fonts/TTF",
"/usr/share/fonts/opentype",
"/usr/local/share/fonts",
]
} else if cfg!(target_os = "windows") {
vec!["C:\\Windows\\Fonts"]
} else {
vec![]
}
}
pub fn find_system_font(name: &str) -> Option<PathBuf> {
let name_lower = name.to_lowercase();
let patterns = [
format!("{}.ttf", name),
format!("{}.otf", name),
format!("{}.ttf", name.replace(" MS", "")),
];
for dir in system_font_dirs() {
let dir_path = Path::new(dir);
if !dir_path.exists() {
continue;
}
let Ok(entries) = std::fs::read_dir(dir_path) else {
continue;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let file_lower = file_name.to_string_lossy().to_lowercase();
if file_lower.ends_with(".ttc") {
continue;
}
if patterns.iter().any(|p| file_lower == p.to_lowercase()) {
return Some(entry.path());
}
if file_lower.starts_with(&name_lower)
&& (file_lower.ends_with(".ttf") || file_lower.ends_with(".otf"))
{
return Some(entry.path());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_name_recognized() {
assert!(is_builtin_font_name("Helvetica"));
assert!(is_builtin_font_name("helvetica"));
assert!(is_builtin_font_name("Times New Roman"));
assert!(is_builtin_font_name("courier"));
assert!(!is_builtin_font_name("Georgia"));
}
#[test]
fn resolve_builtin() {
assert!(matches!(
resolve_font_source("Helvetica"),
FontSource::Builtin("Helvetica")
));
assert!(matches!(
resolve_font_source("arial"),
FontSource::Builtin("Helvetica")
));
}
#[test]
fn resolve_path() {
assert!(matches!(
resolve_font_source("/some/path/font.ttf"),
FontSource::File(_)
));
assert!(matches!(
resolve_font_source("relative.otf"),
FontSource::File(_)
));
}
#[test]
fn resolve_system() {
assert!(matches!(
resolve_font_source("Georgia"),
FontSource::System(_)
));
}
#[test]
fn system_font_dirs_present() {
let _ = system_font_dirs();
}
}