#![warn(missing_docs)]
extern crate rusttype;
#[macro_use]
extern crate glium;
use std::borrow::Cow;
use std::collections::HashMap;
use std::default::Default;
use std::io::Read;
use std::ops::Deref;
use std::rc::Rc;
use rusttype::{Rect, Point};
use glium::DrawParameters;
use glium::backend::Context;
use glium::backend::Facade;
pub struct FontTexture {
texture: glium::texture::Texture2d,
character_infos: HashMap<char, CharacterInfos>,
}
#[derive(Debug)]
pub enum Error {
NoGlyph(char),
RusttypeError(rusttype::Error),
}
impl From<rusttype::Error> for Error {
fn from(error: rusttype::Error) -> Self {
Error::RusttypeError(error)
}
}
pub struct TextSystem {
context: Rc<Context>,
program: glium::Program,
}
pub struct TextDisplay<F> where F: Deref<Target=FontTexture> {
context: Rc<Context>,
texture: F,
vertex_buffer: Option<glium::VertexBuffer<VertexFormat>>,
index_buffer: Option<glium::IndexBuffer<u16>>,
total_text_width: f32,
text_height: f32,
is_empty: bool,
}
#[derive(Copy, Clone, Debug)]
struct CharacterInfos {
tex_coords: (f32, f32),
tex_size: (f32, f32),
size: (f32, f32),
height_over_line: f32,
left_padding: f32,
right_padding: f32,
}
struct TextureData {
data: Vec<f32>,
width: u32,
height: u32,
}
impl<'a> glium::texture::Texture2dDataSource<'a> for &'a TextureData {
type Data = f32;
fn into_raw(self) -> glium::texture::RawImage2d<'a, f32> {
glium::texture::RawImage2d {
data: Cow::Borrowed(&self.data),
width: self.width,
height: self.height,
format: glium::texture::ClientFormat::F32,
}
}
}
#[derive(Copy, Clone)]
struct VertexFormat {
position: [f32; 2],
tex_coords: [f32; 2],
}
implement_vertex!(VertexFormat, position, tex_coords);
impl FontTexture {
pub fn ascii_character_list() -> Vec<char> {
(0 .. 255).filter_map(::std::char::from_u32).collect()
}
pub fn new<R, F, I>(facade: &F, font: R, font_size: u32, characters_list: I)
-> Result<FontTexture, Error>
where R: Read, F: Facade, I: IntoIterator<Item=char>
{
let font: Vec<u8> = font.bytes().map(|c| c.unwrap()).collect();
let collection = ::rusttype::FontCollection::from_bytes(&font[..])?;
let font = collection.into_font().unwrap();
let (texture_data, chr_infos) =
build_font_image(&font, characters_list.into_iter(), font_size)?;
let texture = glium::texture::Texture2d::new(facade, &texture_data).unwrap();
Ok(FontTexture {
texture,
character_infos: chr_infos,
})
}
}
impl TextSystem {
pub fn new<F>(facade: &F) -> TextSystem where F: Facade {
TextSystem {
context: facade.get_context().clone(),
program: program!(facade,
140 => {
vertex: "
#version 140
uniform mat4 matrix;
in vec2 position;
in vec2 tex_coords;
out vec2 v_tex_coords;
void main() {
gl_Position = matrix * vec4(position, 0.0, 1.0);
v_tex_coords = tex_coords;
}
",
fragment: "
#version 140
in vec2 v_tex_coords;
out vec4 f_color;
uniform vec4 color;
uniform sampler2D tex;
void main() {
vec4 c = vec4(color.rgb, color.a * texture(tex, v_tex_coords));
if (c.a <= 0.01) {
discard;
} else {
f_color = c;
}
}
"
},
110 => {
vertex: "
#version 110
attribute vec2 position;
attribute vec2 tex_coords;
varying vec2 v_tex_coords;
uniform mat4 matrix;
void main() {
gl_Position = matrix * vec4(position.x, position.y, 0.0, 1.0);
v_tex_coords = tex_coords;
}
",
fragment: "
#version 110
varying vec2 v_tex_coords;
uniform vec4 color;
uniform sampler2D tex;
void main() {
gl_FragColor = vec4(color.rgb, color.a * texture2D(tex, v_tex_coords));
if (gl_FragColor.a <= 0.01) {
discard;
}
}
"
},
).unwrap()
}
}
}
impl<F> TextDisplay<F> where F: Deref<Target=FontTexture> {
pub fn new(system: &TextSystem, texture: F, text: &str) -> TextDisplay<F> {
let mut text_display = TextDisplay {
context: system.context.clone(),
texture,
vertex_buffer: None,
index_buffer: None,
total_text_width: 0.0,
text_height: 0.0,
is_empty: true,
};
text_display.set_text(text);
text_display
}
pub fn get_width(&self) -> f32 {
self.total_text_width
}
pub fn get_height(&self) -> f32 {
self.text_height
}
pub fn set_text(&mut self, text: &str) {
self.is_empty = true;
self.total_text_width = 0.0;
self.vertex_buffer = None;
self.index_buffer = None;
if text.is_empty() {
return;
}
let mut vertex_buffer_data = Vec::with_capacity(text.len() * 4 * 4);
let mut index_buffer_data = Vec::with_capacity(text.len() * 6);
for character in text.chars() {
let infos = match self.texture.character_infos.get(&character) {
Some(infos) => infos,
None => continue,
};
self.is_empty = false;
{
let first_vertex_offset = vertex_buffer_data.len() as u16;
index_buffer_data.push(first_vertex_offset);
index_buffer_data.push(first_vertex_offset + 1);
index_buffer_data.push(first_vertex_offset + 2);
index_buffer_data.push(first_vertex_offset + 2);
index_buffer_data.push(first_vertex_offset + 1);
index_buffer_data.push(first_vertex_offset + 3);
}
self.total_text_width += infos.left_padding;
let left_coord = self.total_text_width;
let right_coord = left_coord + infos.size.0;
let top_coord = infos.height_over_line;
let bottom_coord = infos.height_over_line - infos.size.1;
vertex_buffer_data.push(VertexFormat {
position: [left_coord, top_coord],
tex_coords: [infos.tex_coords.0, infos.tex_coords.1],
});
vertex_buffer_data.push(VertexFormat {
position: [right_coord, top_coord],
tex_coords: [infos.tex_coords.0 + infos.tex_size.0, infos.tex_coords.1],
});
vertex_buffer_data.push(VertexFormat {
position: [left_coord, bottom_coord],
tex_coords: [infos.tex_coords.0, infos.tex_coords.1 + infos.tex_size.1],
});
vertex_buffer_data.push(VertexFormat {
position: [right_coord, bottom_coord],
tex_coords: [
infos.tex_coords.0 + infos.tex_size.0,
infos.tex_coords.1 + infos.tex_size.1
],
});
self.total_text_width = right_coord + infos.right_padding;
if top_coord > self.text_height {
self.text_height = top_coord;
}
}
if !vertex_buffer_data.len() != 0 {
self.vertex_buffer = Some(glium::VertexBuffer::new(&self.context,
&vertex_buffer_data).unwrap());
self.index_buffer = Some(glium::IndexBuffer::new(&self.context,
glium::index::PrimitiveType::TrianglesList,
&index_buffer_data).unwrap());
}
}
}
pub fn draw<F, S: ?Sized, M>(
text: &TextDisplay<F>,
system: &TextSystem,
target: &mut S,
matrix: M,
color: (f32, f32, f32, f32)
) -> Result<(), glium::DrawError>
where S: glium::Surface,
M: Into<[[f32; 4]; 4]>,
F: Deref<Target=FontTexture>
{
let behavior = glium::uniforms::SamplerBehavior {
magnify_filter: glium::uniforms::MagnifySamplerFilter::Linear,
minify_filter: glium::uniforms::MinifySamplerFilter::Linear,
.. Default::default()
};
let params = {
use glium::BlendingFunction::Addition;
use glium::LinearBlendingFactor::*;
let blending_function = Addition {
source: SourceAlpha,
destination: OneMinusSourceAlpha
};
let blend = glium::Blend {
color: blending_function,
alpha: blending_function,
constant_value: (1.0, 1.0, 1.0, 1.0),
};
DrawParameters {
blend,
.. Default::default()
}
};
draw_with_params(text, system, target, matrix, color, behavior, ¶ms)
}
pub fn draw_with_params<F, S: ?Sized, M>(
text: &TextDisplay<F>,
system: &TextSystem,
target: &mut S,
matrix: M,
color: (f32, f32, f32, f32),
sampler_behavior: glium::uniforms::SamplerBehavior,
parameters: &DrawParameters
) -> Result<(), glium::DrawError>
where S: glium::Surface,
M: Into<[[f32; 4]; 4]>,
F: Deref<Target=FontTexture>
{
let matrix = matrix.into();
let &TextDisplay {
ref vertex_buffer,
ref index_buffer,
ref texture,
is_empty,
..
} = text;
let color = [color.0, color.1, color.2, color.3];
if is_empty || vertex_buffer.is_none() || index_buffer.is_none() {
return Ok(());
}
let vertex_buffer = vertex_buffer.as_ref().unwrap();
let index_buffer = index_buffer.as_ref().unwrap();
let uniforms = uniform! {
matrix: matrix,
color: color,
tex: glium::uniforms::Sampler(&texture.texture, sampler_behavior)
};
target.draw(vertex_buffer, index_buffer, &system.program, &uniforms, ¶meters)
}
fn build_font_image<I>(font: &rusttype::Font, characters_list: I, font_size: u32)
-> Result<(TextureData, HashMap<char, CharacterInfos>), Error>
where I: Iterator<Item=char>
{
use std::iter;
const MARGIN: u32 = 2;
let invalid_character_width = font_size / 2;
let size_estimation = characters_list.size_hint().1.unwrap_or(0);
let mut texture_data: Vec<f32> = Vec::with_capacity(
size_estimation * font_size as usize * font_size as usize
);
let texture_width = get_nearest_po2(std::cmp::max(font_size * 2 as u32,
((((size_estimation as u32) * font_size * font_size) as f32).sqrt()) as u32));
let mut cursor_offset = (0u32, 0u32);
let mut rows_to_skip = 0u32;
let em_pixels = font_size as f32;
let characters_infos = characters_list.map(|character| {
struct Bitmap {
rows : i32,
width : i32,
buffer : Vec<u8>
}
let scaled_glyph = font.glyph(character)
.scaled(::rusttype::Scale {x : font_size as f32, y : font_size as f32 });
let h_metrics = scaled_glyph.h_metrics();
let glyph = scaled_glyph
.positioned(::rusttype::Point {x : 0.0, y : 0.0 });
let bb = glyph.pixel_bounding_box();
let bb = if let Some(bb) = bb {
bb
} else {
Rect {
min: Point {x: 0, y: 0},
max: Point {x: invalid_character_width as i32, y: 0}
}
};
let mut buffer = vec![0; (bb.height() * bb.width()) as usize];
glyph.draw(|x, y, v| {
let x = x;
let y = y;
buffer[(y * bb.width() as u32 + x) as usize] = (v * 255.0) as u8;
});
let bitmap : Bitmap = Bitmap {
rows : bb.height(),
width : bb.width(),
buffer
};
cursor_offset.0 += MARGIN;
if cursor_offset.0 + (bitmap.width as u32) + MARGIN >= texture_width {
assert!(bitmap.width as u32 <= texture_width); cursor_offset.0 = 0;
cursor_offset.1 += rows_to_skip;
rows_to_skip = 0;
}
if rows_to_skip < MARGIN + bitmap.rows as u32 {
let diff = MARGIN + (bitmap.rows as u32) - rows_to_skip;
rows_to_skip = MARGIN + bitmap.rows as u32;
texture_data.extend(iter::repeat(0.0).take((diff * texture_width) as usize));
}
let offset_x_before_copy = cursor_offset.0;
if bitmap.rows >= 1 {
let destination = &mut texture_data[(cursor_offset.0 + cursor_offset.1 * texture_width) as usize ..];
let source = &bitmap.buffer;
for y in 0 .. bitmap.rows as u32 {
let source = &source[(y * bitmap.width as u32) as usize ..];
let destination = &mut destination[(y * texture_width) as usize ..];
for x in 0 .. bitmap.width {
let val: u8 = source[x as usize];
let val = f32::from(val) / f32::from(std::u8::MAX);
let dest = &mut destination[x as usize];
*dest = val;
}
}
cursor_offset.0 += bitmap.width as u32;
debug_assert!(cursor_offset.0 <= texture_width);
}
Ok((character, CharacterInfos {
tex_size: (bitmap.width as f32, bitmap.rows as f32),
tex_coords: (offset_x_before_copy as f32, cursor_offset.1 as f32),
size: (bitmap.width as f32, bitmap.rows as f32),
left_padding: h_metrics.left_side_bearing as f32,
right_padding: (h_metrics.advance_width
- bitmap.width as f32
- h_metrics.left_side_bearing as f32) as f32 / 64.0,
height_over_line: -bb.min.y as f32,
}))
}).collect::<Result<Vec<_>, Error>>()?;
{
let current_height = texture_data.len() as u32 / texture_width;
let requested_height = get_nearest_po2(current_height);
texture_data.extend(iter::repeat(0.0).take((texture_width * (requested_height - current_height)) as usize));
}
assert!((texture_data.len() as u32 % texture_width) == 0);
let texture_height = (texture_data.len() as u32 / texture_width) as f32;
let float_texture_width = texture_width as f32;
let mut characters_infos = characters_infos.into_iter().map(|mut chr| {
chr.1.tex_size.0 /= float_texture_width;
chr.1.tex_size.1 /= texture_height;
chr.1.tex_coords.0 /= float_texture_width;
chr.1.tex_coords.1 /= texture_height;
chr.1.size.0 /= em_pixels;
chr.1.size.1 /= em_pixels;
chr.1.left_padding /= em_pixels;
chr.1.right_padding /= em_pixels;
chr.1.height_over_line /= em_pixels;
chr
}).collect::<HashMap<_, _>>();
characters_infos.shrink_to_fit();
Ok((TextureData {
data: texture_data,
width: texture_width,
height: texture_height as u32,
}, characters_infos))
}
fn get_nearest_po2(mut x: u32) -> u32 {
assert!(x > 0);
x -= 1;
x = x | (x >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
x = x | (x >> 8);
x = x | (x >> 16);
x + 1
}