use denise::Size;
use crate::source::{FontMetrics, GlyphId, GlyphMetrics, GlyphSource, Rasterised};
#[derive(Debug)]
pub struct BakedFont {
pub name: &'static str,
pub sizes: &'static [BakedSize],
pub coverage: &'static [u8],
}
#[derive(Debug)]
pub struct BakedSize {
pub size_px: u16,
pub metrics: FontMetrics,
pub glyphs: &'static [BakedGlyph],
}
#[derive(Clone, Copy, Debug)]
pub struct BakedGlyph {
pub ch: char,
pub advance: i16,
pub bearing_x: i16,
pub bearing_y: i16,
pub width: u16,
pub height: u16,
pub offset: u32,
}
impl BakedGlyph {
pub(crate) const fn metrics(&self) -> GlyphMetrics {
GlyphMetrics {
advance: self.advance as i32,
bearing_x: self.bearing_x as i32,
bearing_y: self.bearing_y as i32,
size: Size::new(self.width as u32, self.height as u32),
}
}
}
impl BakedSize {
fn glyph(&self, ch: char) -> Option<&BakedGlyph> {
self.glyphs
.binary_search_by_key(&ch, |glyph| glyph.ch)
.ok()
.map(|index| &self.glyphs[index])
}
}
#[derive(Debug)]
pub struct BakedSource {
font: &'static BakedFont,
}
impl BakedSource {
pub const fn new(font: &'static BakedFont) -> Self {
Self { font }
}
fn nearest(&self, size_px: u16) -> Option<&BakedSize> {
self.font.sizes.iter().min_by_key(|size| {
let distance = size.size_px.abs_diff(size_px);
(distance, size.size_px)
})
}
fn lookup(&self, glyph: GlyphId, size_px: u16) -> Option<&BakedGlyph> {
self.nearest(size_px)?.glyph(glyph.as_char()?)
}
}
impl GlyphSource for BakedSource {
fn name(&self) -> &str {
self.font.name
}
fn metrics(&self, size_px: u16) -> FontMetrics {
self.nearest(size_px)
.map(|size| size.metrics)
.unwrap_or_default()
}
fn glyph_metrics(&mut self, glyph: GlyphId, size_px: u16) -> Option<GlyphMetrics> {
self.lookup(glyph, size_px).map(BakedGlyph::metrics)
}
fn rasterise(&mut self, glyph: GlyphId, size_px: u16) -> Option<Rasterised<'_>> {
let glyph = self.lookup(glyph, size_px)?;
let metrics = glyph.metrics();
let (start, len) = (
glyph.offset as usize,
glyph.width as usize * glyph.height as usize,
);
let coverage = self.font.coverage.get(start..start + len).unwrap_or(&[]);
Some(Rasterised {
metrics: if coverage.len() == len {
metrics
} else {
GlyphMetrics {
size: Size::new(0, 0),
..metrics
}
},
coverage,
stride: glyph.width as usize,
})
}
fn contains(&self, ch: char) -> bool {
ch != '\0'
&& self
.font
.sizes
.first()
.is_some_and(|size| size.glyph(ch).is_some())
}
fn fallback_id(&self, _ch: char) -> Option<GlyphId> {
Some(GlyphId::from_char('\0'))
}
fn snap_size(&self, size_px: u16) -> u16 {
self.nearest(size_px).map_or(size_px, |size| size.size_px)
}
}
#[cfg(test)]
mod tests {
use super::*;
static TINY: BakedFont = BakedFont {
name: "tiny",
sizes: &[
BakedSize {
size_px: 8,
metrics: FontMetrics {
ascent: 6,
descent: 2,
line_gap: 1,
},
glyphs: &[
BakedGlyph {
ch: '\0',
advance: 3,
bearing_x: 0,
bearing_y: 2,
width: 2,
height: 2,
offset: 0,
},
BakedGlyph {
ch: 'a',
advance: 3,
bearing_x: 0,
bearing_y: 2,
width: 2,
height: 2,
offset: 4,
},
BakedGlyph {
ch: 'b',
advance: 3,
bearing_x: 0,
bearing_y: 0,
width: 0,
height: 0,
offset: 8,
},
],
},
BakedSize {
size_px: 16,
metrics: FontMetrics {
ascent: 12,
descent: 4,
line_gap: 2,
},
glyphs: &[
BakedGlyph {
ch: '\0',
advance: 6,
bearing_x: 0,
bearing_y: 4,
width: 4,
height: 4,
offset: 8,
},
BakedGlyph {
ch: 'a',
advance: 6,
bearing_x: 1,
bearing_y: 4,
width: 4,
height: 4,
offset: 24,
},
BakedGlyph {
ch: 'b',
advance: 6,
bearing_x: 0,
bearing_y: 0,
width: 0,
height: 0,
offset: 40,
},
],
},
],
coverage: &[
128, 128, 128, 128, 255, 255, 255, 255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, ],
};
#[test]
fn a_size_snaps_to_the_nearest_baked_one_and_downwards_on_a_tie() {
let source = BakedSource::new(&TINY);
assert_eq!(source.snap_size(8), 8);
assert_eq!(source.snap_size(9), 8);
assert_eq!(source.snap_size(15), 16);
assert_eq!(source.snap_size(12), 8, "half way: the smaller still fits");
assert_eq!(source.snap_size(40), 16, "nothing bigger to give");
assert_eq!(source.metrics(40).ascent, 12);
}
#[test]
fn a_glyph_is_a_slice_of_the_table_and_its_metrics_come_with_it() {
let mut source = BakedSource::new(&TINY);
let a = source.glyph_id('a').expect("baked");
let drawn = source.rasterise(a, 16).expect("ink");
assert_eq!(drawn.coverage, [255; 16]);
assert_eq!(drawn.stride, 4);
assert_eq!(
drawn.metrics,
GlyphMetrics {
advance: 6,
bearing_x: 1,
bearing_y: 4,
size: Size::new(4, 4),
}
);
let b = source.glyph_id('b').expect("baked");
let drawn = source.rasterise(b, 8).expect("an advance");
assert!(drawn.coverage.is_empty());
assert_eq!(drawn.metrics.advance, 3);
}
#[test]
fn a_character_that_was_not_baked_is_the_box() {
let mut source = BakedSource::new(&TINY);
assert!(source.contains('a'));
assert!(!source.contains('z'));
assert!(!source.contains('\0'), "the box is not a character");
assert_eq!(source.glyph_id('z'), None);
let fallback = source.fallback_id('z').expect("the box");
let drawn = source.rasterise(fallback, 8).expect("the box has ink");
assert_eq!(drawn.coverage, [128; 4]);
}
#[test]
fn a_table_that_points_past_its_coverage_draws_nothing_rather_than_panicking() {
static BROKEN: BakedFont = BakedFont {
name: "broken",
sizes: &[BakedSize {
size_px: 8,
metrics: FontMetrics {
ascent: 6,
descent: 2,
line_gap: 0,
},
glyphs: &[BakedGlyph {
ch: 'a',
advance: 3,
bearing_x: 0,
bearing_y: 2,
width: 2,
height: 2,
offset: 1000,
}],
}],
coverage: &[255; 4],
};
let mut source = BakedSource::new(&BROKEN);
let a = source.glyph_id('a').expect("listed");
let drawn = source.rasterise(a, 8).expect("listed");
assert!(drawn.coverage.is_empty());
assert!(drawn.metrics.is_blank());
assert_eq!(drawn.metrics.advance, 3, "it still takes its space");
}
#[test]
fn a_font_with_no_sizes_answers_nothing_and_panics_never() {
static EMPTY: BakedFont = BakedFont {
name: "empty",
sizes: &[],
coverage: &[],
};
let mut source = BakedSource::new(&EMPTY);
assert_eq!(source.snap_size(16), 16);
assert_eq!(source.metrics(16), FontMetrics::default());
assert!(!source.contains('a'));
assert!(source.rasterise(GlyphId::from_char('a'), 16).is_none());
}
}