use fontdue::Font;
use rustybuzz::Face;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::RwLock;
use super::Script;
pub(crate) const FONT_ID_DEFAULT: u8 = 0;
pub(crate) const FONT_ID_MONO: u8 = Script::SLOTS as u8 + 1;
pub(crate) struct FontEntry {
pub face: Face<'static>,
pub body: Font,
pub id: u8,
}
static FONT_INSTALL_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn font_install_count() -> usize {
FONT_INSTALL_COUNTER.load(Ordering::Relaxed)
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
pub struct TextStyle {
pub bold: bool,
pub italic: bool,
pub mono: bool,
pub link: bool,
}
impl TextStyle {
pub const PLAIN: TextStyle = TextStyle {
bold: false,
italic: false,
mono: false,
link: false,
};
pub fn is_plain(self) -> bool {
self == Self::PLAIN
}
pub(crate) fn bits(self) -> u8 {
(self.bold as u8)
| ((self.italic as u8) << 1)
| ((self.mono as u8) << 2)
| ((self.link as u8) << 3)
}
}
pub(crate) struct FontRegistry {
pub default: FontEntry,
pub mono: FontEntry,
pub scripts: [RwLock<Option<FontEntry>>; Script::SLOTS],
}
static FONTS: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
type GlyphMap = std::collections::HashMap<(u8, u16, u32), (fontdue::Metrics, Vec<u8>)>;
pub(crate) static GLYPH_CACHE: std::sync::OnceLock<std::sync::Mutex<GlyphMap>> =
std::sync::OnceLock::new();
pub(crate) fn glyph_cache() -> &'static std::sync::Mutex<GlyphMap> {
GLYPH_CACHE.get_or_init(std::sync::Mutex::default)
}
fn load_font(data: &'static [u8], id: u8) -> FontEntry {
let face = Face::from_slice(data, 0).expect("font parse");
let body = Font::from_bytes(data, fontdue::FontSettings::default()).expect("fontdue load");
FontEntry { face, body, id }
}
pub(crate) fn fonts() -> &'static FontRegistry {
FONTS.get_or_init(|| FontRegistry {
default: load_font(super::FONT_LATIN, FONT_ID_DEFAULT),
mono: load_font(super::FONT_MONO, FONT_ID_MONO),
scripts: std::array::from_fn(|_| RwLock::new(None)),
})
}
pub fn install_font(script: Script, data: Vec<u8>) -> bool {
if matches!(script, Script::Latin | Script::Other) {
return false;
}
let entry = match load_font_owned(data, script.font_id()) {
Some(e) => e,
None => return false,
};
let reg = fonts();
if let Ok(mut guard) = reg.scripts[script.slot()].write() {
*guard = Some(entry);
}
super::clear_width_cache();
FONT_INSTALL_COUNTER.fetch_add(1, Ordering::Relaxed);
true
}
fn load_font_owned(data: Vec<u8>, id: u8) -> Option<FontEntry> {
let data_boxed: Box<[u8]> = data.into_boxed_slice();
let leaked: &'static [u8] = Box::leak(data_boxed);
let face = Face::from_slice(leaked, 0)?;
let body = Font::from_bytes(leaked, fontdue::FontSettings::default()).ok()?;
Some(FontEntry { face, body, id })
}
pub fn font_covers(data: &[u8], probe_chars: &str) -> bool {
let face = match Face::from_slice(data, 0) {
Some(f) => f,
None => return false,
};
probe_chars.chars().all(|c| face.glyph_index(c).is_some())
}
pub(crate) fn font_for(script: Script, style: TextStyle) -> &'static FontEntry {
if style.mono {
return &fonts().mono;
}
font_for_script(script)
}
pub(crate) fn font_for_script(script: Script) -> &'static FontEntry {
let reg = fonts();
if matches!(script, Script::Latin | Script::Other) {
return ®.default;
}
let slot = ®.scripts[script.slot()];
if let Ok(guard) = slot.read() {
if let Some(ref entry) = *guard {
unsafe {
return &*(entry as *const FontEntry);
}
}
}
®.default
}
pub(crate) fn fallback_for_char(c: char) -> Option<(&'static FontEntry, u16)> {
let reg = fonts();
for slot in reg.scripts.iter() {
if let Ok(g) = slot.read() {
if let Some(entry) = g.as_ref() {
if let Some(gid) = entry.face.glyph_index(c) {
let e: &'static FontEntry = unsafe { &*(entry as *const FontEntry) };
return Some((e, gid.0));
}
}
}
}
reg.default
.face
.glyph_index(c)
.map(|gid| (®.default, gid.0))
}
pub fn has_font_for(script: Script) -> bool {
if matches!(script, Script::Latin | Script::Other) {
return true;
}
fonts().scripts[script.slot()]
.read()
.map(|g| g.is_some())
.unwrap_or(false)
}
pub fn line_height_for(script: Script, px_size: f32) -> usize {
let fd = font_for_script(script);
let lm = fd.body.horizontal_line_metrics(px_size);
let h = match lm {
Some(m) => m.ascent - m.descent + m.line_gap,
None => px_size * 1.2,
};
h.max(1.0) as usize
}
pub fn line_height(px_size: f32) -> usize {
line_height_for(Script::Latin, px_size)
}
pub fn word_width(word: &str, px_size: f32) -> f32 {
word_width_styled(word, px_size, TextStyle::PLAIN)
}
pub fn word_width_styled(word: &str, px_size: f32, style: TextStyle) -> f32 {
let key = (
format!("{}\u{1}{}", word, style.bits()),
(px_size * 100.0) as u32,
);
let cache = super::width_cache();
if let Ok(c) = cache.lock() {
if let Some(&w) = c.get(&key) {
return w;
}
}
let fd = font_for(super::detect_script(word), style);
let scale = px_size / fd.face.units_per_em() as f32;
let mut ub = rustybuzz::UnicodeBuffer::new();
ub.push_str(word);
let dir = if super::detect_script(word).is_rtl() {
rustybuzz::Direction::RightToLeft
} else {
rustybuzz::Direction::LeftToRight
};
ub.set_direction(dir);
let gb = rustybuzz::shape(&fd.face, &[], ub);
let w = gb
.glyph_positions()
.iter()
.map(|p| p.x_advance as f32)
.sum::<f32>()
* scale;
if let Ok(mut c) = cache.lock() {
c.insert(key, w);
}
w
}
pub fn char_widths_batch(text: &str, px_size: f32, style: TextStyle) -> Vec<f32> {
let fd = font_for(super::detect_script(text), style);
let scale = px_size / fd.face.units_per_em() as f32;
let mut ub = rustybuzz::UnicodeBuffer::new();
ub.push_str(text);
ub.set_direction(rustybuzz::Direction::LeftToRight);
let gb = rustybuzz::shape(&fd.face, &[], ub);
let clusters = gb.glyph_infos();
let positions = gb.glyph_positions();
let mut out = vec![0.0f32; text.chars().count()];
let mut ci = 0usize;
for (gi, pos) in positions.iter().enumerate() {
let adv = pos.x_advance as f32 * scale;
let cluster = clusters.get(gi).map(|c| c.cluster as usize).unwrap_or(0);
let char_idx = text[..cluster.min(text.len())].chars().count();
if char_idx < out.len() {
ci = char_idx;
}
out[ci] += adv;
}
out
}