use std::sync::{Mutex, OnceLock};
use std::collections::HashMap;
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Wrap};
use crate::fonts::{parse_css_font, FontFamily};
use crate::render::GlyphMetric;
fn font_system() -> &'static Mutex<FontSystem> {
static FS: OnceLock<Mutex<FontSystem>> = OnceLock::new();
FS.get_or_init(|| {
use cosmic_text::fontdb;
use uzor_fonts as f;
let mut db = fontdb::Database::new();
for bytes in &[
f::ROBOTO_REGULAR,
f::ROBOTO_BOLD,
f::ROBOTO_ITALIC,
f::ROBOTO_BOLD_ITALIC,
f::PT_ROOT_UI_VF,
f::JETBRAINS_MONO_REGULAR,
f::JETBRAINS_MONO_BOLD,
f::SYMBOLS_NERD_FONT_MONO,
f::NOTO_SANS_SYMBOLS2,
f::NOTO_COLOR_EMOJI,
f::NOTO_EMOJI,
f::DEJAVU_SANS,
] {
db.load_font_data(bytes.to_vec());
}
db.set_sans_serif_family("Roboto");
db.set_monospace_family("JetBrains Mono");
db.set_serif_family("Roboto");
let fs = FontSystem::new_with_locale_and_db("en-US".to_string(), db);
Mutex::new(fs)
})
}
fn shape_cache() -> &'static Mutex<HashMap<(String, String), Vec<GlyphMetric>>> {
static CACHE: OnceLock<Mutex<HashMap<(String, String), Vec<GlyphMetric>>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn measure_glyphs(text: &str, font: &str) -> Vec<GlyphMetric> {
if text.is_empty() {
return Vec::new();
}
let cache_key = (font.to_string(), text.to_string());
if let Ok(cache) = shape_cache().lock() {
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
}
let result = shape_uncached(text, font);
if let Ok(mut cache) = shape_cache().lock() {
cache.insert(cache_key, result.clone());
}
result
}
fn shape_uncached(text: &str, font: &str) -> Vec<GlyphMetric> {
let info = parse_css_font(font);
let font_size = info.size;
let family_name: &str = match info.family {
FontFamily::Roboto => "Roboto",
FontFamily::PtRootUi => "PT Root UI",
FontFamily::JetBrainsMono => "JetBrains Mono",
};
let Ok(mut fs) = font_system().lock() else {
return fallback_per_char(text, font);
};
let metrics = Metrics::new(font_size, font_size * 1.2);
let mut buf = Buffer::new_empty(metrics);
buf.set_size(&mut fs, Some(f32::MAX), Some(f32::MAX));
buf.set_wrap(&mut fs, Wrap::None);
let attrs = Attrs::new()
.family(Family::Name(family_name))
.weight(if info.bold {
cosmic_text::Weight::BOLD
} else {
cosmic_text::Weight::NORMAL
})
.style(if info.italic {
cosmic_text::Style::Italic
} else {
cosmic_text::Style::Normal
});
buf.set_text(&mut fs, text, attrs, Shaping::Advanced);
buf.shape_until_scroll(&mut fs, false);
let mut result: Vec<GlyphMetric> = Vec::new();
let mut last_byte_range: Option<(usize, usize)> = None;
for run in buf.layout_runs() {
let line_text = run.text;
for glyph in run.glyphs {
let cluster_str = &line_text[glyph.start..glyph.end];
let x_off = glyph.x as f64;
let y_off = (glyph.y_offset * glyph.font_size) as f64;
let width = glyph.w as f64;
let advance = width;
let same_cluster = last_byte_range == Some((glyph.start, glyph.end));
if same_cluster {
if let Some(last) = result.last_mut() {
last.advance += advance;
last.width += width;
continue;
}
}
last_byte_range = Some((glyph.start, glyph.end));
result.push(GlyphMetric {
cluster: cluster_str.to_string(),
x_offset: x_off,
y_offset: y_off,
advance,
width,
});
}
}
result
}
fn fallback_per_char(text: &str, font: &str) -> Vec<GlyphMetric> {
let info = parse_css_font(font);
let char_w = info.size as f64 * 0.6;
let mut x = 0.0f64;
text.chars()
.map(|c| {
let cluster = c.to_string();
let advance = char_w;
let m = GlyphMetric {
cluster,
x_offset: x,
y_offset: 0.0,
advance,
width: advance,
};
x += advance;
m
})
.collect()
}