use crate::compat::{OnceLock, RwLock, String, ToString, Vec};
use crate::platform::SystemFont;
use alloc::boxed::Box;
use core::sync::atomic::{AtomicBool, Ordering};
static SNAPSHOT: OnceLock<RwLock<Option<&'static [SystemFont]>>> = OnceLock::new();
static WARMING: AtomicBool = AtomicBool::new(false);
fn slot() -> &'static RwLock<Option<&'static [SystemFont]>> {
SNAPSHOT.get_or_init(|| RwLock::new(None))
}
fn with_snapshot<R>(f: impl FnOnce(Option<&'static [SystemFont]>) -> R) -> R {
#[cfg(not(alloc_frugal))]
{
let guard = slot().read().unwrap_or_else(|poisoned| poisoned.into_inner());
f(*guard)
}
#[cfg(alloc_frugal)]
{
f(*slot().read())
}
}
fn with_snapshot_mut<R>(f: impl FnOnce(&mut Option<&'static [SystemFont]>) -> R) -> R {
#[cfg(not(alloc_frugal))]
{
let mut guard = slot().write().unwrap_or_else(|poisoned| poisoned.into_inner());
f(&mut guard)
}
#[cfg(alloc_frugal)]
{
let mut guard = slot().write();
f(&mut guard)
}
}
fn read_snapshot() -> Option<&'static [SystemFont]> {
with_snapshot(|snapshot| snapshot)
}
pub fn system_fonts() -> &'static [SystemFont] {
if let Some(fonts) = read_snapshot() {
return fonts;
}
let fresh: &'static [SystemFont] =
Box::leak(crate::platform::platform_facts().system_fonts().into_boxed_slice());
with_snapshot_mut(|slot| match *slot {
Some(existing) => existing,
None => {
*slot = Some(fresh);
fresh
}
})
}
pub fn cached_system_fonts() -> Option<&'static [SystemFont]> {
read_snapshot()
}
pub fn cached_system_font_count() -> Option<usize> {
read_snapshot().map(<[SystemFont]>::len)
}
pub fn prewarm_system_fonts() {
if read_snapshot().is_some() {
return;
}
if WARMING.swap(true, Ordering::SeqCst) {
return;
}
let spawned = std::thread::Builder::new().name("system-fonts".to_string()).spawn(|| {
system_fonts();
WARMING.store(false, Ordering::SeqCst);
});
if let Err(e) = spawned {
WARMING.store(false, Ordering::SeqCst);
log::warn!("[fonts] failed to start the system-font warm-up worker: {e}");
}
}
pub fn system_font_count_nonblocking() -> usize {
match cached_system_font_count() {
Some(count) => count,
None => {
prewarm_system_fonts();
0
}
}
}
pub fn refresh_system_fonts() -> &'static [SystemFont] {
let fresh = crate::platform::platform_facts().system_fonts();
with_snapshot_mut(|slot| {
if let Some(existing) = *slot {
if existing == fresh.as_slice() {
return existing;
}
}
let leaked: &'static [SystemFont] = Box::leak(fresh.into_boxed_slice());
*slot = Some(leaked);
leaked
})
}
pub fn families() -> Vec<String> {
let mut families: Vec<String> = system_fonts()
.iter()
.map(|font| font.family.trim().to_string())
.filter(|family| !family.is_empty())
.collect();
families.sort_unstable_by_key(|family| family.to_lowercase());
families.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
families
}
#[cfg(feature = "text-shaping")]
pub fn bundled_families() -> Vec<String> {
let mut families: Vec<String> = crate::render::text::font_assets::active_faces()
.iter()
.map(|face| face.name.trim().to_string())
.filter(|family| !family.is_empty())
.collect();
families.sort_unstable_by_key(|family| family.to_lowercase());
families.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
families
}
pub fn chooser_families() -> Vec<String> {
let system = families();
if !system.is_empty() {
return system;
}
#[cfg(feature = "text-shaping")]
{
bundled_families()
}
#[cfg(not(feature = "text-shaping"))]
{
Vec::new()
}
}
#[cfg(all(feature = "runtime-fonts", not(alloc_frugal)))]
pub fn register_family(family: &str) -> bool {
let (faces, len) = crate::render::text::runtime_fonts::active_runtime_faces();
if faces[..len].iter().any(|face| face.name.eq_ignore_ascii_case(family.trim())) {
return true;
}
if crate::render::text::registered_face_count() >= crate::render::text::MAX_RUNTIME_FACES {
return false;
}
let Some(font) =
system_fonts().iter().find(|font| font.family.eq_ignore_ascii_case(family.trim()))
else {
return false;
};
let Some(path) = font.source.as_ref() else {
return false;
};
let Ok(bytes) = std::fs::read(path) else {
return false;
};
let name: &'static str = Box::leak(font.family.clone().into_boxed_str());
let bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice());
crate::render::text::register_face(name, bytes)
}
#[cfg(all(feature = "runtime-fonts", not(alloc_frugal)))]
pub const SYSTEM_CJK_FAMILIES: &[&str] = &[
"Noto Sans CJK SC",
"Noto Sans SC",
"Source Han Sans SC",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"WenQuanYi Micro Hei",
"Droid Sans Fallback",
];
#[cfg(all(feature = "runtime-fonts", not(alloc_frugal)))]
pub fn register_system_cjk_font() -> Option<String> {
SYSTEM_CJK_FAMILIES.iter().find(|family| register_family(family)).map(|family| (*family).into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_refresh_with_no_change_reuses_the_installed_snapshot() {
let first = refresh_system_fonts();
let second = refresh_system_fonts();
assert!(core::ptr::eq(first, second), "an unchanged refresh leaked a second snapshot");
assert_eq!(second, system_fonts(), "the refresh must install what it returns");
}
#[test]
fn families_are_sorted_and_have_no_blank_or_duplicate_entries() {
let families = families();
assert!(families.windows(2).all(|pair| pair[0].to_lowercase() <= pair[1].to_lowercase()));
assert!(families.iter().all(|family| !family.trim().is_empty()));
let mut unique = families.clone();
unique.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
assert_eq!(unique.len(), families.len());
}
#[cfg(all(feature = "runtime-fonts", not(alloc_frugal)))]
#[test]
fn an_unknown_family_is_not_registered() {
assert!(!register_family("This Family Does Not Exist 12345"));
}
#[cfg(all(feature = "runtime-fonts", not(alloc_frugal)))]
#[test]
fn a_reported_cjk_registration_is_actually_in_the_registry() {
let _guard = crate::render::text::runtime_fonts::test_lock();
let registered = register_system_cjk_font();
let Some(family) = registered else {
return;
};
let (faces, len) = crate::render::text::runtime_fonts::active_runtime_faces();
assert!(
faces[..len].iter().any(|face| face.name.eq_ignore_ascii_case(&family)),
"reported `{family}` but the registry holds {:?}",
faces[..len].iter().map(|face| face.name).collect::<Vec<_>>()
);
let face = crate::render::text::font_assets::face_for_char('窗')
.expect("a registered CJK face must cover U+7A97");
assert!(
face.name.eq_ignore_ascii_case(&family),
"U+7A97 resolved to `{}`, not the registered `{family}`",
face.name
);
crate::render::text::clear_faces();
}
#[cfg(feature = "text-shaping")]
#[test]
fn bundled_families_are_sorted_and_blank_free() {
let families = bundled_families();
assert!(families.windows(2).all(|pair| pair[0].to_lowercase() <= pair[1].to_lowercase()));
assert!(families.iter().all(|family| !family.trim().is_empty()));
}
}