@@ 1,5 1,189 @@
+//! A simple glyph cache for video games and other programs.
+//!
+//! Does no actual rendering, drawing or anything else, simply mongles resources. You provide
+//! callbacks for it to talk to a rasterizer such as TODO, and interact with a drawing API such as
+//! OpenGL or Vulkan.
+//!
+//! This also sure as heck does not handle RTL or top-to-bottom text nicely, I bet. Let alone
+//! ligatures.
+//!
+//! TODO: How do we handle pairs of glyphs correctly, a la ligatures? Things to check:
+//! fi ffi rn rm fj ll ...
+//! We might just step through characters by pairs instead of single things
+
+use std::collections::HashMap;
+
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub struct Rect {
+ pub x: f32,
+ pub y: f32,
+ pub w: f32,
+ pub h: f32,
+}
+
+/// A trait for the user to implement that defines how to interact with where their glyphs are
+/// stored.
+pub trait Storage {
+ fn write_bytes_to(&mut self, rect: Rect, bytes: &[u8]) -> Result<(), ()>;
+ fn resize(&mut self, new_w: f32, new_h: f32);
+}
+
+pub trait Rasterizer {
+ fn glyph_dimensions(&self, c: char) -> (f32, f32);
+ fn rasterize_glyph(&self, c: char) -> Vec<u8>;
+}
+
+/// The `Storage` type is the backing store that the glyphs are actually cached in, such as a
+/// texture.
+#[derive(Clone, Debug)]
+pub struct Glif<S>
+where
+ S: 'static + Storage,
+{
+ /// The backing storage, ie a texture
+ storage: S,
+ /// Where various characters are in the texture.
+ locations: HashMap<char, Rect>,
+ /// We keep the width and height together.
+ /// TODO: Assert somewhere that they're > 0.0
+ storage_dims: f32,
+ col_left: f32,
+ row_top: f32,
+ row_height: f32,
+}
+
+impl<S> Glif<S>
+where
+ S: 'static + Storage,
+{
+ pub fn new(storage: S, initial_size: f32) -> Self {
+ Self {
+ storage,
+ locations: HashMap::new(),
+ storage_dims: initial_size,
+ col_left: 0.0,
+ row_top: 0.0,
+ row_height: 0.0,
+ }
+ }
+
+ /// TODO: Context type
+ /// TODO: Glyph parameters like size, font, bold/italic, etc
+ pub fn draw_str<F, R>(&mut self, s: &str, mut draw_glyph: F, r: &R) -> Result<(), ()>
+ where
+ F: FnMut(&S, Rect),
+ R: Rasterizer,
+ {
+ for c in s.chars() {
+ if let Some(rect) = self.locations.get(&c) {
+ // The character alrleady exists, ezpz
+ draw_glyph(&self.storage, *rect);
+ } else {
+ // The character does not exist, rasterize it and find a place to draw it
+ let (w, h) = r.glyph_dimensions(c);
+ let rect = self.make_new_rect(w, h);
+ let bytes = r.rasterize_glyph(c);
+ self.storage.write_bytes_to(rect, &bytes)?;
+ self.locations.insert(c, rect);
+ draw_glyph(&self.storage, rect);
+ }
+ }
+ Ok(())
+ }
+
+ /// Chooses the best spot in the storage to put a rect of the given dimensions, or returns `None` if it can't fit.
+ fn find_new_rect(&mut self, width: f32, height: f32) -> Option<Rect> {
+ // Ohai an easy case
+ if width >= self.storage_dims || height >= self.storage_dims {
+ return None;
+ }
+
+ // TODO: Solve the knapsack problem.
+ // Currently we just fill the glyph cache from left to right and top to bottom.
+ // As long as glyphs are mostly the same size this might theoretically not be the worst
+ // thing ever maybe kinda
+ let mut target_x = self.col_left;
+ let mut target_y = self.row_top;
+ let mut target_height = f32::max(self.row_height, height);
+ if target_x + width >= self.storage_dims {
+ // Scroll to next line
+ target_x = 0.0;
+ target_y += target_height;
+ target_height = height;
+ }
+ if target_y + target_height >= self.storage_dims {
+ // We've walked off the bottom of the storage.
+ return None;
+ }
+ // Yay we've found a spot
+ self.col_left = target_x + width;
+ self.row_top = target_y;
+ self.row_height = target_height;
+ Some(Rect {
+ x: target_x,
+ y: target_y,
+ w: width,
+ h: height,
+ })
+ }
+
+ /// Gets a rect in the given storage of the given storage. If it can't fit,
+ /// make a new storage, copy the old bytes into it, and
+ ///
+ /// TODO: Is resizing the existing storage practical? If Vulkan or OpenGL can't
+ /// do it practically, it's not worth pretending and we should just replace the
+ /// old one.
+ fn make_new_rect(&mut self, width: f32, height: f32) -> Rect {
+ self.find_new_rect(width, height).unwrap_or_else(|| {
+ assert!(self.storage_dims > 0.0);
+ assert!(width > 0.0);
+ assert!(width < f32::INFINITY);
+ assert!(height > 0.0);
+ assert!(height < f32::INFINITY);
+
+ let mut new_dims = self.storage_dims * 2.0;
+ // Crudely figure out how much bigger we need to make the texture.
+ // Just make it huger until it's guarenteed bigger than we need.
+ while new_dims < (f32::max(width, height) * 2.0) {
+ new_dims *= 2.0;
+ }
+ assert!(new_dims < f32::INFINITY);
+ self.storage_dims = new_dims;
+ self.storage.resize(self.storage_dims, self.storage_dims);
+ self.find_new_rect(width, height)
+ .expect("Made storage bigger but still can't hold glyph, should never happen")
+ })
+ }
+}
+
#[cfg(test)]
mod tests {
+ use crate::*;
+
+ struct St {
+ contents: Vec<u8>,
+ size: u32,
+ }
+
+ impl Storage for St {
+ fn write_bytes_to(&mut self, _rect: Rect, _bytes: &[u8]) -> Result<(), ()> {
+ todo!()
+ }
+ fn resize(&mut self, _new_w: f32, _new_h: f32) {
+ todo!()
+ }
+ }
+
+ struct Ra {}
+ impl Rasterizer for Ra {
+ fn glyph_dimensions(&self, _c: char) -> (f32, f32) {
+ todo!()
+ }
+ fn rasterize_glyph(&self, _c: char) -> Vec<u8> {
+ todo!()
+ }
+ }
+
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);