pub mod bitmap_8x8;
pub use bitmap_8x8::{CHAR_H, CHAR_W, FONT_8X8, glyph};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use crate::core::resource::HasProbe;
use crate::ecs::World;
#[derive(Clone, Debug)]
pub struct Glyph {
pub advance: u16,
pub bitmap: &'static [u8],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FontMetrics {
pub ascender: u16,
pub descender: u16,
pub line_height: u16,
}
pub trait FontProvider: 'static {
fn glyph(&self, ch: char) -> Option<Glyph>;
fn metrics(&self) -> FontMetrics;
}
pub enum FontBackend {
Bitmap8x8,
Custom(Box<dyn FontProvider>),
}
impl core::fmt::Debug for FontBackend {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FontBackend::Bitmap8x8 => f.write_str("Bitmap8x8"),
FontBackend::Custom(_) => f.write_str("Custom(<dyn FontProvider>)"),
}
}
}
#[derive(Debug)]
pub struct Font {
pub family: &'static str,
pub size: u16,
pub backend: FontBackend,
}
impl Font {
pub fn bitmap_8x8() -> Self {
Self {
family: "bitmap8x8",
size: 8,
backend: FontBackend::Bitmap8x8,
}
}
pub fn glyph(&self, ch: char) -> Option<Glyph> {
match &self.backend {
FontBackend::Bitmap8x8 => bitmap_8x8_glyph(ch),
FontBackend::Custom(p) => p.glyph(ch),
}
}
pub fn metrics(&self) -> FontMetrics {
match &self.backend {
FontBackend::Bitmap8x8 => BITMAP_8X8_METRICS,
FontBackend::Custom(p) => p.metrics(),
}
}
}
const BITMAP_8X8_METRICS: FontMetrics = FontMetrics {
ascender: 7,
descender: 1,
line_height: 8,
};
fn bitmap_8x8_glyph(ch: char) -> Option<Glyph> {
let byte = if ('\u{20}'..'\u{7f}').contains(&ch) {
ch as u8
} else {
b'?'
};
let bitmap: &'static [u8; 8] = bitmap_8x8::glyph(byte);
Some(Glyph {
advance: bitmap_8x8::CHAR_W as u16,
bitmap,
})
}
impl crate::core::cache::HasSize for FontMetrics {
fn cache_size(&self) -> usize {
core::mem::size_of::<Self>()
}
}
impl HasProbe for Font {
type Meta = FontMetrics;
fn extract_meta(&self) -> Self::Meta {
self.metrics()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FontToken {
Default,
Heading,
Mono,
Custom(&'static str),
}
pub struct FontRegistry {
fonts: BTreeMap<FontToken, Font>,
}
impl FontRegistry {
pub fn new() -> Self {
let mut fonts = BTreeMap::new();
fonts.insert(FontToken::Default, Font::bitmap_8x8());
Self { fonts }
}
pub fn set(&mut self, token: FontToken, font: Font) -> &mut Self {
self.fonts.insert(token, font);
self
}
pub fn resolve(&self, token: &FontToken) -> &Font {
self.fonts
.get(token)
.or_else(|| self.fonts.get(&FontToken::Default))
.expect("FontRegistry::Default invariant")
}
pub fn contains(&self, token: &FontToken) -> bool {
self.fonts.contains_key(token)
}
}
impl Default for FontRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn resolve_or_default<'a>(world: &'a World, token: &FontToken) -> Option<&'a Font> {
world.resource::<FontRegistry>().map(|r| r.resolve(token))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_token_resolves_to_bitmap8x8() {
let reg = FontRegistry::new();
let f = reg.resolve(&FontToken::Default);
assert_eq!(f.family, "bitmap8x8");
assert_eq!(f.size, 8);
}
#[test]
fn unbound_token_falls_back_to_default() {
let reg = FontRegistry::new();
let f = reg.resolve(&FontToken::Heading);
assert_eq!(f.family, "bitmap8x8");
assert!(!reg.contains(&FontToken::Heading));
}
#[test]
fn set_overrides_token_resolution() {
let mut reg = FontRegistry::new();
reg.set(
FontToken::Heading,
Font {
family: "fake-heading",
size: 16,
backend: FontBackend::Bitmap8x8,
},
);
let f = reg.resolve(&FontToken::Heading);
assert_eq!(f.family, "fake-heading");
assert_eq!(f.size, 16);
assert!(reg.contains(&FontToken::Heading));
}
#[test]
fn set_default_overrides_bundled_font() {
let mut reg = FontRegistry::new();
reg.set(
FontToken::Default,
Font {
family: "user-default",
size: 12,
backend: FontBackend::Bitmap8x8,
},
);
assert_eq!(reg.resolve(&FontToken::Default).family, "user-default");
}
#[test]
fn custom_token_resolves_when_registered() {
let mut reg = FontRegistry::new();
let token = FontToken::Custom("brand");
reg.set(
token.clone(),
Font {
family: "brand-face",
size: 10,
backend: FontBackend::Bitmap8x8,
},
);
assert_eq!(reg.resolve(&token).family, "brand-face");
}
#[test]
fn metrics_is_extractable_via_has_probe() {
let font = Font::bitmap_8x8();
let meta = font.extract_meta();
assert_eq!(meta, BITMAP_8X8_METRICS);
assert_eq!(meta.line_height, 8);
}
#[test]
fn glyph_roundtrip_for_ascii() {
let font = Font::bitmap_8x8();
let g = font.glyph('A').expect("ASCII glyph");
assert_eq!(g.advance, 8);
assert_eq!(g.bitmap.len(), 8);
}
#[test]
fn glyph_falls_back_to_question_mark_outside_ascii() {
let font = Font::bitmap_8x8();
let g = font.glyph('日').expect("fallback glyph");
let q = font.glyph('?').expect("? glyph");
assert_eq!(g.bitmap, q.bitmap);
}
#[test]
fn custom_provider_routes_through_backend() {
struct AllX;
impl FontProvider for AllX {
fn glyph(&self, _ch: char) -> Option<Glyph> {
Some(Glyph {
advance: 6,
bitmap: &[0xFF; 8],
})
}
fn metrics(&self) -> FontMetrics {
FontMetrics {
ascender: 6,
descender: 0,
line_height: 6,
}
}
}
let font = Font {
family: "all-x",
size: 6,
backend: FontBackend::Custom(Box::new(AllX)),
};
assert_eq!(font.glyph('A').unwrap().advance, 6);
assert_eq!(font.metrics().line_height, 6);
}
}