use ab_glyph::{Font as _, FontRef, PxScale, ScaleFont as _};
use super::atlas::{Atlas, Uv};
use super::color::{Color, linear_rgba};
use super::renderer2d::QuadInstance;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Family {
#[default]
Neon,
Argon,
Xenon,
Radon,
Krypton,
}
impl Family {
pub const ALL: [Family; 5] = [
Family::Neon,
Family::Argon,
Family::Xenon,
Family::Radon,
Family::Krypton,
];
pub fn label(self) -> &'static str {
match self {
Family::Neon => "Neon",
Family::Argon => "Argon",
Family::Xenon => "Xenon",
Family::Radon => "Radon",
Family::Krypton => "Krypton",
}
}
pub fn next(self) -> Self {
let at = Self::ALL.iter().position(|&f| f == self).unwrap_or(0);
Self::ALL[(at + 1) % Self::ALL.len()]
}
fn bytes(self) -> &'static [u8] {
macro_rules! face {
($file:literal) => {
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/fonts/monaspace/",
$file
))
};
}
match self {
Family::Neon => face!("MonaspaceNeon-Regular.otf"),
Family::Argon => face!("MonaspaceArgon-Regular.otf"),
Family::Xenon => face!("MonaspaceXenon-Regular.otf"),
Family::Radon => face!("MonaspaceRadon-Regular.otf"),
Family::Krypton => face!("MonaspaceKrypton-Regular.otf"),
}
}
}
const CAP_UNITS: f32 = 7.0;
const ADVANCE_PER_EM: f32 = 0.541_666_6;
const NOMINAL_CAP_PER_EM: f32 = 0.65625;
const ADVANCE_UNITS: f32 = ADVANCE_PER_EM / NOMINAL_CAP_PER_EM * CAP_UNITS;
pub struct Face {
family: Family,
font: FontRef<'static>,
ascent: f32,
cap: f32,
}
impl Face {
pub fn new(family: Family) -> Self {
let font = FontRef::try_from_slice(family.bytes()).expect("a Monaspace face");
let ascent = font.as_scaled(PxScale::from(PROBE)).ascent() / PROBE;
let cap = font
.outline_glyph(font.glyph_id('H').with_scale(PROBE))
.map(|outlined| outlined.px_bounds().height() / PROBE)
.unwrap_or(NOMINAL_CAP_PER_EM);
Self {
family,
font,
ascent,
cap,
}
}
pub fn family(&self) -> Family {
self.family
}
fn em(&self, pixel_size: f32) -> f32 {
CAP_UNITS * pixel_size / self.cap
}
pub fn rasterize(&self, ch: char, size: u32) -> Option<Vec<u8>> {
let scale = size as f32 * CELL_FILL;
let inset = (size as f32 - scale) * 0.5;
let baseline = inset + self.ascent * scale;
let outlined = self
.font
.outline_glyph(self.font.glyph_id(ch).with_scale(scale))?;
let bounds = outlined.px_bounds();
let mut pixels = vec![0u8; (size * size * 4) as usize];
outlined.draw(|x, y, coverage| {
let px = bounds.min.x + x as f32;
let py = baseline + bounds.min.y + y as f32;
if px < 0.0 || py < 0.0 || px >= size as f32 || py >= size as f32 {
return;
}
let at = ((py as u32 * size + px as u32) * 4) as usize;
pixels[at] = 255;
pixels[at + 1] = 255;
pixels[at + 2] = 255;
pixels[at + 3] = (coverage.clamp(0.0, 1.0) * 255.0) as u8;
});
Some(pixels)
}
}
const CELL_FILL: f32 = 0.82;
const PROBE: f32 = 64.0;
impl Default for Face {
fn default() -> Self {
Self::new(Family::default())
}
}
#[derive(bevy_ecs::prelude::Resource, Default)]
pub struct Font {
face: Face,
changed: bool,
}
impl Font {
pub fn family(&self) -> Family {
self.face.family()
}
pub fn face(&self) -> &Face {
&self.face
}
pub fn set_family(&mut self, family: Family) {
if self.face.family() == family {
return;
}
self.face = Face::new(family);
self.changed = true;
log::info!("drawing the UI in Monaspace {}", family.label());
}
pub fn cycle(&mut self) {
self.set_family(self.family().next());
}
pub fn take_changed(&mut self) -> bool {
std::mem::take(&mut self.changed)
}
}
pub fn push_text(
quads: &mut Vec<QuadInstance>,
atlas: &mut Atlas,
face: &Face,
text: &str,
origin_x: f32,
origin_y: f32,
pixel_size: f32,
color: Color,
) {
let em = face.em(pixel_size);
let advance = ADVANCE_UNITS * pixel_size;
let cell = em / CELL_FILL;
let inset = (cell - em) * 0.5;
let top = origin_y + CAP_UNITS * pixel_size - (inset + face.ascent * em);
let color = linear_rgba(color);
for (i, ch) in text.chars().enumerate() {
let Some(Uv { min, max }) = atlas.glyph(face, ch) else {
continue;
};
quads.push(QuadInstance {
pos: [origin_x + i as f32 * advance - inset, top],
size: [cell, cell],
color,
uv: [min[0], min[1], max[0], max[1]],
});
}
}
pub fn text_width(text: &str, pixel_size: f32) -> f32 {
text.chars().count() as f32 * ADVANCE_UNITS * pixel_size
}
pub fn text_height(pixel_size: f32) -> f32 {
CAP_UNITS * pixel_size
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_five_faces_load() {
for family in Family::ALL {
assert_eq!(Face::new(family).family(), family);
}
}
#[test]
fn the_measurements_match_the_faces() {
use ab_glyph::Font as _;
for family in Family::ALL {
let font = FontRef::try_from_slice(family.bytes()).expect("a face");
let scaled = font.as_scaled(PxScale::from(PROBE));
for ch in ['M', 'i', '.', 'W'] {
let advance = scaled.h_advance(font.glyph_id(ch)) / PROBE;
assert!(
(advance - ADVANCE_PER_EM).abs() < 1e-3,
"{} advances {advance} for {ch:?}, not {ADVANCE_PER_EM}",
family.label(),
);
}
let cap = Face::new(family).em(3.0) * Face::new(family).cap;
assert!(
(cap - text_height(3.0)).abs() < 1e-4,
"{} puts a capital at {cap}, not {}",
family.label(),
text_height(3.0),
);
}
}
#[test]
fn a_capital_is_seven_pixel_sizes_tall() {
assert_eq!(text_height(3.0), 21.0);
assert_eq!(text_height(1.0), 7.0);
}
#[test]
fn text_gets_wider_with_the_size_and_the_string() {
assert!(text_width("MENU", 3.0) > text_width("OK", 3.0));
assert!(text_width("MENU", 6.0) > text_width("MENU", 3.0));
assert_eq!(text_width("", 3.0), 0.0);
assert_eq!(text_width("MMMM", 3.0), text_width("il.'", 3.0), "monospaced");
}
#[test]
fn cycling_goes_round_all_five_and_comes_back() {
let mut font = Font::default();
assert_eq!(font.family(), Family::Neon);
let seen: Vec<Family> = (0..Family::ALL.len())
.map(|_| {
font.cycle();
font.family()
})
.collect();
assert_eq!(seen.last(), Some(&Family::Neon), "back where it started");
let mut sorted = seen.clone();
sorted.sort_by_key(|f| f.label());
sorted.dedup();
assert_eq!(sorted.len(), 5, "and through every one on the way: {seen:?}");
}
#[test]
fn changing_the_family_is_reported_once() {
let mut font = Font::default();
assert!(!font.take_changed(), "nothing has changed yet");
font.set_family(Family::Xenon);
assert!(font.take_changed(), "the atlas has letters to forget");
assert!(!font.take_changed(), "and is only told the once");
}
#[test]
fn setting_the_family_it_already_has_changes_nothing() {
let mut font = Font::default();
font.set_family(Family::Neon);
assert!(
!font.take_changed(),
"re-rasterising the alphabet for no change would be a waste",
);
}
#[test]
fn a_glyph_is_drawn_inside_its_cell() {
let face = Face::default();
for ch in ['A', 'g', 'Q', '(', '_', '`'] {
let raster = face.rasterize(ch, 64).expect("a glyph");
assert_eq!(raster.len(), 64 * 64 * 4);
assert!(
raster.chunks(4).any(|px| px[3] > 0),
"{ch:?} rasterised blank",
);
}
}
#[test]
fn capitals_sit_on_one_baseline() {
let face = Face::default();
let foot = |ch: char| {
let raster = face.rasterize(ch, 64).expect("a glyph");
(0..64)
.rev()
.find(|&y| (0..64).any(|x| raster[((y * 64 + x) * 4 + 3) as usize] > 0))
.expect("some ink")
};
let (h, e, t): (u32, u32, u32) = (foot('H'), foot('E'), foot('T'));
assert!(
h.abs_diff(e) <= 1 && h.abs_diff(t) <= 1,
"H at {h}, E at {e}, T at {t}",
);
}
#[test]
fn a_label_becomes_one_quad_per_letter() {
let mut quads = Vec::new();
let mut atlas = Atlas::new();
let face = Face::default();
push_text(
&mut quads,
&mut atlas,
&face,
"NEW GAME",
100.0,
50.0,
3.0,
Color::WHITE,
);
assert_eq!(quads.len(), 7, "eight characters, and the space has no ink");
for quad in &quads {
assert!(
quad.uv[2] > quad.uv[0] && quad.uv[3] > quad.uv[1],
"a glyph quad has to sample the atlas, not draw flat: {:?}",
quad.uv,
);
assert!(quad.size[0] > 0.0 && quad.size[1] > 0.0);
}
let advance = ADVANCE_UNITS * 3.0;
assert!((quads[1].pos[0] - quads[0].pos[0] - advance).abs() < 1e-3);
assert!(
(quads[3].pos[0] - quads[2].pos[0] - advance * 2.0).abs() < 1e-3,
"the space still moves the cursor on",
);
}
#[test]
fn the_faces_all_draw_to_the_same_place() {
let laid_out = |family: Family| {
let mut quads = Vec::new();
let mut atlas = Atlas::new();
push_text(
&mut quads,
&mut atlas,
&Face::new(family),
"MENU",
10.0,
20.0,
3.0,
Color::WHITE,
);
quads.iter().map(|q| q.pos[0]).collect::<Vec<_>>()
};
let neon = laid_out(Family::Neon);
for family in Family::ALL {
let other = laid_out(family);
assert_eq!(neon.len(), other.len(), "{}", family.label());
for (a, b) in neon.iter().zip(&other) {
assert!(
(a - b).abs() < 0.5,
"{} puts a letter at {b} where Neon puts it at {a}",
family.label(),
);
}
}
}
#[test]
fn a_space_has_nothing_to_draw() {
assert!(Face::default().rasterize(' ', 64).is_none());
}
}