use std::collections::HashMap;
use skia_safe::Font;
use super::request::{FaceRequest, Toggle};
use super::FontRegistry;
use crate::render::dimension::Pt;
const SYNTHETIC_OBLIQUE_SKEW_X: f32 = -0.249;
#[derive(Hash, Eq, PartialEq)]
struct FontKey {
family: String,
size_bits: u32,
bold: Toggle,
italic: Toggle,
}
struct LastCall {
family: String,
size_bits: u32,
bold: Toggle,
italic: Toggle,
idx: usize,
}
#[derive(Default)]
pub struct FontCache {
fonts: Vec<Font>,
index: HashMap<FontKey, usize>,
last: Option<LastCall>,
}
impl FontCache {
pub fn new() -> Self {
Self::default()
}
pub fn get(
&mut self,
registry: &FontRegistry,
font_family: &str,
font_size: Pt,
bold: Toggle,
italic: Toggle,
) -> &Font {
self.get_indexed(registry, font_family, font_size, bold, italic)
.1
}
pub fn get_indexed(
&mut self,
registry: &FontRegistry,
font_family: &str,
font_size: Pt,
bold: Toggle,
italic: Toggle,
) -> (usize, &Font) {
let size_bits = f32::from(font_size).to_bits();
let hit = self.last.as_ref().and_then(|last| {
(last.size_bits == size_bits
&& last.bold == bold
&& last.italic == italic
&& last.family == font_family)
.then_some(last.idx)
});
if let Some(idx) = hit {
return (idx, &self.fonts[idx]);
}
let key = FontKey {
family: font_family.to_lowercase(),
size_bits,
bold,
italic,
};
let idx = match self.index.get(&key) {
Some(&i) => i,
None => {
let entry = registry.resolve(&FaceRequest::new(font_family, bold, italic));
let mut font = Font::from_typeface(entry.typeface, f32::from(font_size));
font.set_subpixel(true);
font.set_linear_metrics(true);
font.set_hinting(skia_safe::FontHinting::None);
if entry.synthesis.embolden {
font.set_embolden(true);
}
if entry.synthesis.oblique {
font.set_skew_x(SYNTHETIC_OBLIQUE_SKEW_X);
}
let i = self.fonts.len();
self.fonts.push(font);
self.index.insert(key, i);
i
}
};
self.last = Some(LastCall {
family: font_family.to_owned(),
size_bits,
bold,
italic,
idx,
});
(idx, &self.fonts[idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::EmbeddedFontVariant;
use crate::render::fonts::tests::arbitrary_system_font_bytes;
use crate::render::fonts::FaceRequest;
use crate::render::fonts::Toggle;
use crate::render::fonts::{TypefaceId, TypefaceOrigin};
use skia_safe::{Data, FontMgr};
fn fixture_bytes(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("test-files/fonts")
.join(name);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture '{name}' is missing ({e}) — run scripts/make_font_fixtures.py")
})
}
fn dx_registry() -> FontRegistry {
let mut r = FontRegistry::new(FontMgr::new());
r.register_embedded(
"Dx",
EmbeddedFontVariant::Regular,
fixture_bytes("Dx-Regular.ttf"),
)
.expect("Dx-Regular.ttf must register");
r
}
#[test]
fn font_cache_uses_registry_after_replacement() {
let mut r = FontRegistry::new(FontMgr::new());
let original = r.resolve(&FaceRequest::plain("CacheReplaceProbe"));
let original_id = TypefaceId::from(&original.typeface);
let bytes = arbitrary_system_font_bytes();
let data = Data::new_copy(&bytes);
let replacement = r
.font_mgr()
.new_from_data(&data, 0)
.expect("replacement typeface should construct");
let new_id = TypefaceId::from(&replacement);
assert_ne!(
new_id, original_id,
"test precondition — replacement must be a different typeface"
);
let replacement_origin = TypefaceOrigin::System {
typeface_id: new_id,
};
r.replace_typeface_by_id(original_id, replacement, replacement_origin);
let mut fresh_cache = FontCache::new();
let font = fresh_cache.get(
&r,
"CacheReplaceProbe",
Pt::new(12.0),
Toggle::Absent,
Toggle::Absent,
);
assert_eq!(
TypefaceId::from(&font.typeface()),
new_id,
"FontCache must observe the post-replacement typeface"
);
}
#[test]
fn embolden_is_applied_to_the_cached_font() {
let r = dx_registry();
let mut cache = FontCache::new();
let font = cache.get(&r, "Dx", Pt::new(12.0), Toggle::On, Toggle::Absent);
assert!(
font.is_embolden(),
"the single-face fixture has nothing bolder to select"
);
}
#[test]
fn oblique_is_applied_to_the_cached_font() {
let r = dx_registry();
let mut cache = FontCache::new();
let font = cache.get(&r, "Dx", Pt::new(12.0), Toggle::Absent, Toggle::On);
assert_ne!(
font.skew_x(),
0.0,
"the single-face fixture has nothing slanted to select"
);
}
#[test]
fn a_plain_request_synthesises_neither() {
let r = dx_registry();
let mut cache = FontCache::new();
let font = cache.get(&r, "Dx", Pt::new(12.0), Toggle::Absent, Toggle::Absent);
assert!(!font.is_embolden());
assert_eq!(font.skew_x(), 0.0);
}
#[test]
fn an_oblique_font_leans_the_way_a_reader_expects() {
let r = dx_registry();
let mut cache = FontCache::new();
let font = cache.get(&r, "Dx", Pt::new(1000.0), Toggle::Absent, Toggle::On);
let glyphs = font.text_to_glyphs_vec("A");
let path = font.get_path(glyphs[0]).expect("glyph must have a path");
let points = path.points();
let top_x = points
.iter()
.min_by(|a, b| a.y.total_cmp(&b.y))
.expect("glyph has points")
.x;
let bottom_x = points
.iter()
.filter(|p| p.y == 0.0)
.map(|p| p.x)
.fold(f32::INFINITY, f32::min);
assert!(
top_x > bottom_x,
"the top of the glyph must lean right of the bottom: top={top_x} bottom={bottom_x}"
);
}
}