use std::collections::HashMap;
use std::sync::Arc;
use crate::gl_renderer::tessellate_fill;
use crate::text::{flatten_glyph_at_origin, Font};
pub struct CachedGlyph {
pub verts: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
pub struct GlyphCache {
entries: HashMap<GlyphKey, Option<CachedGlyph>>,
}
impl GlyphCache {
pub fn new() -> Self {
GlyphCache {
entries: HashMap::new(),
}
}
pub fn get_or_insert(&mut self, font: &Font, glyph_id: u16, size: f64) -> Option<&CachedGlyph> {
let key = GlyphKey {
font_ptr: Arc::as_ptr(&font.data) as usize,
glyph_id,
size_bits: size.to_bits(),
};
self.entries
.entry(key)
.or_insert_with(|| tessellate_glyph(font, glyph_id, size))
.as_ref()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl Default for GlyphCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Hash, Eq, PartialEq, Clone)]
struct GlyphKey {
font_ptr: usize,
glyph_id: u16,
size_bits: u64,
}
fn tessellate_glyph(font: &Font, glyph_id: u16, size: f64) -> Option<CachedGlyph> {
let contours = flatten_glyph_at_origin(font, glyph_id, size)?;
let has_ccw = contours.iter().any(|c| contour_is_ccw(c));
let (verts_flat, indices) = if has_ccw {
tessellate_fill(&contours)?
} else {
let mut all_vf: Vec<f32> = Vec::new();
let mut all_idx: Vec<u32> = Vec::new();
for contour in &contours {
if let Some((vf, idx)) = tessellate_fill(&[contour.clone()]) {
let base = (all_vf.len() / 2) as u32;
all_vf.extend_from_slice(&vf);
all_idx.extend(idx.iter().map(|&i| i + base));
}
}
if all_vf.is_empty() {
return None;
}
(all_vf, all_idx)
};
let verts: Vec<[f32; 2]> = verts_flat.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
Some(CachedGlyph { verts, indices })
}
fn contour_is_ccw(pts: &[[f32; 2]]) -> bool {
let n = pts.len();
if n < 3 {
return false;
}
let mut area = 0.0f32;
for i in 0..n {
let j = (i + 1) % n;
area += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
area > 0.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::Font;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
fn test_font() -> Arc<Font> {
Arc::new(Font::from_slice(FONT_BYTES).expect("font ok"))
}
#[test]
fn test_cache_hit_on_second_access() {
use crate::text::shape_glyphs;
let font = test_font();
let mut cache = GlyphCache::new();
let glyphs = shape_glyphs(&font, "H", 14.0);
let gid = glyphs[0].glyph_id;
assert!(cache.is_empty(), "cache starts empty");
let first = cache.get_or_insert(&font, gid, 14.0);
assert!(first.is_some(), "'H' should have an outline");
assert_eq!(cache.len(), 1, "one entry after first access");
let second = cache.get_or_insert(&font, gid, 14.0);
assert!(second.is_some());
assert_eq!(cache.len(), 1, "cache still has one entry — no duplicate");
}
#[test]
fn test_cache_none_for_space() {
use crate::text::shape_glyphs;
let font = test_font();
let mut cache = GlyphCache::new();
let glyphs = shape_glyphs(&font, " ", 14.0);
let gid = glyphs[0].glyph_id;
let result = cache.get_or_insert(&font, gid, 14.0);
assert!(result.is_none(), "space glyph has no outline");
assert_eq!(
cache.len(),
1,
"None is cached to avoid re-entering the shaper"
);
}
#[test]
fn test_different_sizes_are_separate_entries() {
use crate::text::shape_glyphs;
let font = test_font();
let mut cache = GlyphCache::new();
let gid = shape_glyphs(&font, "H", 14.0)[0].glyph_id;
cache.get_or_insert(&font, gid, 14.0);
cache.get_or_insert(&font, gid, 16.0);
assert_eq!(cache.len(), 2, "14px and 16px are separate entries");
}
#[test]
fn test_cached_verts_are_in_pixel_range() {
use crate::text::shape_glyphs;
let font = test_font();
let mut cache = GlyphCache::new();
let size = 14.0_f64;
let gid = shape_glyphs(&font, "H", size)[0].glyph_id;
let cached = cache
.get_or_insert(&font, gid, size)
.expect("H has outline");
for &[x, y] in &cached.verts {
assert!(
x >= -2.0 && x <= 20.0,
"x={x} must be in glyph-local pixels, not font units"
);
assert!(
y >= -4.0 && y <= 18.0,
"y={y} must be in glyph-local pixels, not font units"
);
}
}
}