use alloc::collections::btree_map::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::time::{Duration, Instant};
use crate::{
expand_font_families, FcFontCache, FcFontPath, FcParseFontBytes, FcPattern, FcWeight,
FontFallbackChain, FontId, FontMatch, NamedFont, OperatingSystem, PatternMatch,
};
use crate::scoring::{
family_exists_in_patterns, find_family_paths, find_incomplete_paths,
FcBuildJob, Priority,
};
use crate::utils::normalize_family_name;
pub struct FcFontRegistry {
pub cache: RwLock<FcFontCache>,
pub known_paths: RwLock<BTreeMap<String, Vec<PathBuf>>>,
pub build_queue: Mutex<Vec<FcBuildJob>>,
pub queue_condvar: Condvar,
pub processed_paths: Mutex<HashSet<PathBuf>>,
pub completed_paths: Mutex<HashSet<PathBuf>>,
pub progress: Condvar,
pub scan_complete: AtomicBool,
pub build_complete: AtomicBool,
pub shutdown: AtomicBool,
pub cache_loaded: AtomicBool,
pub os: OperatingSystem,
}
impl std::fmt::Debug for FcFontRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FcFontRegistry")
.field("scan_complete", &self.scan_complete.load(Ordering::Relaxed))
.field("build_complete", &self.build_complete.load(Ordering::Relaxed))
.field("cache_loaded", &self.cache_loaded.load(Ordering::Relaxed))
.finish()
}
}
impl FcFontRegistry {
pub fn new() -> Arc<Self> {
Arc::new(Self {
cache: RwLock::new(FcFontCache::default()),
known_paths: RwLock::new(BTreeMap::new()),
build_queue: Mutex::new(Vec::new()),
queue_condvar: Condvar::new(),
processed_paths: Mutex::new(HashSet::new()),
completed_paths: Mutex::new(HashSet::new()),
progress: Condvar::new(),
scan_complete: AtomicBool::new(false),
build_complete: AtomicBool::new(false),
shutdown: AtomicBool::new(false),
cache_loaded: AtomicBool::new(false),
os: OperatingSystem::current(),
})
}
pub fn register_memory_fonts(&self, fonts: Vec<NamedFont>) {
for named_font in fonts {
let Some(parsed) = FcParseFontBytes(&named_font.bytes, &named_font.name) else {
continue;
};
let Ok(mut cache) = self.cache.write() else { continue };
cache.with_memory_fonts(parsed);
}
}
pub fn spawn_scout_and_builders(self: &Arc<Self>) {
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
.saturating_sub(1)
.max(1);
let registry = Arc::clone(self);
std::thread::Builder::new()
.name("rfc-font-scout".to_string())
.spawn(move || {
registry.scout_thread();
})
.expect("failed to spawn font scout thread");
for i in 0..num_threads {
let registry = Arc::clone(self);
std::thread::Builder::new()
.name(format!("rfc-font-builder-{}", i))
.spawn(move || {
registry.builder_thread();
})
.expect("failed to spawn font builder thread");
}
}
pub fn request_fonts(
&self,
family_stacks: &[Vec<String>],
) -> Vec<FontFallbackChain> {
let deadline = Instant::now() + Duration::from_secs(5);
let mut needed_families: Vec<String> = Vec::new();
let mut expanded_stacks: Vec<Vec<String>> = Vec::new();
for stack in family_stacks {
let expanded = expand_font_families(stack, self.os, &[]);
for family in &expanded {
let normalized = normalize_family_name(family);
if !needed_families.contains(&normalized) {
needed_families.push(normalized);
}
}
expanded_stacks.push(expanded);
}
if self.cache_loaded.load(Ordering::Acquire) {
return self.resolve_chains(&expanded_stacks);
}
if !self.scan_complete.load(Ordering::Acquire) {
let Ok(mut completed) = self.completed_paths.lock() else {
return self.resolve_chains(&expanded_stacks);
};
while !self.scan_complete.load(Ordering::Acquire) {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
eprintln!(
"[rfc-font-registry] WARNING: Timed out waiting for font scout (5s). \
Proceeding with available fonts."
);
return self.resolve_chains(&expanded_stacks);
}
completed = match self.progress.wait_timeout(completed, remaining) {
Ok((c, _)) => c,
Err(_) => return self.resolve_chains(&expanded_stacks),
};
}
}
let missing: Vec<String> = self.cache.read()
.map(|cache| {
needed_families
.iter()
.filter(|fam| !family_exists_in_patterns(fam, cache.patterns.keys()))
.cloned()
.collect()
})
.unwrap_or_else(|_| needed_families.clone());
let incomplete_paths = self.known_paths.read().ok()
.zip(self.completed_paths.lock().ok())
.map(|(known, completed)| find_incomplete_paths(&needed_families, &known, &completed))
.unwrap_or_default();
if missing.is_empty() && incomplete_paths.is_empty() {
return self.resolve_chains(&expanded_stacks);
}
let wait_paths: HashSet<PathBuf> = if let (Ok(known_paths), Ok(mut queue)) =
(self.known_paths.read(), self.build_queue.lock())
{
let missing_paths: Vec<_> = missing
.iter()
.flat_map(|fam| {
find_family_paths(fam, &known_paths)
.into_iter()
.map(move |p| (p, fam.clone()))
})
.collect();
for (path, family) in missing_paths.iter().chain(incomplete_paths.iter()) {
queue.push(FcBuildJob {
priority: Priority::Critical,
path: path.clone(),
font_index: None,
guessed_family: family.clone(),
});
}
queue.sort();
missing_paths
.iter()
.chain(incomplete_paths.iter())
.map(|(p, _)| p.clone())
.collect()
} else {
incomplete_paths.iter().map(|(p, _)| p.clone()).collect()
};
self.queue_condvar.notify_all();
if !wait_paths.is_empty() {
let Ok(mut completed) = self.completed_paths.lock() else {
return self.resolve_chains(&expanded_stacks);
};
loop {
if wait_paths.iter().all(|p| completed.contains(p)) {
break;
}
if self.build_complete.load(Ordering::Acquire) {
break;
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
eprintln!(
"[rfc-font-registry] WARNING: Timed out waiting for font files (5s). \
Proceeding with available fonts."
);
break;
}
completed = match self.progress.wait_timeout(completed, remaining) {
Ok((c, _)) => c,
Err(_) => break,
};
}
}
self.resolve_chains(&expanded_stacks)
}
pub fn get_metadata_by_id(&self, id: &FontId) -> Option<FcPattern> {
self.cache.read().ok()?.metadata.get(id).cloned()
}
pub fn get_font_bytes(&self, id: &FontId) -> Option<Vec<u8>> {
self.cache.read().ok()?.get_font_bytes(id)
}
pub fn get_disk_font_path(&self, id: &FontId) -> Option<FcFontPath> {
self.cache.read().ok()?.disk_fonts.get(id).cloned()
}
pub fn is_memory_font(&self, id: &FontId) -> bool {
self.cache.read().ok()
.map(|c| c.is_memory_font(id))
.unwrap_or(false)
}
pub fn list(&self) -> Vec<(FcPattern, FontId)> {
self.cache.read().ok()
.map(|c| c.list().into_iter().map(|(p, id)| (p.clone(), id)).collect())
.unwrap_or_default()
}
pub fn query(&self, pattern: &FcPattern) -> Option<FontMatch> {
let cache = self.cache.read().ok()?;
let mut trace = Vec::new();
cache.query(pattern, &mut trace)
}
pub fn resolve_font_chain(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
) -> FontFallbackChain {
let Ok(cache) = self.cache.read() else {
return FontFallbackChain {
css_fallbacks: Vec::new(),
unicode_fallbacks: Vec::new(),
original_stack: font_families.to_vec(),
};
};
let mut trace = Vec::new();
cache.resolve_font_chain_with_os(
font_families, weight, italic, oblique, &mut trace, self.os,
)
}
pub fn into_fc_font_cache(&self) -> FcFontCache {
self.cache.read()
.map(|c| c.clone())
.unwrap_or_default()
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Release);
self.queue_condvar.notify_all();
self.progress.notify_all();
}
pub fn is_scan_complete(&self) -> bool {
self.scan_complete.load(Ordering::Acquire)
}
pub fn is_build_complete(&self) -> bool {
self.build_complete.load(Ordering::Acquire)
}
pub fn is_cache_loaded(&self) -> bool {
self.cache_loaded.load(Ordering::Acquire)
}
pub fn insert_font(&self, pattern: FcPattern, path: FcFontPath) {
let Ok(mut cache) = self.cache.write() else { return };
let id = FontId::new();
cache.index_pattern_tokens(&pattern, id);
cache.patterns.insert(pattern.clone(), id);
cache.disk_fonts.insert(id, path);
cache.metadata.insert(id, pattern);
let _ = cache.chain_cache.lock().map(|mut cc| cc.clear());
}
fn resolve_chains(&self, expanded_stacks: &[Vec<String>]) -> Vec<FontFallbackChain> {
expanded_stacks
.iter()
.map(|stack| {
self.resolve_font_chain(
stack,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
)
})
.collect()
}
}
impl Drop for FcFontRegistry {
fn drop(&mut self) {
self.shutdown();
}
}