use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::geometry::Color;
use crate::painter::Painter;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum FontStyle {
#[default]
Regular = 0,
Bold = 1,
Italic = 2,
BoldItalic = 3,
}
impl FontStyle {
pub fn new(bold: bool, italic: bool) -> Self {
match (bold, italic) {
(false, false) => FontStyle::Regular,
(true, false) => FontStyle::Bold,
(false, true) => FontStyle::Italic,
(true, true) => FontStyle::BoldItalic,
}
}
pub fn is_bold(self) -> bool {
matches!(self, FontStyle::Bold | FontStyle::BoldItalic)
}
pub fn is_italic(self) -> bool {
matches!(self, FontStyle::Italic | FontStyle::BoldItalic)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum FontFamily {
#[default]
Sans,
Serif,
Mono,
}
#[derive(Clone, Copy, Default)]
pub struct FontSet<'a> {
pub sans: Option<&'a Font>,
pub serif: Option<&'a Font>,
pub mono: Option<&'a Font>,
}
const STYLE_COUNT: usize = 4;
fn resolve_style(present: [bool; STYLE_COUNT], style: FontStyle) -> FontStyle {
use FontStyle::*;
let has = |s: FontStyle| present[s as usize];
match style {
Regular => Regular,
Bold => {
if has(Bold) {
Bold
} else {
Regular
}
}
Italic => {
if has(Italic) {
Italic
} else {
Regular
}
}
BoldItalic => {
if has(BoldItalic) {
BoldItalic
} else if has(Bold) {
Bold
} else if has(Italic) {
Italic
} else {
Regular
}
}
}
}
pub struct Font {
faces: [Option<fontdue::Font>; STYLE_COUNT],
glyphs: RefCell<LruCache<GlyphKey, Rc<Glyph>>>,
advances: RefCell<HashMap<GlyphKey, f32>>,
}
type GlyphKey = (char, u32, FontStyle);
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 from_face(regular: fontdue::Font) -> Self {
Self {
faces: [Some(regular), None, None, None],
glyphs: RefCell::new(LruCache::new(GLYPH_CACHE_CAP)),
advances: RefCell::new(HashMap::new()),
}
}
pub fn load_sans() -> 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_serif() -> Option<Self> {
const SERIF_FAMILIES: &[&str] = &[
"Times New Roman",
"Times",
"MS Serif",
"Georgia",
"Liberation Serif",
"DejaVu Serif",
"Noto Serif",
];
load_family_chain(SERIF_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_sans_bytes(data: Vec<u8>) -> Option<Self> {
fontdue::Font::from_bytes(data, fontdue::FontSettings::default())
.ok()
.map(Self::from_face)
}
pub fn with_bold_bytes(self, data: Vec<u8>) -> Self {
self.with_style_bytes(FontStyle::Bold, data)
}
pub fn with_italic_bytes(self, data: Vec<u8>) -> Self {
self.with_style_bytes(FontStyle::Italic, data)
}
pub fn with_bold_italic_bytes(self, data: Vec<u8>) -> Self {
self.with_style_bytes(FontStyle::BoldItalic, data)
}
fn with_style_bytes(mut self, style: FontStyle, data: Vec<u8>) -> Self {
if let Ok(face) = fontdue::Font::from_bytes(data, fontdue::FontSettings::default()) {
self.faces[style as usize] = Some(face);
}
self
}
fn presence(&self) -> [bool; STYLE_COUNT] {
std::array::from_fn(|i| self.faces[i].is_some())
}
fn face(&self, style: FontStyle) -> (&fontdue::Font, FontStyle) {
let resolved = resolve_style(self.presence(), style);
(
self.faces[resolved as usize]
.as_ref()
.expect("resolved face is loaded"),
resolved,
)
}
fn advance(&self, ch: char, size: f32, style: FontStyle) -> f32 {
let (face, resolved) = self.face(style);
let key = (ch, size.to_bits(), resolved);
if let Some(a) = self.advances.borrow().get(&key) {
return *a;
}
let a = face.metrics(ch, size).advance_width;
self.advances.borrow_mut().insert(key, a);
a
}
fn glyph(&self, ch: char, size_phys: f32, style: FontStyle) -> Rc<Glyph> {
let (face, resolved) = self.face(style);
let key = (ch, size_phys.to_bits(), resolved);
if let Some(g) = self.glyphs.borrow_mut().get(&key) {
return g;
}
let (metrics, bitmap) = face.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) {
self.measure_styled(text, size, FontStyle::Regular)
}
pub fn measure_styled(&self, text: &str, size: f32, style: FontStyle) -> (f32, f32) {
let width: f32 = text.chars().map(|ch| self.advance(ch, size, style)).sum();
(width, size * 1.2)
}
pub fn cumulative_widths(&self, text: &str, size: f32, style: FontStyle) -> 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, style);
out.push(acc.ceil() as i32);
}
out
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_phys(
&self,
painter: &mut Painter,
text: &str,
x: f32,
y: f32,
size_phys: f32,
color: Color,
style: FontStyle,
) -> 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, style);
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()
}
const BOLD_WEIGHT_THRESHOLD: u16 = 600;
fn query_verified(
db: &fontdb::Database,
family: &str,
weight: fontdb::Weight,
style: fontdb::Style,
) -> Option<fontdue::Font> {
let query = fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight,
stretch: fontdb::Stretch::Normal,
style,
};
let id = db.query(&query)?;
let info = db.face(id)?;
let want_bold = weight.0 >= BOLD_WEIGHT_THRESHOLD;
let want_slanted = style != fontdb::Style::Normal;
let is_bold = info.weight.0 >= BOLD_WEIGHT_THRESHOLD;
let is_slanted = info.style != fontdb::Style::Normal;
if is_bold != want_bold || is_slanted != want_slanted {
return None;
}
load_face(db, id)
}
fn load_styled_family(db: &fontdb::Database, family: &str) -> Option<Font> {
let regular = query_verified(db, family, fontdb::Weight::NORMAL, fontdb::Style::Normal)?;
let mut font = Font::from_face(regular);
let mut attach = |style: FontStyle, weight, slant| {
if let Some(face) = query_verified(db, family, weight, slant) {
font.faces[style as usize] = Some(face);
}
};
attach(FontStyle::Bold, fontdb::Weight::BOLD, fontdb::Style::Normal);
attach(
FontStyle::Italic,
fontdb::Weight::NORMAL,
fontdb::Style::Italic,
);
attach(
FontStyle::BoldItalic,
fontdb::Weight::BOLD,
fontdb::Style::Italic,
);
Some(font)
}
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 {
if let Some(font) = load_styled_family(&db, family) {
return Some(font);
}
}
if monospace_fallback {
for face in db.faces() {
if face.monospaced
&& let Some(font) = load_face(&db, face.id)
{
return Some(Font::from_face(font));
}
}
}
for face in db.faces() {
if let Some(font) = load_face(&db, face.id) {
return Some(Font::from_face(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);
}
#[test]
fn font_style_from_flags() {
assert_eq!(FontStyle::new(false, false), FontStyle::Regular);
assert_eq!(FontStyle::new(true, false), FontStyle::Bold);
assert_eq!(FontStyle::new(false, true), FontStyle::Italic);
assert_eq!(FontStyle::new(true, true), FontStyle::BoldItalic);
assert!(FontStyle::Bold.is_bold() && !FontStyle::Bold.is_italic());
assert!(FontStyle::Italic.is_italic() && !FontStyle::Italic.is_bold());
assert!(FontStyle::BoldItalic.is_bold() && FontStyle::BoldItalic.is_italic());
assert!(!FontStyle::Regular.is_bold() && !FontStyle::Regular.is_italic());
}
#[test]
fn resolve_uses_the_real_face_when_present() {
let all = [true, true, true, true];
assert_eq!(resolve_style(all, FontStyle::Bold), FontStyle::Bold);
assert_eq!(resolve_style(all, FontStyle::Italic), FontStyle::Italic);
assert_eq!(
resolve_style(all, FontStyle::BoldItalic),
FontStyle::BoldItalic
);
}
#[test]
fn resolve_falls_back_to_regular_when_a_face_is_missing() {
let only_regular = [true, false, false, false];
for style in [
FontStyle::Regular,
FontStyle::Bold,
FontStyle::Italic,
FontStyle::BoldItalic,
] {
assert_eq!(resolve_style(only_regular, style), FontStyle::Regular);
}
}
#[test]
fn resolve_bold_italic_prefers_the_closest_real_face() {
let no_bi_has_bold = [true, true, false, false];
assert_eq!(
resolve_style(no_bi_has_bold, FontStyle::BoldItalic),
FontStyle::Bold
);
let no_bi_has_italic = [true, false, true, false];
assert_eq!(
resolve_style(no_bi_has_italic, FontStyle::BoldItalic),
FontStyle::Italic
);
}
}