use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
use yog_sync::{Arc, Mutex};
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>,
runs: Mutex<HashMap<String, Arc<Glyph>>>,
}
const MAX_RUNS: usize = 1024;
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 mut font = yog_data::from_text::<yog_font::Font>(&text).ok()?.value;
font.normalize();
let chars = font
.chars
.iter()
.filter_map(|c| Some((char::from_u32(c.char)?, c.glyph)))
.collect();
Some(Arc::new(YogProvider {
font,
chars,
runs: Mutex::new(HashMap::new()),
}))
}
impl YogProvider {
pub(super) fn chars(&self) -> impl Iterator<Item = char> + '_ {
self.chars.keys().copied()
}
fn units(&self) -> f32 {
EM_PX / self.font.units_per_em.max(1) as f32
}
pub(super) fn glyph(&self, ch: char, baseline: f32) -> Option<Glyph> {
let &index = self.chars.get(&ch)?;
let advance = self.font.glyphs.get(index as usize)?.advance as f32 * self.units();
let placed =
yog_font::raster::glyph(&self.font, index, self.units() * OVERSAMPLE, Color::WHITE);
Some(drawn(placed, advance, baseline))
}
pub(super) fn run(&self, text: &str, baseline: f32) -> Arc<Glyph> {
if let Some(r) = self.runs.lock().get(text) {
return r.clone();
}
let shaped = yog_font::shape(&self.font, text);
let placed =
yog_font::raster::run(&self.font, &shaped, self.units() * OVERSAMPLE, Color::WHITE);
let run = Arc::new(drawn(placed, shaped.advance * self.units(), baseline));
let mut runs = self.runs.lock();
if runs.len() >= MAX_RUNS {
runs.clear();
}
runs.insert(text.to_owned(), run.clone());
run
}
}
fn drawn(placed: yog_graphics::PlacedImage, advance: f32, baseline: f32) -> Glyph {
let image = placed.image;
if image.width == 0 || image.height == 0 {
return 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;
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.clone()],
..Default::default()
},
yog_font::Glyph {
name: "aa".into(),
advance: 32,
ops: vec![square],
..Default::default()
},
],
chars: vec![yog_font::CharGlyph {
char: 'a' as u32,
glyph: 0,
}],
ligatures: vec![yog_font::Ligature {
glyphs: vec![0, 0],
glyph: 1,
}],
..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
);
assert_eq!(
loaded.width("aa"),
4.0,
"the ligature's advance, not two letters'"
);
assert_eq!(loaded.width("aaa"), 7.0);
let _ = std::fs::remove_dir_all(&dir);
}
}