use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use core::ops::{Deref, DerefMut};
pub use fontdb;
pub use rustybuzz;
use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FontMatchKey {
pub(crate) font_weight_diff: u16,
pub(crate) font_weight: u16,
pub(crate) id: fontdb::ID,
}
struct FontCachedCodepointSupportInfo {
supported: Vec<u32>,
not_supported: Vec<u32>,
}
impl FontCachedCodepointSupportInfo {
const SUPPORTED_MAX_SZ: usize = 512;
const NOT_SUPPORTED_MAX_SZ: usize = 1024;
fn new() -> Self {
Self {
supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
}
}
#[inline(always)]
fn unknown_has_codepoint(
&mut self,
font_codepoints: &[u32],
codepoint: u32,
supported_insert_pos: usize,
not_supported_insert_pos: usize,
) -> bool {
let ret = font_codepoints.contains(&codepoint);
if ret {
if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
self.supported.insert(supported_insert_pos, codepoint);
self.supported.truncate(Self::SUPPORTED_MAX_SZ);
}
} else {
if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
self.not_supported
.insert(not_supported_insert_pos, codepoint);
self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
}
}
ret
}
#[inline(always)]
fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
match self.supported.binary_search(&codepoint) {
Ok(_) => true,
Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
Ok(_) => false,
Err(not_supported_insert_pos) => self.unknown_has_codepoint(
font_codepoints,
codepoint,
supported_insert_pos,
not_supported_insert_pos,
),
},
}
}
}
pub struct FontSystem {
locale: String,
db: fontdb::Database,
font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
monospace_font_ids: Vec<fontdb::ID>,
per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
pub(crate) shape_buffer: ShapeBuffer,
pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
#[cfg(feature = "shape-run-cache")]
pub shape_run_cache: crate::ShapeRunCache,
pub(crate) dyn_fallback: Box<dyn Fallback>,
pub(crate) fallbacks: Fallbacks,
}
impl fmt::Debug for FontSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FontSystem")
.field("locale", &self.locale)
.field("db", &self.db)
.finish()
}
}
impl FontSystem {
const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
pub fn new() -> Self {
Self::new_with_fonts(core::iter::empty())
}
pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
let locale = Self::get_locale();
log::debug!("Locale: {}", locale);
let mut db = fontdb::Database::new();
Self::load_fonts(&mut db, fonts.into_iter());
db.set_monospace_family("Noto Sans Mono");
db.set_sans_serif_family("Open Sans");
db.set_serif_family("DejaVu Serif");
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
}
pub fn new_with_locale_and_db_and_fallback(
locale: String,
db: fontdb::Database,
impl_fallback: impl Fallback + 'static,
) -> Self {
let mut monospace_font_ids = db
.faces()
.filter(|face_info| {
face_info.monospaced && !face_info.post_script_name.contains("Emoji")
})
.map(|face_info| face_info.id)
.collect::<Vec<_>>();
monospace_font_ids.sort();
let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
HashMap::default();
if cfg!(feature = "monospace_fallback") {
monospace_font_ids.iter().for_each(|&id| {
db.with_face_data(id, |font_data, face_index| {
let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
face.tables()
.gpos
.into_iter()
.chain(face.tables().gsub)
.flat_map(|table| table.scripts)
.inspect(|script| {
per_script_monospace_font_ids
.entry(script.tag.to_bytes())
.or_default()
.insert(id);
})
});
});
});
}
let per_script_monospace_font_ids = per_script_monospace_font_ids
.into_iter()
.map(|(k, v)| (k, Vec::from_iter(v)))
.collect();
let fallbacks = Fallbacks::new(&impl_fallback, &[], &locale);
Self {
locale,
db,
monospace_font_ids,
per_script_monospace_font_ids,
font_cache: Default::default(),
font_matches_cache: Default::default(),
font_codepoint_support_info_cache: Default::default(),
monospace_fallbacks_buffer: BTreeSet::default(),
#[cfg(feature = "shape-run-cache")]
shape_run_cache: crate::ShapeRunCache::default(),
shape_buffer: ShapeBuffer::default(),
dyn_fallback: Box::new(impl_fallback),
fallbacks,
}
}
pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn db(&self) -> &fontdb::Database {
&self.db
}
pub fn db_mut(&mut self) -> &mut fontdb::Database {
self.font_matches_cache.clear();
&mut self.db
}
pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
(self.locale, self.db)
}
pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
self.font_cache
.entry(id)
.or_insert_with(|| {
#[cfg(feature = "std")]
unsafe {
self.db.make_shared_face_data(id);
}
match Font::new(&self.db, id) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!(
"failed to load font '{}'",
self.db.face(id)?.post_script_name
);
None
}
}
})
.clone()
}
pub fn is_monospace(&self, id: fontdb::ID) -> bool {
self.monospace_font_ids.binary_search(&id).is_ok()
}
pub fn get_monospace_ids_for_scripts(
&self,
scripts: impl Iterator<Item = [u8; 4]>,
) -> Vec<fontdb::ID> {
let mut ret = scripts
.filter_map(|script| self.per_script_monospace_font_ids.get(&script))
.flat_map(|ids| ids.iter().copied())
.collect::<Vec<_>>();
ret.sort();
ret.dedup();
ret
}
#[inline(always)]
pub fn get_font_supported_codepoints_in_word(
&mut self,
id: fontdb::ID,
word: &str,
) -> Option<usize> {
self.get_font(id).map(|font| {
let code_points = font.unicode_codepoints();
let cache = self
.font_codepoint_support_info_cache
.entry(id)
.or_insert_with(FontCachedCodepointSupportInfo::new);
word.chars()
.filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
.count()
})
}
pub fn get_font_matches(&mut self, attrs: &Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
log::trace!("clear font mache cache");
self.font_matches_cache.clear();
}
self.font_matches_cache
.entry(attrs.into())
.or_insert_with(|| {
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
let now = std::time::Instant::now();
let mut font_match_keys = self
.db
.faces()
.filter(|face| attrs.matches(face))
.map(|face| FontMatchKey {
font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
font_weight: face.weight.0,
id: face.id,
})
.collect::<Vec<_>>();
font_match_keys.sort();
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
{
let elapsed = now.elapsed();
log::debug!("font matches for {:?} in {:?}", attrs, elapsed);
}
Arc::new(font_match_keys)
})
.clone()
}
#[cfg(feature = "std")]
fn get_locale() -> String {
sys_locale::get_locale().unwrap_or_else(|| {
log::warn!("failed to get system locale, falling back to en-US");
String::from("en-US")
})
}
#[cfg(not(feature = "std"))]
fn get_locale() -> String {
String::from("en-US")
}
#[cfg(feature = "std")]
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
#[cfg(not(target_arch = "wasm32"))]
let now = std::time::Instant::now();
db.load_system_fonts();
for source in fonts {
db.load_font_source(source);
}
#[cfg(not(target_arch = "wasm32"))]
log::debug!(
"Parsed {} font faces in {}ms.",
db.len(),
now.elapsed().as_millis()
);
}
#[cfg(not(feature = "std"))]
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
for source in fonts {
db.load_font_source(source);
}
}
}
#[derive(Debug)]
pub struct BorrowedWithFontSystem<'a, T> {
pub(crate) inner: &'a mut T,
pub(crate) font_system: &'a mut FontSystem,
}
impl<T> Deref for BorrowedWithFontSystem<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner
}
}
impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner
}
}