use std::sync::{Mutex, OnceLock};
use std::collections::HashMap;
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache, Wrap};
use crate::fonts::{parse_css_font, FontFamily};
use crate::render::{GlyphMetric, WrappedLine};
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()))
}
fn outline_cache() -> &'static Mutex<HashMap<(String, String), String>> {
static CACHE: OnceLock<Mutex<HashMap<(String, String), String>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn wrapped_cache() -> &'static Mutex<HashMap<(String, String, u64), Vec<WrappedLine>>> {
static CACHE: OnceLock<Mutex<HashMap<(String, String, u64), Vec<WrappedLine>>>> =
OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn swash_cache() -> &'static Mutex<SwashCache> {
static SC: OnceLock<Mutex<SwashCache>> = OnceLock::new();
SC.get_or_init(|| Mutex::new(SwashCache::new()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShaperFontId(cosmic_text::fontdb::ID);
#[derive(Debug, Clone, Copy)]
pub struct ShapedGlyph {
pub glyph_id: u32,
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone)]
pub struct GlyphSegment {
pub font: ShaperFontId,
pub font_size: f32,
pub glyphs: Vec<ShapedGlyph>,
pub text: String,
}
pub fn shape_glyph_runs(text: &str, font: &str) -> Vec<GlyphSegment> {
if text.is_empty() {
return Vec::new();
}
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 Vec::new();
};
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);
struct InProgress {
font: ShaperFontId,
font_size: f32,
range: std::ops::Range<usize>,
line_text: String,
glyphs: Vec<ShapedGlyph>,
}
let mut segments: Vec<InProgress> = Vec::new();
let mut first_line_y: Option<f32> = None;
for run in buf.layout_runs() {
let line_text = run.text;
let baseline_y = *first_line_y.get_or_insert(run.line_y);
let pen_y = run.line_y - baseline_y;
for glyph in run.glyphs {
let font_id = ShaperFontId(glyph.font_id);
let shaped = ShapedGlyph { glyph_id: glyph.glyph_id as u32, x: glyph.x, y: pen_y };
let mut extended_open_segment = false;
if let Some(seg) = segments.last_mut() {
if seg.font == font_id && seg.line_text == line_text {
seg.range.start = seg.range.start.min(glyph.start);
seg.range.end = seg.range.end.max(glyph.end);
seg.glyphs.push(shaped);
extended_open_segment = true;
}
}
if !extended_open_segment {
segments.push(InProgress {
font: font_id,
font_size: glyph.font_size,
range: glyph.start..glyph.end,
line_text: line_text.to_string(),
glyphs: vec![shaped],
});
}
}
}
segments
.into_iter()
.map(|s| GlyphSegment {
font: s.font,
font_size: s.font_size,
glyphs: s.glyphs,
text: s.line_text.get(s.range).unwrap_or_default().to_string(),
})
.collect()
}
pub fn font_bytes_for(id: ShaperFontId) -> Option<Vec<u8>> {
let fs = font_system().lock().ok()?;
fs.db().with_face_data(id.0, |data, _face_index| data.to_vec())
}
pub fn font_family_for(id: ShaperFontId) -> Option<String> {
let fs = font_system().lock().ok()?;
fs.db().face(id.0).and_then(|face| face.families.first().map(|(name, _)| name.clone()))
}
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
}
pub fn text_to_path(text: &str, font: &str) -> String {
if text.is_empty() {
return String::new();
}
let cache_key = (font.to_string(), text.to_string());
if let Ok(cache) = outline_cache().lock() {
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
}
let result = text_to_path_uncached(text, font);
if let Ok(mut cache) = outline_cache().lock() {
cache.insert(cache_key, result.clone());
}
result
}
pub fn measure_glyphs_wrapped(text: &str, font: &str, max_width: f64) -> Vec<WrappedLine> {
if text.is_empty() {
return Vec::new();
}
let cache_key = (font.to_string(), text.to_string(), max_width.to_bits());
if let Ok(cache) = wrapped_cache().lock() {
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
}
let result = measure_glyphs_wrapped_uncached(text, font, max_width);
if let Ok(mut cache) = wrapped_cache().lock() {
cache.insert(cache_key, result.clone());
}
result
}
fn text_to_path_uncached(text: &str, font: &str) -> String {
use cosmic_text::Command;
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 String::new();
};
let Ok(mut sc) = swash_cache().lock() else {
return String::new();
};
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 d = String::new();
for run in buf.layout_runs() {
for glyph in run.glyphs {
let pen_x = glyph.x;
let pen_y = run.line_y;
let physical = glyph.physical((0.0, 0.0), 1.0);
let Some(cmds) = sc.get_outline_commands(&mut fs, physical.cache_key) else {
continue;
};
for cmd in cmds {
match *cmd {
Command::MoveTo(p) => {
let x = pen_x + p.x;
let y = pen_y - p.y;
if !d.is_empty() { d.push(' '); }
d.push_str(&format!("M {x:.2} {y:.2}"));
}
Command::LineTo(p) => {
let x = pen_x + p.x;
let y = pen_y - p.y;
d.push_str(&format!(" L {x:.2} {y:.2}"));
}
Command::QuadTo(c, p) => {
let cx = pen_x + c.x;
let cy = pen_y - c.y;
let x = pen_x + p.x;
let y = pen_y - p.y;
d.push_str(&format!(" Q {cx:.2} {cy:.2} {x:.2} {y:.2}"));
}
Command::CurveTo(c1, c2, p) => {
let c1x = pen_x + c1.x;
let c1y = pen_y - c1.y;
let c2x = pen_x + c2.x;
let c2y = pen_y - c2.y;
let x = pen_x + p.x;
let y = pen_y - p.y;
d.push_str(&format!(" C {c1x:.2} {c1y:.2} {c2x:.2} {c2y:.2} {x:.2} {y:.2}"));
}
Command::Close => {
d.push_str(" Z");
}
}
}
}
}
d
}
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 measure_glyphs_wrapped_uncached(text: &str, font: &str, max_width: f64) -> Vec<WrappedLine> {
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_wrapped(text, font, max_width);
};
let metrics = Metrics::new(font_size, font_size * 1.2);
let mut buf = Buffer::new_empty(metrics);
let width_f32 = max_width.max(1.0) as f32;
buf.set_size(&mut fs, Some(width_f32), None);
buf.set_wrap(&mut fs, Wrap::Word);
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<WrappedLine> = Vec::new();
for run in buf.layout_runs() {
let line_text = run.text;
let mut glyphs: Vec<GlyphMetric> = Vec::new();
let mut last_byte_range: Option<(usize, usize)> = None;
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) = glyphs.last_mut() {
last.advance += advance;
last.width += width;
continue;
}
}
last_byte_range = Some((glyph.start, glyph.end));
glyphs.push(GlyphMetric {
cluster: cluster_str.to_string(),
x_offset: x_off,
y_offset: y_off,
advance,
width,
});
}
result.push(WrappedLine {
glyphs,
line_top: run.line_top as f64,
baseline_y: run.line_y as f64,
width: run.line_w as f64,
});
}
result
}
fn fallback_per_char_wrapped(text: &str, font: &str, max_width: f64) -> Vec<WrappedLine> {
let info = parse_css_font(font);
let char_w = info.size as f64 * 0.6;
let line_height = info.size as f64 * 1.2;
let ascent = info.size as f64 * 0.9;
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
let mut current_w = 0.0f64;
for word in text.split_whitespace() {
let word_w = word.chars().count() as f64 * char_w;
if current.is_empty() {
current.push_str(word);
current_w = word_w;
continue;
}
let candidate_w = current_w + char_w + word_w;
if candidate_w > max_width {
lines.push(std::mem::take(&mut current));
current.push_str(word);
current_w = word_w;
} else {
current.push(' ');
current.push_str(word);
current_w = candidate_w;
}
}
lines.push(current);
let mut line_top = 0.0f64;
lines
.into_iter()
.map(|line_text| {
let glyphs = fallback_per_char(&line_text, font);
let width = line_text.chars().count() as f64 * char_w;
let wrapped = WrappedLine {
glyphs,
line_top,
baseline_y: line_top + ascent,
width,
};
line_top += line_height;
wrapped
})
.collect()
}
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()
}
#[cfg(test)]
mod tests {
use super::*;
const FONT: &str = "16px Roboto";
const LONG_SENTENCE: &str = "The quick brown fox jumps over the lazy dog \
and then keeps running further down the road without stopping for a \
very long time indeed";
#[test]
fn wrapped_at_huge_width_matches_unwrapped_glyph_for_glyph() {
let wrapped = measure_glyphs_wrapped(LONG_SENTENCE, FONT, f64::MAX);
assert_eq!(
wrapped.len(),
1,
"expected exactly one line at f64::MAX width, got {}",
wrapped.len()
);
let unwrapped = measure_glyphs(LONG_SENTENCE, FONT);
let wrapped_glyphs = &wrapped[0].glyphs;
assert_eq!(wrapped_glyphs.len(), unwrapped.len());
for (w, u) in wrapped_glyphs.iter().zip(unwrapped.iter()) {
assert_eq!(w.cluster, u.cluster);
assert!((w.x_offset - u.x_offset).abs() < 0.01);
assert!((w.y_offset - u.y_offset).abs() < 0.01);
assert!((w.advance - u.advance).abs() < 0.01);
assert!((w.width - u.width).abs() < 0.01);
}
}
#[test]
fn wrapped_at_wide_enough_width_is_a_single_line() {
let natural_width = measure_glyphs(LONG_SENTENCE, FONT)
.iter()
.map(|g| g.x_offset + g.advance)
.fold(0.0f64, f64::max);
let lines = measure_glyphs_wrapped(LONG_SENTENCE, FONT, natural_width + 100.0);
assert_eq!(lines.len(), 1);
}
#[test]
fn narrow_width_wraps_into_multiple_lines_with_expected_geometry() {
let max_width = 150.0;
let line_height = 16.0 * 1.2;
let lines = measure_glyphs_wrapped(LONG_SENTENCE, FONT, max_width);
assert!(
lines.len() > 1,
"expected wrap into multiple lines, got {}",
lines.len()
);
for (i, line) in lines.iter().enumerate() {
let expected_top = i as f64 * line_height;
assert!(
(line.line_top - expected_top).abs() < 0.5,
"line {i} top {} != expected {expected_top}",
line.line_top
);
assert!(
line.width <= max_width + 1.0,
"line {i} width {} exceeds max_width {max_width}",
line.width
);
for glyph in &line.glyphs {
let end = glyph.x_offset + glyph.advance;
assert!(
end <= max_width + 1.0,
"glyph '{}' end {end} exceeds max_width {max_width} on line {i}",
glyph.cluster
);
}
}
let total_height = lines.len() as f64 * line_height;
let last_top = lines.last().map(|l| l.line_top).unwrap_or(0.0);
assert!((last_top + line_height - total_height).abs() < 0.5);
}
#[test]
fn wrapped_empty_text_is_empty() {
assert!(measure_glyphs_wrapped("", FONT, 100.0).is_empty());
}
#[test]
fn shape_glyph_runs_empty_text_is_empty() {
assert!(shape_glyph_runs("", FONT).is_empty());
}
#[test]
fn shape_glyph_runs_single_font_text_is_one_segment() {
let segments = shape_glyph_runs("Hello", FONT);
assert_eq!(segments.len(), 1, "plain ASCII text in one font must stay one segment");
let seg = &segments[0];
assert_eq!(seg.glyphs.len(), 5, "one glyph per character, no ligatures in \"Hello\"");
assert_eq!(seg.text, "Hello");
assert!(seg.glyphs.iter().all(|g| g.glyph_id != 0), "no glyph should resolve to .notdef for plain ASCII text");
}
#[test]
fn shape_glyph_runs_x_positions_are_monotonically_increasing_for_ltr_text() {
let segments = shape_glyph_runs("Hello", FONT);
let seg = &segments[0];
for w in seg.glyphs.windows(2) {
assert!(w[1].x >= w[0].x, "LTR glyph pen positions must not go backwards: {} then {}", w[0].x, w[1].x);
}
}
#[test]
fn shape_glyph_runs_font_bytes_for_returns_a_real_font_file() {
let segments = shape_glyph_runs("Hello", FONT);
let bytes = font_bytes_for(segments[0].font).expect("Roboto must resolve to real font bytes");
assert!(bytes.len() > 1024, "a real font file must be more than 1KB, got {}", bytes.len());
}
#[test]
fn shape_glyph_runs_font_family_for_reports_roboto() {
let segments = shape_glyph_runs("Hello", FONT);
let family = font_family_for(segments[0].font).expect("Roboto must resolve to a family name");
assert_eq!(family, "Roboto");
}
#[test]
fn shape_glyph_runs_is_deterministic_across_calls() {
let a = shape_glyph_runs("Hello, world!", FONT);
let b = shape_glyph_runs("Hello, world!", FONT);
assert_eq!(a.len(), b.len());
for (sa, sb) in a.iter().zip(b.iter()) {
assert_eq!(sa.font, sb.font);
assert_eq!(sa.text, sb.text);
assert_eq!(sa.glyphs.len(), sb.glyphs.len());
for (ga, gb) in sa.glyphs.iter().zip(sb.glyphs.iter()) {
assert_eq!(ga.glyph_id, gb.glyph_id);
assert_eq!(ga.x, gb.x);
assert_eq!(ga.y, gb.y);
}
}
}
}