use hashbrown::hash_map::Entry;
use hashbrown::HashMap;
use xi_unicode::LineBreakIterator;
use crate::graphics::text::packer::ShelfPacker;
use crate::graphics::{FilterMode, Rectangle, Texture};
use crate::math::Vec2;
use crate::platform::GraphicsDevice;
use crate::{Context, Result};
pub(crate) struct RasterizedGlyph {
pub bounds: Rectangle,
pub data: Vec<u8>,
}
#[derive(Debug, Copy, Clone)]
pub struct TextQuad {
pub position: Vec2<f32>,
pub region: Rectangle,
}
impl TextQuad {
fn bounds(&self) -> Rectangle {
Rectangle::new(
self.position.x,
self.position.y,
self.region.width,
self.region.height,
)
}
}
enum CacheError {
OutOfSpace,
}
#[derive(PartialEq, Eq, Hash)]
struct CacheKey {
glyph: char,
subpixel_x: u32,
subpixel_y: u32,
}
pub(crate) trait Rasterizer {
fn rasterize(&self, glyph: char, position: Vec2<f32>) -> Option<RasterizedGlyph>;
fn advance(&self, glyph: char) -> f32;
fn line_height(&self) -> f32;
fn ascent(&self) -> f32;
fn kerning(&self, previous: char, current: char) -> f32;
}
#[derive(Debug, Clone)]
pub(crate) struct TextGeometry {
pub quads: Vec<TextQuad>,
pub bounds: Option<Rectangle>,
pub resize_count: usize,
}
pub(crate) struct FontCache {
rasterizer: Box<dyn Rasterizer>,
packer: ShelfPacker,
glyphs: HashMap<CacheKey, Option<TextQuad>>,
resize_count: usize,
}
impl FontCache {
pub fn new(
device: &mut GraphicsDevice,
rasterizer: Box<dyn Rasterizer>,
filter_mode: FilterMode,
) -> Result<FontCache> {
Ok(FontCache {
rasterizer,
packer: ShelfPacker::new(device, 128, 128, filter_mode)?,
glyphs: HashMap::new(),
resize_count: 0,
})
}
pub fn texture(&self) -> &Texture {
self.packer.texture()
}
pub fn resize_count(&self) -> usize {
self.resize_count
}
pub fn filter_mode(&self) -> FilterMode {
self.packer.filter_mode()
}
pub fn set_filter_mode(&mut self, ctx: &mut Context, filter_mode: FilterMode) {
self.packer.set_filter_mode(ctx, filter_mode);
}
pub fn render(
&mut self,
device: &mut GraphicsDevice,
input: &str,
max_width: Option<f32>,
) -> TextGeometry {
loop {
match self.try_render(device, input, max_width) {
Ok(new_geometry) => return new_geometry,
Err(CacheError::OutOfSpace) => {
self.resize(device).expect("Failed to resize font texture");
}
}
}
}
fn try_render(
&mut self,
device: &mut GraphicsDevice,
input: &str,
max_width: Option<f32>,
) -> std::result::Result<TextGeometry, CacheError> {
let line_height = self.rasterizer.line_height().round();
let mut quads = Vec::new();
let mut cursor = Vec2::new(0.0, self.rasterizer.ascent().round());
let mut last_glyph: Option<char> = None;
let mut text_bounds: Option<Rectangle> = None;
let mut words_on_line = 0;
for (word, _) in UnicodeLineBreaks::new(input) {
if let Some(max_width) = max_width {
if words_on_line > 0 && cursor.x + self.measure_word(word) > max_width {
cursor.x = 0.0;
cursor.y += line_height;
last_glyph = None;
words_on_line = 0;
}
}
words_on_line += 1;
for ch in word.chars() {
if ch.is_control() {
if ch == '\n' {
cursor.x = 0.0;
cursor.y += line_height;
last_glyph = None;
words_on_line = 0;
}
continue;
}
if let Some(last_glyph) = last_glyph {
cursor.x += self.rasterizer.kerning(last_glyph, ch);
}
if let Some(quad) = self.rasterize_char(device, ch, cursor)? {
match &mut text_bounds {
Some(existing) => *existing = quad.bounds().combine(existing),
None => {
text_bounds.replace(quad.bounds());
}
}
quads.push(quad);
}
cursor.x += self.rasterizer.advance(ch);
last_glyph = Some(ch);
}
}
Ok(TextGeometry {
quads,
resize_count: self.resize_count,
bounds: text_bounds,
})
}
fn measure_word(&self, word: &str) -> f32 {
let mut last_glyph = None;
let mut word_width = 0.0;
for ch in word.trim_end().chars() {
word_width += self.rasterizer.advance(ch);
if let Some(last) = last_glyph {
word_width += self.rasterizer.kerning(last, ch);
}
last_glyph = Some(ch);
}
word_width
}
fn rasterize_char(
&mut self,
device: &mut GraphicsDevice,
ch: char,
position: Vec2<f32>,
) -> std::result::Result<Option<TextQuad>, CacheError> {
let subpixel_offset = position.map(f32::fract);
let subpixel_x = (subpixel_offset.x * 10.0).round() as u32;
let subpixel_y = (subpixel_offset.y * 10.0).round() as u32;
let cache_key = CacheKey {
glyph: ch,
subpixel_x,
subpixel_y,
};
let cached_quad = match self.glyphs.entry(cache_key) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let outline = match self.rasterizer.rasterize(ch, position) {
Some(r) => Some(add_glyph_to_texture(device, &mut self.packer, &r)?),
None => None,
};
e.insert(outline)
}
};
if let Some(mut quad) = *cached_quad {
quad.position += position;
Ok(Some(quad))
} else {
Ok(None)
}
}
fn resize(&mut self, device: &mut GraphicsDevice) -> Result {
let (texture_width, texture_height) = self.packer.texture().size();
let new_width = texture_width * 2;
let new_height = texture_height * 2;
self.packer.resize(device, new_width, new_height)?;
self.glyphs.clear();
self.resize_count += 1;
Ok(())
}
}
fn add_glyph_to_texture(
device: &mut GraphicsDevice,
packer: &mut ShelfPacker,
glyph: &RasterizedGlyph,
) -> std::result::Result<TextQuad, CacheError> {
const PADDING: i32 = 1;
let region = packer
.insert(
device,
&glyph.data,
glyph.bounds.width as i32,
glyph.bounds.height as i32,
PADDING,
)
.ok_or(CacheError::OutOfSpace)?;
Ok(TextQuad {
position: Vec2::new(
glyph.bounds.x - PADDING as f32,
glyph.bounds.y - PADDING as f32,
),
region: Rectangle::new(
region.x as f32,
region.y as f32,
region.width as f32,
region.height as f32,
),
})
}
struct UnicodeLineBreaks<'a> {
input: &'a str,
breaker: LineBreakIterator<'a>,
last_break: usize,
}
impl<'a> UnicodeLineBreaks<'a> {
fn new(input: &'a str) -> UnicodeLineBreaks<'a> {
UnicodeLineBreaks {
input,
breaker: LineBreakIterator::new(input),
last_break: 0,
}
}
}
impl<'a> Iterator for UnicodeLineBreaks<'a> {
type Item = (&'a str, bool);
fn next(&mut self) -> Option<Self::Item> {
self.breaker.next().map(|(offset, hard_break)| {
let word = &self.input[self.last_break..offset];
self.last_break = offset;
(word, hard_break)
})
}
}