use super::{
FallbackKey, FamilyId, FamilyInfo, FamilyNameMap, GenericFamily, GenericFamilyMap, ScriptExt,
scan,
};
use alloc::format;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::ptr::{null, null_mut};
use hashbrown::{HashMap, HashSet};
use objc2_core_foundation::{
CFArray, CFDictionary, CFRange, CFRetained, CFString, CFType, CFURL, CFURLPathStyle,
};
use objc2_core_text::{
CTFont, CTFontCollection, CTFontDescriptor, CTFontUIFontType, kCTFontURLAttribute,
};
use objc2_foundation::{
NSSearchPathDirectory, NSSearchPathDomainMask, NSSearchPathForDirectoriesInDomains,
};
use parlance::Script;
use std::path::{Path, PathBuf};
const DEFAULT_GENERIC_FAMILIES: &[(GenericFamily, &[&str])] = &[
(GenericFamily::Serif, &["Times", "Times New Roman"]),
(GenericFamily::SansSerif, &["Helvetica"]),
(GenericFamily::Monospace, &["Courier", "Courier New"]),
(GenericFamily::Cursive, &["Apple Chancery"]),
(GenericFamily::Fantasy, &["Papyrus"]),
(GenericFamily::SystemUi, &["System Font", ".SF NS"]),
(GenericFamily::Emoji, &["Apple Color Emoji"]),
(GenericFamily::Math, &["STIX Two Math"]),
];
pub(crate) struct SystemFonts {
pub(crate) name_map: Arc<FamilyNameMap>,
pub(crate) generic_families: Arc<GenericFamilyMap>,
family_map: HashMap<FamilyId, FamilyInfo>,
}
impl SystemFonts {
pub(crate) fn new() -> Self {
let scanned = scan_system_fonts().unwrap_or_default();
let name_map = scanned.family_names;
let mut generic_families = GenericFamilyMap::default();
for (family, names) in DEFAULT_GENERIC_FAMILIES {
generic_families.set(
*family,
names
.iter()
.filter_map(|name| name_map.get(name))
.map(|name| name.id()),
);
}
Self {
name_map: Arc::new(name_map),
generic_families: Arc::new(generic_families),
family_map: scanned.families,
}
}
pub(crate) fn family(&mut self, id: FamilyId) -> Option<FamilyInfo> {
self.family_map.get(&id).cloned()
}
pub(crate) fn fallback(&mut self, key: impl Into<FallbackKey>) -> Option<FamilyId> {
const HANI: Script = Script::from_bytes(*b"Hani");
const HANT: Script = Script::from_bytes(*b"Hant");
const HANS: Script = Script::from_bytes(*b"Hans");
let key = key.into();
let script = key.script();
let font = create_fallback_font_for_text(script.sample()?, key.locale_str(), false)?;
let family_name = unsafe { font.family_name() };
if let Some(family) = self.name_map.get(&family_name.to_string()) {
return Some(family.id());
}
let name = match script {
HANI | HANS => "Heiti SC",
HANT => "Heiti TC",
_ => return None,
};
self.name_map.get(name).map(|family| family.id())
}
}
fn scan_system_fonts() -> Option<scan::ScannedCollection> {
let collection = unsafe { CTFontCollection::from_available_fonts(None) };
let descriptors = unsafe { collection.matching_font_descriptors()? };
let descriptors: CFRetained<CFArray<CTFontDescriptor>> =
unsafe { CFRetained::cast_unchecked(descriptors) };
let mut paths: HashSet<PathBuf> = HashSet::new();
for index in 0..descriptors.len() {
let Some(descriptor) = descriptors.get(index) else {
continue;
};
let Some(url_cf): Option<CFRetained<CFType>> =
(unsafe { descriptor.attribute(kCTFontURLAttribute) })
else {
continue;
};
let Ok(url_cf): Result<CFRetained<CFURL>, _> = url_cf.downcast::<CFURL>() else {
continue;
};
let Some(path_cf): Option<CFRetained<CFString>> =
url_cf.file_system_path(CFURLPathStyle::CFURLPOSIXPathStyle)
else {
continue;
};
let path = PathBuf::from(path_cf.to_string());
if path.exists() {
paths.insert(path);
}
}
paths.extend(library_font_files());
if paths.is_empty() {
return None;
}
Some(scan::ScannedCollection::from_paths(paths.iter(), 0))
}
fn library_font_files() -> Vec<PathBuf> {
let mut files = Vec::new();
for dir in NSSearchPathForDirectoriesInDomains(
NSSearchPathDirectory::LibraryDirectory,
NSSearchPathDomainMask::AllDomainsMask,
true,
) {
let font_dir = PathBuf::from(format!("{dir}/Fonts"));
if font_dir.is_dir() {
collect_files(&font_dir, 8, 0, &mut files);
}
}
files
}
fn collect_files(dir: &Path, max_depth: u32, depth: u32, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
if depth < max_depth {
collect_files(&path, max_depth, depth + 1, out);
}
} else {
out.push(path);
}
}
}
fn create_base_font(prefer_ui_font: bool) -> CFRetained<CTFont> {
if prefer_ui_font {
if let Some(font) =
unsafe { CTFont::new_ui_font_for_language(CTFontUIFontType::System, 0.0, None) }
{
return font;
}
}
unsafe {
let attrs = CFDictionary::new(None, null_mut(), null_mut(), 0, null(), null());
let desc = CTFontDescriptor::with_attributes(&attrs.unwrap());
CTFont::with_font_descriptor(&desc, 0.0, null())
}
}
fn create_fallback_font_for_text(
text: &str,
locale: Option<&str>,
prefer_ui_font: bool,
) -> Option<CFRetained<CTFont>> {
let text = CFString::from_str(text);
let text_range = CFRange {
location: 0,
length: text.length(),
};
let locale = locale.map(CFString::from_str);
let base_font = create_base_font(prefer_ui_font);
let font = unsafe {
if let Some(locale) = locale {
CTFont::for_string_with_language(&base_font, &text, text_range, Some(&locale))
} else {
CTFont::for_string(&base_font, &text, text_range)
}
};
Some(font)
}