use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::geometry::Color;
use crate::painter::Painter;
pub struct Font {
inner: fontdue::Font,
glyphs: RefCell<LruCache<(char, u32), Rc<Glyph>>>,
advances: RefCell<HashMap<(char, u32), f32>>,
}
const GLYPH_CACHE_CAP: usize = 1024;
struct Glyph {
metrics: fontdue::Metrics,
bitmap: Vec<u8>,
}
struct LruCache<K, V> {
entries: HashMap<K, (V, u64)>,
clock: u64,
capacity: usize,
}
impl<K: Eq + std::hash::Hash + Copy, V: Clone> LruCache<K, V> {
fn new(capacity: usize) -> Self {
Self {
entries: HashMap::new(),
clock: 0,
capacity: capacity.max(1),
}
}
fn get(&mut self, key: &K) -> Option<V> {
self.clock += 1;
let stamp = self.clock;
let slot = self.entries.get_mut(key)?;
slot.1 = stamp;
Some(slot.0.clone())
}
fn insert(&mut self, key: K, value: V) {
self.clock += 1;
if self.entries.len() >= self.capacity
&& !self.entries.contains_key(&key)
&& let Some(lru) = self
.entries
.iter()
.min_by_key(|(_, (_, stamp))| *stamp)
.map(|(k, _)| *k)
{
self.entries.remove(&lru);
}
self.entries.insert(key, (value, self.clock));
}
}
impl Font {
fn new(inner: fontdue::Font) -> Self {
Self {
inner,
glyphs: RefCell::new(LruCache::new(GLYPH_CACHE_CAP)),
advances: RefCell::new(HashMap::new()),
}
}
pub fn load_system() -> Option<Self> {
const SANS_FAMILIES: &[&str] = &[
"MS Sans Serif",
"Microsoft Sans Serif",
"Tahoma",
"Segoe UI",
"Arial",
"Helvetica",
"Geneva",
"DejaVu Sans",
"Liberation Sans",
];
load_family_chain(SANS_FAMILIES, false)
}
pub fn load_monospace() -> Option<Self> {
const MONO_FAMILIES: &[&str] = &[
"Lucida Console",
"Consolas",
"Courier New",
"Courier",
"Liberation Mono",
"DejaVu Sans Mono",
"Menlo",
"Monaco",
];
load_family_chain(MONO_FAMILIES, true)
}
pub fn from_bytes(data: Vec<u8>) -> Option<Self> {
fontdue::Font::from_bytes(data, fontdue::FontSettings::default())
.ok()
.map(Self::new)
}
fn advance(&self, ch: char, size: f32) -> f32 {
let key = (ch, size.to_bits());
if let Some(a) = self.advances.borrow().get(&key) {
return *a;
}
let a = self.inner.metrics(ch, size).advance_width;
self.advances.borrow_mut().insert(key, a);
a
}
fn glyph(&self, ch: char, size_phys: f32) -> Rc<Glyph> {
let key = (ch, size_phys.to_bits());
if let Some(g) = self.glyphs.borrow_mut().get(&key) {
return g;
}
let (metrics, bitmap) = self.inner.rasterize(ch, size_phys);
let g = Rc::new(Glyph { metrics, bitmap });
self.glyphs.borrow_mut().insert(key, g.clone());
g
}
pub fn measure(&self, text: &str, size: f32) -> (f32, f32) {
let width: f32 = text.chars().map(|ch| self.advance(ch, size)).sum();
(width, size * 1.2)
}
pub fn cumulative_widths(&self, text: &str, size: f32) -> Vec<i32> {
let mut out = Vec::with_capacity(text.len() + 1);
let mut acc = 0.0_f32;
out.push(0);
for ch in text.chars() {
acc += self.advance(ch, size);
out.push(acc.ceil() as i32);
}
out
}
pub(crate) fn draw_phys(
&self,
painter: &mut Painter,
text: &str,
x: f32,
y: f32,
size_phys: f32,
color: Color,
) -> f32 {
let baseline = y + size_phys;
let (clip_lo, clip_hi) = painter.glyph_clip_x();
let mut pen_x = x;
for ch in text.chars() {
let glyph = self.glyph(ch, size_phys);
let metrics = &glyph.metrics;
let glyph_x = pen_x + metrics.xmin as f32;
if glyph_x >= clip_hi as f32 {
break;
}
if glyph_x + metrics.width as f32 <= clip_lo as f32 {
pen_x += metrics.advance_width;
continue;
}
let glyph_y = baseline - metrics.ymin as f32 - metrics.height as f32;
for row in 0..metrics.height {
let dy = glyph_y as i32 + row as i32;
let src_row = row * metrics.width;
for col in 0..metrics.width {
let alpha = glyph.bitmap[src_row + col];
if alpha == 0 {
continue;
}
let dx = glyph_x as i32 + col as i32;
painter.blend_pixel_phys(dx, dy, color, alpha);
}
}
pen_x += metrics.advance_width;
}
pen_x
}
}
fn load_face(db: &fontdb::Database, id: fontdb::ID) -> Option<fontdue::Font> {
let mut data: Option<Vec<u8>> = None;
db.with_face_data(id, |bytes, _| data = Some(bytes.to_vec()));
let data = data?;
fontdue::Font::from_bytes(data, fontdue::FontSettings::default()).ok()
}
fn load_family_chain(families: &[&str], monospace_fallback: bool) -> Option<Font> {
let mut db = fontdb::Database::new();
db.load_system_fonts();
if db.faces().next().is_none() {
db.load_fonts_dir("/usr/local/share/fonts");
}
for family in families {
let query = fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight: fontdb::Weight::NORMAL,
stretch: fontdb::Stretch::Normal,
style: fontdb::Style::Normal,
};
if let Some(id) = db.query(&query)
&& let Some(font) = load_face(&db, id)
{
return Some(Font::new(font));
}
}
if monospace_fallback {
for face in db.faces() {
if face.monospaced
&& let Some(font) = load_face(&db, face.id)
{
return Some(Font::new(font));
}
}
}
for face in db.faces() {
if let Some(font) = load_face(&db, face.id) {
return Some(Font::new(font));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
impl<K: Eq + std::hash::Hash + Copy, V: Clone> LruCache<K, V> {
fn len(&self) -> usize {
self.entries.len()
}
fn contains(&self, key: &K) -> bool {
self.entries.contains_key(key)
}
}
#[test]
fn evicts_the_least_recently_used_entry() {
let mut cache: LruCache<i32, i32> = LruCache::new(2);
cache.insert(1, 10);
cache.insert(2, 20);
assert_eq!(cache.get(&1), Some(10));
cache.insert(3, 30);
assert_eq!(cache.len(), 2);
assert!(cache.contains(&1));
assert!(!cache.contains(&2), "the untouched entry is evicted");
assert!(cache.contains(&3));
}
#[test]
fn overwriting_an_existing_key_never_evicts() {
let mut cache: LruCache<i32, i32> = LruCache::new(2);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(1, 11);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(&1), Some(11));
assert!(cache.contains(&2));
}
#[test]
fn a_miss_returns_none() {
let mut cache: LruCache<i32, i32> = LruCache::new(4);
assert_eq!(cache.get(&99), None);
}
}