use std::collections::HashMap;
use super::font::Face;
pub const ATLAS_SIZE: u32 = 1024;
pub const ICON_SIZE: u32 = 32;
const ICON_COLUMNS: u32 = ATLAS_SIZE / ICON_SIZE;
const ICON_ROWS: u32 = 8;
const ICON_SLOTS: u32 = ICON_COLUMNS * ICON_ROWS;
const GLYPH_TOP: u32 = ICON_SIZE * ICON_ROWS;
pub const GLYPH_SIZE: u32 = 64;
const GLYPH_COLUMNS: u32 = ATLAS_SIZE / GLYPH_SIZE;
const GLYPH_ROWS: u32 = (ATLAS_SIZE - GLYPH_TOP) / GLYPH_SIZE;
const GLYPH_SLOTS: u32 = GLYPH_COLUMNS * GLYPH_ROWS;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Uv {
pub min: [f32; 2],
pub max: [f32; 2],
}
#[derive(bevy_ecs::prelude::Resource)]
pub struct Atlas {
icons: HashMap<&'static str, Uv>,
glyphs: HashMap<char, Uv>,
next_icon: u32,
next_glyph: u32,
pixels: Vec<u8>,
dirty: bool,
}
impl Default for Atlas {
fn default() -> Self {
Self::new()
}
}
impl Atlas {
pub fn new() -> Self {
Self {
icons: HashMap::new(),
glyphs: HashMap::new(),
next_icon: 0,
next_glyph: 0,
pixels: vec![0; (ATLAS_SIZE * ATLAS_SIZE * 4) as usize],
dirty: true,
}
}
pub fn icon(&mut self, icon: &'static str) -> Option<Uv> {
if let Some(uv) = self.icons.get(icon) {
return Some(*uv);
}
if self.next_icon >= ICON_SLOTS {
log::warn!("the icon band is full; {icon} will not be drawn");
return None;
}
let Some(svg) = super::icons::source(icon) else {
log::warn!("{icon} is not compiled in; add it to `icons::embedded`");
return None;
};
let Some(raster) = super::icons::rasterize(svg.as_bytes(), ICON_SIZE) else {
log::warn!("cannot rasterise {icon}");
return None;
};
let slot = self.next_icon;
self.next_icon += 1;
let (column, row) = (slot % ICON_COLUMNS, slot / ICON_COLUMNS);
let (x0, y0) = (column * ICON_SIZE, row * ICON_SIZE);
self.blit(&raster, x0, y0, ICON_SIZE);
let uv = uv_of(x0, y0, ICON_SIZE);
self.icons.insert(icon, uv);
Some(uv)
}
pub fn glyph(&mut self, face: &Face, ch: char) -> Option<Uv> {
if let Some(uv) = self.glyphs.get(&ch) {
return Some(*uv);
}
if self.next_glyph >= GLYPH_SLOTS {
log::warn!("the glyph band is full; {ch:?} will not be drawn");
return None;
}
let raster = face.rasterize(ch, GLYPH_SIZE)?;
let slot = self.next_glyph;
self.next_glyph += 1;
let (column, row) = (slot % GLYPH_COLUMNS, slot / GLYPH_COLUMNS);
let (x0, y0) = (column * GLYPH_SIZE, GLYPH_TOP + row * GLYPH_SIZE);
self.blit(&raster, x0, y0, GLYPH_SIZE);
let uv = uv_of(x0, y0, GLYPH_SIZE);
self.glyphs.insert(ch, uv);
Some(uv)
}
pub fn forget_glyphs(&mut self) {
self.glyphs.clear();
self.next_glyph = 0;
let from = (GLYPH_TOP * ATLAS_SIZE * 4) as usize;
self.pixels[from..].fill(0);
self.dirty = true;
}
fn blit(&mut self, raster: &[u8], x0: u32, y0: u32, size: u32) {
for y in 0..size {
let from = (y * size * 4) as usize;
let to = (((y0 + y) * ATLAS_SIZE + x0) * 4) as usize;
let width = (size * 4) as usize;
self.pixels[to..to + width].copy_from_slice(&raster[from..from + width]);
}
self.dirty = true;
}
pub fn take_if_dirty(&mut self) -> Option<&[u8]> {
match std::mem::take(&mut self.dirty) {
true => Some(&self.pixels),
false => None,
}
}
pub fn len(&self) -> usize {
self.icons.len()
}
pub fn is_empty(&self) -> bool {
self.icons.is_empty()
}
pub fn glyphs(&self) -> usize {
self.glyphs.len()
}
}
fn uv_of(x0: u32, y0: u32, size: u32) -> Uv {
let atlas = ATLAS_SIZE as f32;
let inset = 0.5 / atlas;
Uv {
min: [x0 as f32 / atlas + inset, y0 as f32 / atlas + inset],
max: [
(x0 + size) as f32 / atlas - inset,
(y0 + size) as f32 / atlas - inset,
],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::font::Family;
#[test]
fn the_bands_do_not_overlap() {
assert_eq!(GLYPH_TOP % ICON_SIZE, 0, "the icon band ends on a cell");
assert_eq!(GLYPH_TOP % GLYPH_SIZE, 0, "the glyph band starts on one");
assert!(GLYPH_TOP + GLYPH_ROWS * GLYPH_SIZE <= ATLAS_SIZE);
}
#[test]
fn the_glyph_band_holds_printable_ascii() {
let printable = (0x20u8..0x7f).count() as u32;
assert!(
GLYPH_SLOTS >= printable,
"{GLYPH_SLOTS} cells for {printable} characters",
);
}
#[test]
fn an_icon_is_rasterised_once_and_kept() {
let mut atlas = Atlas::new();
let first = atlas.icon(super::super::icons::path::CUBE).expect("a cube");
let again = atlas.icon(super::super::icons::path::CUBE).expect("again");
assert_eq!(first, again, "the same icon should keep its cell");
assert_eq!(atlas.len(), 1);
let other = atlas
.icon(super::super::icons::path::LIGHTBULB)
.expect("a light");
assert_ne!(first, other, "two icons cannot share a cell");
assert_eq!(atlas.len(), 2);
}
#[test]
fn an_icon_that_is_not_compiled_in_is_nothing_rather_than_wrong() {
let mut atlas = Atlas::new();
assert!(atlas.icon("icons/no-such-icon.svg").is_none());
assert!(atlas.is_empty());
}
#[test]
fn icons_and_glyphs_do_not_share_cells() {
let mut atlas = Atlas::new();
let face = Face::new(Family::Neon);
let icon = atlas.icon(super::super::icons::path::CUBE).expect("a cube");
let glyph = atlas.glyph(&face, 'A').expect("an A");
assert!(
glyph.min[1] > icon.max[1],
"the glyph band sits below the icon band: {glyph:?} against {icon:?}",
);
}
#[test]
fn a_glyph_is_rasterised_once_and_remembered() {
let mut atlas = Atlas::new();
let face = Face::new(Family::Neon);
let first = atlas.glyph(&face, 'A').expect("A is in the font");
assert_eq!(atlas.glyphs(), 1);
let again = atlas.glyph(&face, 'A').expect("A is still there");
assert_eq!(first, again, "the same cell, not a second one");
assert_eq!(atlas.glyphs(), 1, "and nothing new was rasterised");
}
#[test]
fn a_space_takes_no_cell() {
let mut atlas = Atlas::new();
let face = Face::new(Family::Neon);
assert!(atlas.glyph(&face, ' ').is_none());
assert_eq!(atlas.glyphs(), 0);
}
#[test]
fn changing_face_forgets_the_glyphs_and_keeps_the_icons() {
let mut atlas = Atlas::new();
let face = Face::new(Family::Neon);
atlas.glyph(&face, 'A');
atlas.icon(crate::ui::icons::path::CUBE);
assert_eq!((atlas.glyphs(), atlas.len()), (1, 1));
atlas.forget_glyphs();
assert_eq!(atlas.glyphs(), 0, "the letters go");
assert_eq!(atlas.len(), 1, "the icons stay");
}
#[test]
fn forgetting_the_glyphs_wipes_what_they_drew() {
let mut atlas = Atlas::new();
atlas.glyph(&Face::new(Family::Neon), 'A');
atlas.take_if_dirty();
atlas.forget_glyphs();
let pixels = atlas.take_if_dirty().expect("wiping is a change");
let band = &pixels[(GLYPH_TOP * ATLAS_SIZE * 4) as usize..];
assert!(band.iter().all(|&b| b == 0), "the band is blank again");
}
}