use fontdue::{Font, FontSettings};
const DEFAULT_FONT: &[u8] = include_bytes!("../assets/Roboto-Regular.ttf");
#[derive(Debug, Clone)]
pub(crate) struct RasterizedGlyph {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub advance: f32,
pub offset_x: f32,
pub offset_y: f32,
}
pub(crate) struct GlyphAtlas {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
glyphs: Vec<Option<RasterizedGlyph>>,
font: Font,
size: f32,
}
impl GlyphAtlas {
pub fn new(size: f32) -> Self {
let font = Font::from_bytes(DEFAULT_FONT, FontSettings::default())
.expect("embedded font must be valid");
let mut atlas = Self {
width: 512,
height: 512,
pixels: vec![0; 512 * 512],
glyphs: vec![None; 128],
font,
size,
};
atlas.rasterize_ascii();
atlas
}
fn rasterize_ascii(&mut self) {
let mut cursor_x: u32 = 0;
let mut cursor_y: u32 = 0;
let mut row_height: u32 = 0;
for c in 32u8..127 {
let (metrics, bitmap) = self.font.rasterize(c as char, self.size);
if metrics.width == 0 || metrics.height == 0 {
self.glyphs[c as usize] = Some(RasterizedGlyph {
x: 0,
y: 0,
width: 0,
height: 0,
advance: metrics.advance_width,
offset_x: metrics.xmin as f32,
offset_y: metrics.ymin as f32,
});
continue;
}
let w = metrics.width as u32;
let h = metrics.height as u32;
if cursor_x + w + 1 > self.width {
cursor_x = 0;
cursor_y += row_height + 1;
row_height = 0;
}
if cursor_y + h + 1 > self.height {
self.grow();
self.rasterize_ascii();
return;
}
for row in 0..h {
let src_start = (row * w) as usize;
let dst_start = ((cursor_y + row) * self.width + cursor_x) as usize;
self.pixels[dst_start..dst_start + w as usize]
.copy_from_slice(&bitmap[src_start..src_start + w as usize]);
}
self.glyphs[c as usize] = Some(RasterizedGlyph {
x: cursor_x,
y: cursor_y,
width: w,
height: h,
advance: metrics.advance_width,
offset_x: metrics.xmin as f32,
offset_y: metrics.ymin as f32,
});
cursor_x += w + 1;
row_height = row_height.max(h);
}
}
fn grow(&mut self) {
self.height *= 2;
self.pixels.resize((self.width * self.height) as usize, 0);
}
pub fn glyph(&self, c: char) -> Option<&RasterizedGlyph> {
let idx = c as usize;
if idx < self.glyphs.len() {
self.glyphs[idx].as_ref()
} else {
None
}
}
pub fn measure(&self, text: &str) -> (f32, f32) {
let mut line_width: f32 = 0.0;
let mut max_width: f32 = 0.0;
let mut lines = 1_u32;
for c in text.chars() {
if c == '\n' {
max_width = max_width.max(line_width);
line_width = 0.0;
lines += 1;
continue;
}
if let Some(g) = self.glyph(c) {
line_width += g.advance;
}
}
(max_width.max(line_width), lines as f32 * self.size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atlas_creation() {
let atlas = GlyphAtlas::new(16.0);
assert!(atlas.width >= 512);
assert!(atlas.height >= 512);
assert!(!atlas.pixels.is_empty());
}
#[test]
fn ascii_glyphs_rasterized() {
let atlas = GlyphAtlas::new(16.0);
assert!(atlas.glyph('A').is_some());
assert!(atlas.glyph('z').is_some());
assert!(atlas.glyph(' ').is_some());
}
#[test]
fn non_ascii_returns_none() {
let atlas = GlyphAtlas::new(16.0);
assert!(atlas.glyph('\u{1F600}').is_none());
}
#[test]
fn glyph_has_positive_dimensions() {
let atlas = GlyphAtlas::new(16.0);
let g = atlas.glyph('A').unwrap();
assert!(g.width > 0);
assert!(g.height > 0);
assert!(g.advance > 0.0);
}
#[test]
fn measure_empty_string() {
let atlas = GlyphAtlas::new(16.0);
let (w, h) = atlas.measure("");
assert_eq!(w, 0.0);
assert_eq!(h, 16.0);
}
#[test]
fn measure_single_char() {
let atlas = GlyphAtlas::new(16.0);
let (w, _h) = atlas.measure("A");
assert!(w > 0.0);
}
#[test]
fn measure_multiline() {
let atlas = GlyphAtlas::new(16.0);
let (w, h) = atlas.measure("AAAA\nB");
let (first_line_width, _) = atlas.measure("AAAA");
assert!((w - first_line_width).abs() < f32::EPSILON);
assert!((h - 32.0).abs() < f32::EPSILON);
}
#[test]
fn measure_multiline_uses_widest_line() {
let atlas = GlyphAtlas::new(16.0);
let (w, _) = atlas.measure("A\nBBBB");
let (second_line_width, _) = atlas.measure("BBBB");
assert!((w - second_line_width).abs() < f32::EPSILON);
}
#[test]
fn space_glyph_has_zero_dimensions_but_advance() {
let atlas = GlyphAtlas::new(16.0);
let g = atlas.glyph(' ').unwrap();
assert_eq!(g.width, 0);
assert!(g.advance > 0.0);
}
}