cubik 0.1.0

OpenGL/glium-based multiplayer game engine
Documentation
use std::fs::File;
use std::io;
use std::io::Read;
use std::collections::HashMap;
use std::hash::Hasher;
use std::collections::hash_map::DefaultHasher;
use rusttype::{Font, Scale, Point, Rect, HMetrics};
use derive_more::{Error, From};
use crate::draw::{Vertex, load_data_to_gpu, ObjDef};
use crate::draw::{UIDrawInfo, ui_draw};
use glium::{Display, Frame, texture::{SrgbTexture2d, RawImage2d, TextureCreationError}};
use crate::assets::find_asset;

pub enum TextAlign {
	Left,
	Center,
	Right
}

struct FontChar {
	texture: Option<SrgbTexture2d>,
	bbox: Option<Rect<i32>>,
	h_metrics: HMetrics
}

pub struct FontText {
	chars: Vec<(char, Option<ObjDef>)>,
	last_font_hash: u64,
	align: TextAlign,
	text: String,
	pub ui_draw_info: UIDrawInfo
}

#[derive(Default)]
pub struct LoadedFont {
	hash: u64, 
	chars: HashMap<char, FontChar>,
	pub font_size: f32
}

#[derive(Debug, derive_more::Display, Error, From)]
pub enum FontError {
	IOError(io::Error),
	LoadError,
	TextureUploadError(TextureCreationError),
	CharNotAvailable
}

impl LoadedFont {
	pub fn load(display: &Display, filename: &str, app_id: &str, font_size: f32) -> Result<Self, FontError> {
		let mut f = File::open(find_asset(filename, app_id))?;
		let mut f_contents = Vec::new();
		f.read_to_end(&mut f_contents)?;

		let font = Font::try_from_vec(f_contents).ok_or(FontError::LoadError)?;
		let scale = Scale::uniform(font_size);

		let mut hasher = DefaultHasher::new();
		hasher.write(filename.as_bytes());
		hasher.write_u32(font_size.to_bits());
		let mut result = Self { hash: hasher.finish(), font_size: font_size, ..Default::default() };

		for ch in b' '..=b'~' {
			let glyph = font.glyph(ch as char).scaled(scale);
			let h_metrics = glyph.h_metrics();
			let positioned = glyph.positioned(Point { x: 0., y: 0. });
			let mut char_result = FontChar { texture: None, bbox: None, h_metrics: h_metrics };
			if let Some(positioned_box) = positioned.pixel_bounding_box() {
				let glyph_size = (positioned_box.width() as usize, positioned_box.height() as usize);
				let mut glyph_data = vec![0u8; glyph_size.1 * glyph_size.0 * 4];
				positioned.draw(|x, y, v| {
					let val = (v * 255.) as u8;
					let index = ((y * (glyph_size.0 as u32) + x) * 4) as usize;
					glyph_data[index] = 255;
					glyph_data[index + 1] = 255;
					glyph_data[index + 2] = 255;
					glyph_data[index + 3] = val;
				});

				let txt = SrgbTexture2d::new(display,
					RawImage2d::from_raw_rgba_reversed(&glyph_data, (glyph_size.0 as u32, glyph_size.1 as u32)))?;

				char_result.texture = Some(txt);
				char_result.bbox = Some(positioned_box);
			}

			result.chars.insert(ch as char, char_result);
		}

		Ok(result)
	}
}

impl FontText {
	pub fn new(text: String, size: f32, pos: (f32, f32), align: TextAlign) -> Self {
		let text_len = text.len();
		Self {
			text: text,
			align: align,
			last_font_hash: 0,
			chars: Vec::with_capacity(text_len),
			ui_draw_info: UIDrawInfo::new(pos, (size, size))
		}
	}
	
	pub fn measure_width(&mut self, font: &LoadedFont) -> Result<f32, FontError> {
		let mut width = 0.0f32;
		for c in self.text.chars() {
			let ch = font.chars.get(&c).ok_or(FontError::CharNotAvailable)?;
			
			width += ch.h_metrics.advance_width / font.font_size;
		}

		Ok(width)
	}

	fn starting_position(&mut self, font: &LoadedFont) -> Result<f32, FontError> {
		let width = self.measure_width(font)?;
		let x_offset: f32 = match self.align {
			TextAlign::Left => 0.,
			TextAlign::Right => width,
			TextAlign::Center => width / 2.
		};
		Ok(-x_offset)
	}

	fn prepare_chars(&mut self, display: &Display, font: &LoadedFont) -> Result<(), FontError> {
		let mut pos = (self.starting_position(font)?, 0.0);
		for c in self.text.chars() {
			let mut char_result: Option<ObjDef> = None;

			let ch = font.chars.get(&c).ok_or(FontError::CharNotAvailable)?;

			pos.0 += ch.h_metrics.left_side_bearing / font.font_size;

			if let Some(bbox) = ch.bbox {
				let ch_size = (bbox.width() as f32, bbox.height() as f32);
				let real_size = (ch_size.0 / font.font_size, ch_size.1 / font.font_size);

				pos.1 -= (bbox.max.y as f32) / font.font_size;

				let vertices = [
					Vertex { position: [pos.0, pos.1, 0.], normal: [0., 0., -1.], texcoords: [0., 0.] },
					Vertex { position: [pos.0 + real_size.0, pos.1, 0.], normal: [0., 0., -1.], texcoords: [1., 0.] },
					Vertex { position: [pos.0 + real_size.0, pos.1 + real_size.1, 0.], normal: [0., 0., -1.], texcoords: [1., 1.] },
					Vertex { position: [pos.0, pos.1 + real_size.1, 0.], normal: [0., 0., -1.], texcoords: [0., 1.] }
				];
				
				let indices = [0, 1, 2, 0, 2, 3];

				char_result = Some(load_data_to_gpu(display, &vertices, &indices));

				pos.1 += (bbox.max.y as f32) / font.font_size;
			}
			self.chars.push((c, char_result));

			pos.0 += (ch.h_metrics.advance_width - ch.h_metrics.left_side_bearing) / font.font_size;
		}
		Ok(())
	}

	pub fn draw(&mut self, target: &mut Frame, display: &Display, program: &glium::Program, font: &LoadedFont) -> Result<(), FontError> {
		if self.last_font_hash != font.hash {
			self.chars.clear();
			self.prepare_chars(display, font)?;
			self.last_font_hash = font.hash;
		}
		self.ui_draw_info.generate_matrix(target);

		for (c, obj_def) in &self.chars {
			let ch = font.chars.get(&c).ok_or(FontError::CharNotAvailable)?;

			if let Some(obj_def) = obj_def {
				ui_draw(target, &obj_def, &self.ui_draw_info, program, ch.texture.as_ref().unwrap());
			}
		}
		Ok(())
	}
}