use std::sync::Arc;
use smudge_core::geometry::{Color, Point};
use smudge_core::paint::FontBlob;
use smudge_text::{TextLayout, TextStyle, TextSystem};
use crate::glyph_cache::{GlyphCache, MaskKey};
fn setup() {
TextSystem::bundled_only().install();
}
fn keys(text: &str, index: usize, count: usize) -> (FontBlob, Vec<MaskKey>) {
let style = TextStyle::default();
let layout = TextLayout::new(text, &style, None);
let runs = layout.glyph_runs_at(Point { x: 0.0, y: 64.0 }, Color::BLACK);
let run = runs.first().expect("the text has at least one run");
let glyph = run.glyphs[index];
let keys = (0..count)
.map(|step| MaskKey {
font_id: run.font.id,
font_index: run.font.index,
glyph_id: glyph.id,
size: 64 * 64 + step as i32,
subpixel: 0,
})
.collect();
(clone_font_blob(&run.font), keys)
}
fn clone_font_blob(font: &FontBlob) -> FontBlob {
FontBlob {
id: font.id,
data: Arc::clone(&font.data),
index: font.index,
}
}
#[test]
fn repeated_draw_hits_the_cache() {
setup();
let (font, keys) = keys("M", 0, 1);
let mut cache = GlyphCache::new();
assert!(cache.entry(keys[0], &font).is_some());
assert_eq!(cache.misses(), 1, "the first draw rasterizes the mask");
assert_eq!(cache.hits(), 0);
assert!(cache.entry(keys[0], &font).is_some());
assert_eq!(cache.misses(), 1, "the second draw reuses the mask");
assert_eq!(cache.hits(), 1);
assert!(
cache.entry(keys[0], &font).is_some() && cache.entry(keys[0], &font).is_some(),
"the mask stays cached"
);
assert_eq!(cache.hits(), 3);
assert_eq!(cache.misses(), 1);
}
#[test]
fn a_glyph_without_outline_is_cached_as_empty() {
setup();
let style = TextStyle::default();
let layout = TextLayout::new("a b", &style, None);
let runs = layout.glyph_runs_at(Point { x: 0.0, y: 20.0 }, Color::BLACK);
let run = runs.first().expect("the text shapes to a run");
let font = clone_font_blob(&run.font);
let keys: Vec<_> = run
.glyphs
.iter()
.map(|glyph| MaskKey {
font_id: run.font.id,
font_index: run.font.index,
glyph_id: glyph.id,
size: 14 * 64,
subpixel: 0,
})
.collect();
let mut cache = GlyphCache::new();
let mut empty = false;
for key in &keys {
let mask = cache.entry(*key, &font).expect("every glyph gets a mask");
empty |= mask.width() == 0;
}
assert!(empty, "the space has no outline, so one mask is empty");
assert_eq!(
cache.misses(),
keys.len() as u64,
"each glyph rasterized once"
);
for key in &keys {
assert!(cache.entry(*key, &font).is_some());
}
assert_eq!(
cache.misses(),
keys.len() as u64,
"the empty mask is cached like any other"
);
}
#[test]
fn filling_the_budget_drops_the_older_generation() {
setup();
let (font, probe) = keys("M", 0, 1);
let mut probe_cache = GlyphCache::new();
probe_cache
.entry(probe[0], &font)
.expect("the probe rasterizes");
let mask_bytes = probe_cache.bytes();
let mut cache = GlyphCache::with_budget(mask_bytes * 10);
let (font, keys) = keys("M", 0, 40);
for key in &keys {
assert!(cache.entry(*key, &font).is_some());
}
assert_eq!(cache.misses(), 40, "every distinct key rasterized once");
let misses = cache.misses();
assert!(
cache.entry(keys[0], &font).is_some(),
"a dropped mask rasterizes again"
);
assert_eq!(cache.misses(), misses + 1, "the first mask was evicted");
assert!(
cache.entry(keys[39], &font).is_some(),
"the last mask rasterized"
);
assert_eq!(
cache.misses(),
misses + 1,
"a recently rasterized mask is still cached"
);
assert!(
cache.bytes() <= mask_bytes * 10,
"the current generation stays within the budget"
);
}
#[test]
fn a_hit_in_the_previous_generation_moves_back_to_the_current_one() {
setup();
let (font, probe) = keys("M", 0, 1);
let mut probe_cache = GlyphCache::new();
probe_cache
.entry(probe[0], &font)
.expect("the probe rasterizes");
let mask_bytes = probe_cache.bytes();
let mut cache = GlyphCache::with_budget(mask_bytes * 6);
let (font, keys) = keys("M", 0, 12);
for key in &keys[..6] {
cache.entry(*key, &font).expect("each mask rasterizes");
}
cache.entry(keys[6], &font).expect("the mask rasterizes");
cache
.entry(keys[0], &font)
.expect("the mask is still cached");
for key in &keys[7..] {
cache.entry(*key, &font).expect("each mask rasterizes");
}
let misses = cache.misses();
cache
.entry(keys[0], &font)
.expect("the mask is still cached");
assert_eq!(cache.misses(), misses, "the mask survived the rotation");
}