use parley::fontique::{Collection, FallbackKey, FamilyId, FontInfo, GenericFamily, Script};
#[derive(Default)]
pub(super) struct ResourceFontFamilies {
generic: Vec<FamilyId>,
hani_simplified: Vec<FamilyId>,
hani_traditional: Vec<FamilyId>,
hani_japanese: Vec<FamilyId>,
hani_korean: Vec<FamilyId>,
arabic: Vec<FamilyId>,
hebrew: Vec<FamilyId>,
}
fn extend_family_ids(target: &mut Vec<FamilyId>, families: &[(FamilyId, Vec<FontInfo>)]) {
target.extend(families.iter().map(|(family_id, _)| *family_id));
}
fn set_fallbacks(collection: &mut Collection, key: impl Into<FallbackKey>, families: &[FamilyId]) {
if families.is_empty() {
return;
}
assert!(
collection.set_fallbacks(key, families.iter().copied()),
"hydrolysis font loader attempted to install an untracked script fallback"
);
}
impl ResourceFontFamilies {
pub(super) fn classify(&mut self, name: &str, families: &[(FamilyId, Vec<FontInfo>)]) {
let key = name.to_ascii_lowercase().replace(' ', "");
if key.contains("roboto") {
extend_family_ids(&mut self.generic, families);
} else if key.contains("notosanscjksc") {
extend_family_ids(&mut self.hani_simplified, families);
} else if key.contains("notosanscjktc") {
extend_family_ids(&mut self.hani_traditional, families);
} else if key.contains("notosanscjkjp") {
extend_family_ids(&mut self.hani_japanese, families);
} else if key.contains("notosanscjkkr") {
extend_family_ids(&mut self.hani_korean, families);
} else if key.contains("notosansarabic") {
extend_family_ids(&mut self.arabic, families);
} else if key.contains("notosanshebrew") {
extend_family_ids(&mut self.hebrew, families);
}
}
pub(super) fn install(&self, collection: &mut Collection) {
if !self.generic.is_empty() {
collection.set_generic_families(GenericFamily::SansSerif, self.generic.iter().copied());
collection
.set_generic_families(GenericFamily::UiSansSerif, self.generic.iter().copied());
collection.set_generic_families(GenericFamily::SystemUi, self.generic.iter().copied());
}
let hani = Script::from_str_unchecked("Hani");
set_fallbacks(collection, hani, &self.hani_simplified);
for locale in ["zh", "zh-CN", "zh-SG"] {
set_fallbacks(collection, (hani, locale), &self.hani_simplified);
}
for locale in ["zh-Hant", "zh-TW", "zh-HK", "zh-MO"] {
set_fallbacks(collection, (hani, locale), &self.hani_traditional);
}
set_fallbacks(collection, (hani, "ja"), &self.hani_japanese);
set_fallbacks(collection, (hani, "ko"), &self.hani_korean);
set_fallbacks(collection, Script::from_str_unchecked("Arab"), &self.arabic);
set_fallbacks(collection, Script::from_str_unchecked("Hebr"), &self.hebrew);
}
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn install_deterministic_test_fonts(renderer: &mut crate::renderer::HydrolysisRenderer) {
use parley::fontique::{Blob, CollectionOptions};
use std::sync::Arc;
const FONTS: &[(&str, &[u8])] = &[
(
"Roboto-Regular.ttf",
include_bytes!("../../test-fonts/Roboto-Regular.ttf"),
),
(
"Roboto-Medium.ttf",
include_bytes!("../../test-fonts/Roboto-Medium.ttf"),
),
(
"Roboto-Bold.ttf",
include_bytes!("../../test-fonts/Roboto-Bold.ttf"),
),
(
"Roboto-Italic.ttf",
include_bytes!("../../test-fonts/Roboto-Italic.ttf"),
),
];
let font_cx = renderer.state_mut().text_fonts_mut();
font_cx.collection = Collection::new(CollectionOptions {
system_fonts: false,
..CollectionOptions::default()
});
let mut resource_fonts = ResourceFontFamilies::default();
for (name, bytes) in FONTS {
let families = font_cx
.collection
.register_fonts(Blob::new(Arc::new(*bytes)), None);
resource_fonts.classify(name, &families);
}
resource_fonts.install(&mut font_cx.collection);
}
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn load_native_resource_fonts(renderer: &mut crate::renderer::HydrolysisRenderer) {
use parley::fontique::Blob;
use std::sync::Arc;
let mut roots = Vec::new();
if let Ok(current_dir) = std::env::current_dir() {
roots.push(current_dir.join("resources").join("fonts"));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(exe_dir) = exe.parent()
{
roots.push(exe_dir.join("resources").join("fonts"));
if let Some(contents_dir) = exe_dir.parent()
&& contents_dir
.file_name()
.is_some_and(|name| name == "Contents")
{
roots.push(
contents_dir
.join("Resources")
.join("resources")
.join("fonts"),
);
}
}
let font_cx = renderer.state_mut().text_fonts_mut();
let mut resource_fonts = ResourceFontFamilies::default();
for root in roots {
if !root.exists() {
continue;
}
let entries = std::fs::read_dir(&root).unwrap_or_else(|error| {
panic!(
"hydrolysis native font loader failed to read `{}`: {error}",
root.display()
)
});
for entry in entries {
let entry = entry.unwrap_or_else(|error| {
panic!(
"hydrolysis native font loader failed to read an entry in `{}`: {error}",
root.display()
)
});
let path = entry.path();
let Some(extension) = path.extension().and_then(|extension| extension.to_str()) else {
continue;
};
if !extension.eq_ignore_ascii_case("ttf") && !extension.eq_ignore_ascii_case("otf") {
continue;
}
let font_data = std::fs::read(&path).unwrap_or_else(|error| {
panic!(
"hydrolysis native font loader failed to read `{}`: {error}",
path.display()
)
});
let families = font_cx
.collection
.register_fonts(Blob::new(Arc::new(font_data)), None);
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_else(|| {
panic!(
"hydrolysis native font loader found a font path without UTF-8 file name: `{}`",
path.display()
)
});
resource_fonts.classify(file_name, &families);
tracing::debug!(
target: "waterui::hydrolysis::fonts",
path = %path.display(),
families = families.len(),
"registered native Hydrolysis font"
);
}
}
resource_fonts.install(&mut font_cx.collection);
}