use crate::{TextPipeline, TextStyle};
use oxitext::Bitmap;
use oxiui_core::UiError;
use std::collections::{HashMap, VecDeque};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub glyph_id: u16,
pub font_size_pixels: u32,
pub subpixel_offset_16ths: (u8, u8),
}
impl GlyphKey {
pub fn new(glyph_id: u16, font_size: f32, subpixel_x: f32, subpixel_y: f32) -> Self {
GlyphKey {
glyph_id,
font_size_pixels: (font_size * 16.0) as u32,
subpixel_offset_16ths: (
((subpixel_x.fract() * 16.0) as u8).min(15),
((subpixel_y.fract() * 16.0) as u8).min(15),
),
}
}
}
#[derive(Clone, Debug)]
pub struct GlyphEntry {
pub bitmap: Bitmap,
pub advance_x: f32,
pub bearing: (i32, i32),
}
pub struct GlyphAtlas {
cache: HashMap<GlyphKey, GlyphEntry>,
lru: VecDeque<GlyphKey>,
max_entries: usize,
}
impl GlyphAtlas {
pub fn new(max_entries: usize) -> Self {
GlyphAtlas {
cache: HashMap::new(),
lru: VecDeque::new(),
max_entries,
}
}
pub fn get(&self, key: &GlyphKey) -> Option<&GlyphEntry> {
self.cache.get(key)
}
pub fn get_or_rasterize(
&mut self,
pipeline: &mut TextPipeline,
key: GlyphKey,
text: &str,
style: &TextStyle,
) -> Result<&GlyphEntry, UiError> {
if self.cache.contains_key(&key) {
if let Some(pos) = self.lru.iter().position(|k| k == &key) {
self.lru.remove(pos);
}
self.lru.push_back(key.clone());
return self
.cache
.get(&key)
.ok_or_else(|| UiError::Other("atlas: key confirmed but missing in map".into()));
}
let result = pipeline.render(text, style)?;
let entry = result
.glyphs
.iter()
.zip(result.bitmaps.iter())
.find(|(g, _)| g.gid == key.glyph_id)
.map(|(g, bm)| GlyphEntry {
bitmap: bm.clone(),
advance_x: g.advance_x,
bearing: (0, 0),
})
.ok_or_else(|| {
UiError::Other(format!(
"atlas: glyph id {} not found in render result",
key.glyph_id
))
})?;
if self.max_entries > 0 {
self.evict_to(self.max_entries - 1);
}
self.lru.push_back(key.clone());
self.cache.insert(key.clone(), entry);
self.cache
.get(&key)
.ok_or_else(|| UiError::Other("atlas: entry missing immediately after insert".into()))
}
pub fn evict_to(&mut self, max: usize) {
while self.cache.len() > max {
match self.lru.pop_front() {
Some(oldest) => {
self.cache.remove(&oldest);
}
None => break,
}
}
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn utilization(&self) -> f32 {
if self.max_entries == 0 {
return 0.0;
}
self.cache.len() as f32 / self.max_entries as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_entry(advance: f32) -> GlyphEntry {
GlyphEntry {
bitmap: Bitmap {
width: 1,
height: 1,
pixels: vec![128],
},
advance_x: advance,
bearing: (0, 0),
}
}
fn fill_atlas(atlas: &mut GlyphAtlas, count: u16) {
for i in 0..count {
let key = GlyphKey {
glyph_id: i,
font_size_pixels: 256,
subpixel_offset_16ths: (0, 0),
};
atlas.cache.insert(key.clone(), dummy_entry(8.0));
atlas.lru.push_back(key);
}
}
#[test]
fn utilization_half_capacity() {
let mut atlas = GlyphAtlas::new(10);
fill_atlas(&mut atlas, 5);
let u = atlas.utilization();
assert!((u - 0.5).abs() < f32::EPSILON, "expected 0.5, got {u}");
}
#[test]
fn utilization_empty() {
let atlas = GlyphAtlas::new(10);
assert!((atlas.utilization() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn evict_to_reduces_length() {
let mut atlas = GlyphAtlas::new(10);
fill_atlas(&mut atlas, 5);
assert_eq!(atlas.len(), 5);
atlas.evict_to(2);
assert_eq!(atlas.len(), 2);
}
#[test]
fn lru_eviction_drops_oldest() {
let mut atlas = GlyphAtlas::new(10);
fill_atlas(&mut atlas, 11);
let oldest_key = GlyphKey {
glyph_id: 0,
font_size_pixels: 256,
subpixel_offset_16ths: (0, 0),
};
atlas.evict_to(10);
assert_eq!(atlas.len(), 10);
assert!(
atlas.get(&oldest_key).is_none(),
"oldest entry should have been evicted"
);
}
#[test]
fn glyph_key_font_size_encoding() {
let key = GlyphKey::new(7, 16.0, 0.0, 0.0);
assert_eq!(key.font_size_pixels, 256u32);
assert_eq!(key.glyph_id, 7);
}
#[test]
fn glyph_key_subpixel_quantization() {
let key = GlyphKey::new(1, 12.0, 0.5, 0.25);
assert_eq!(key.subpixel_offset_16ths, (8, 4));
}
#[test]
fn get_returns_none_for_missing_key() {
let atlas = GlyphAtlas::new(10);
let key = GlyphKey::new(99, 16.0, 0.0, 0.0);
assert!(atlas.get(&key).is_none());
}
#[test]
fn is_empty_reflects_state() {
let mut atlas = GlyphAtlas::new(10);
assert!(atlas.is_empty());
fill_atlas(&mut atlas, 1);
assert!(!atlas.is_empty());
}
#[test]
fn get_returns_inserted_entry() {
let mut atlas = GlyphAtlas::new(10);
let key = GlyphKey {
glyph_id: 42,
font_size_pixels: 256,
subpixel_offset_16ths: (0, 0),
};
let entry = dummy_entry(12.5);
atlas.cache.insert(key.clone(), entry);
atlas.lru.push_back(key.clone());
let result = atlas.get(&key);
assert!(result.is_some());
let e = result.expect("entry present");
assert!((e.advance_x - 12.5).abs() < f32::EPSILON);
}
#[test]
fn evict_to_noop_when_within_capacity() {
let mut atlas = GlyphAtlas::new(10);
fill_atlas(&mut atlas, 3);
atlas.evict_to(5); assert_eq!(atlas.len(), 3);
}
}