use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
use yog_sync::Arc;
use yog_graphics::Color;
use yog_ids::Format;
use super::assets::Resources;
use super::font::{Glyph, GlyphBitmap};
const OVERSAMPLE: f32 = 4.0;
const EM_PX: f32 = 9.0;
pub(super) struct YogProvider {
font: yog_font::Font,
chars: HashMap<char, u32>,
}
fn file(res: &Resources, id: &str) -> Option<PathBuf> {
let root = res.mod_root.as_deref()?;
let (ns, path) = yog_ids::split(id);
(super::project::mod_id(root).as_deref() == Some(ns)).then(|| {
root.join(yog_ids::project::CONTENT)
.join(Format::Font.file(path))
})
}
pub(super) fn changed(res: &Resources, id: &str) -> Option<SystemTime> {
std::fs::metadata(file(res, id)?)
.and_then(|m| m.modified())
.ok()
}
pub(super) fn load(res: &Resources, id: &str) -> Option<Arc<YogProvider>> {
let text = crate::files::read_source(file(res, id)?).ok()?;
let font = yog_data::from_text::<yog_font::Font>(&text).ok()?.value;
let chars = font
.chars
.iter()
.filter_map(|c| Some((char::from_u32(c.char)?, c.glyph)))
.collect();
Some(Arc::new(YogProvider { font, chars }))
}
impl YogProvider {
pub(super) fn chars(&self) -> impl Iterator<Item = char> + '_ {
self.chars.keys().copied()
}
pub(super) fn glyph(&self, ch: char, baseline: f32) -> Option<Glyph> {
let &index = self.chars.get(&ch)?;
let units = EM_PX / self.font.units_per_em.max(1) as f32;
let advance = self.font.glyphs.get(index as usize)?.advance as f32 * units;
let placed = yog_font::raster::glyph(&self.font, index, units * OVERSAMPLE, Color::WHITE);
let image = placed.image;
if image.width == 0 || image.height == 0 {
return Some(Glyph {
advance,
shadow_offset: 1.0,
bold_offset: 1.0,
bitmap: None,
});
}
let shift = placed.x;
let w = (image.width as i32 + shift).max(0) as u32;
let mut rgba = vec![0u8; (w * image.height) as usize * yog_graphics::BYTES_PER_PIXEL];
for (y, row) in image.pixels().chunks(image.width as usize).enumerate() {
for (x, p) in row.iter().enumerate() {
let to = x as i32 + shift;
if (0..w as i32).contains(&to) {
let i = (y * w as usize + to as usize) * yog_graphics::BYTES_PER_PIXEL;
rgba[i..i + yog_graphics::BYTES_PER_PIXEL].copy_from_slice(p);
}
}
}
let top = baseline + placed.y as f32 / OVERSAMPLE;
Some(Glyph {
advance,
shadow_offset: 1.0,
bold_offset: 1.0,
bitmap: Some(GlyphBitmap {
w,
h: image.height,
rgba,
oversample: OVERSAMPLE,
top,
}),
})
}
}
#[cfg(test)]
mod tests {
use super::super::font::Font;
use super::*;
use yog_graphics::{Image, ImageOp, Op, Rect};
#[test]
fn the_mods_font_is_drawn_from_its_file() {
let dir = std::env::temp_dir().join(format!("yog-font-yog-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("content/font")).unwrap();
std::fs::write(dir.join("yog.toml"), "[mod]\nid = \"mymod\"\n").unwrap();
let square = Op::Image(ImageOp {
rect: Rect::new(0.0, 0.0, 16.0, 16.0),
image: Image {
width: 1,
height: 1,
rgba: vec![0xFF; 4],
},
tinted: true,
});
let font = yog_font::Font {
units_per_em: 72,
ascent: 56,
descent: -16,
glyphs: vec![yog_font::Glyph {
name: "a".into(),
advance: 24,
ops: vec![square],
..Default::default()
}],
chars: vec![yog_font::CharGlyph {
char: 'a' as u32,
glyph: 0,
}],
..Default::default()
};
std::fs::write(
dir.join("content/font/square.yfont"),
yog_data::to_text(&font),
)
.unwrap();
let res = Resources {
mod_root: Some(dir.clone()),
vanilla: None,
};
let loaded = Font::load(&res, "mymod:font/square");
assert!(loaded.is_real());
let g = loaded.glyph('a');
assert_eq!(g.advance, 3.0);
let b = g.bitmap.as_ref().unwrap();
assert!(
((b.top + b.h as f32 / b.oversample) - 7.0).abs() <= 1.0 / b.oversample,
"{} {}",
b.top,
b.h
);
let _ = std::fs::remove_dir_all(&dir);
}
}