tge 0.0.4

A lightweight cross-platform 2D game framework written in pure Rust and based on OpenGL 3.3+.
Documentation
use super::{Graphics, opengl, Filter, Texture};
use crate::error::{GameError, GameResult};
use crate::math::{Size, Region};
use crate::engine::Engine;
use fontdue::{FontSettings, Metrics};
use std::path::Path;
use std::collections::HashMap;
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct GlyphDrawInfo {
    pub uv_bounds: Region,
    pub uv: Region,
}

impl GlyphDrawInfo {
    fn new(metrics: Metrics, hidpi_scale_factor: f32, uv: Region) -> Self {
        let uv_bounds = Region::new(
            metrics.xmin as f32 / hidpi_scale_factor,
            -(metrics.height as f32 + metrics.ymin as f32) / hidpi_scale_factor,
            metrics.width as f32 / hidpi_scale_factor,
            metrics.height as f32 / hidpi_scale_factor,
        );
        Self {
            uv_bounds,
            uv,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LineMetrics {
    pub ascent: f32,
    pub descent: f32,
    pub height: f32,
    pub line_gap: f32,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct GlyphMetrics {
    pub advance_width: f32,
}

pub enum CachedBy {
    Added(GlyphDrawInfo),
    Existed(GlyphDrawInfo),
}

pub enum CacheError {
    TooLarge,
    NoRoom,
}

#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
struct CacheKey {
    c: char,
    px: u32,
}

struct Cache {
    texture: Texture,
    texture_size: u32,
    draw_infos: HashMap<CacheKey, (Metrics, Region)>,
    rows: Vec<Size<u32>>,
}

pub struct Font {
    font: fontdue::Font,
    cache: RefCell<Cache>,
    hidpi_scale_factor: Option<f32>,
}

impl Font {
    pub(crate) fn new(graphics: &mut Graphics, bytes: &[u8], cache_texture_size: u32) -> GameResult<Self> {
        let font = fontdue::Font::from_bytes(bytes, FontSettings::default())
            .map_err(|error| GameError::InitError(error.into()))?;
        let cache_texture = Texture::for_font_cache(graphics, cache_texture_size)?;
        let cache = Cache {
            texture: cache_texture,
            texture_size: cache_texture_size,
            draw_infos: HashMap::new(),
            rows: Vec::new(),
        };
        Ok(Self {
            font,
            cache: RefCell::new(cache),
            hidpi_scale_factor: None,
        })
    }

    pub fn from_bytes(graphics: &mut Graphics, bytes: &[u8]) -> GameResult<Self> {
        Self::new(graphics, bytes, 1024)
    }

    pub fn load(engine: &mut Engine, path: impl AsRef<Path>) -> GameResult<Self> {
        let bytes = engine.filesystem().read(path)?;
        Self::from_bytes(engine.graphics(), &bytes)
    }

    pub(crate) fn line_metrics(&self, px: f32) -> LineMetrics {
        let horizontal_line_metrics = self.font.horizontal_line_metrics(px)
            .expect("no horizontal line metrics");
        LineMetrics {
            ascent: horizontal_line_metrics.ascent,
            descent: horizontal_line_metrics.descent,
            height: horizontal_line_metrics.ascent - horizontal_line_metrics.descent,
            line_gap: horizontal_line_metrics.line_gap,
        }
    }

    pub(crate) fn glyph_metrics(&self, c: char, px: f32) -> GlyphMetrics {
        let metrics = self.font.metrics(c, px);
        GlyphMetrics {
            advance_width: metrics.advance_width,
        }
    }

    pub(crate) fn cache_texture(&self) -> Rc<opengl::Texture> {
        self.cache.borrow().texture.texture().clone()
    }

    pub(crate) fn cache_texture_size(&self) -> u32 {
        self.cache.borrow().texture_size
    }

    pub(crate) fn cache_glyph(&self, c: char, px: f32, hidpi_scale_factor: f32) -> Result<CachedBy, CacheError> {
        let px = (px * hidpi_scale_factor).round();
        let cache_key = CacheKey { c, px: px as u32 };
        let mut cache = self.cache.borrow_mut();
        if let Some((metrics, uv)) = cache.draw_infos.get(&cache_key) {
            let draw_info = GlyphDrawInfo::new(*metrics, hidpi_scale_factor, *uv);
            return Ok(CachedBy::Existed(draw_info));
        }
        let metrics = self.font.metrics(c, px);
        let glyph_size = Size::new(metrics.width as u32, metrics.height as u32);
        let glyph_cache_size = Size::new(glyph_size.width + 1, glyph_size.height + 1);
        let cache_texture_size = cache.texture_size;
        if glyph_cache_size.width > cache_texture_size || glyph_cache_size.height > cache_texture_size {
            return Err(CacheError::TooLarge);
        }
        let mut region = None;
        let mut row_bottom = 0;
        for row in cache.rows.iter_mut() {
            if glyph_cache_size.height <= row.height && glyph_cache_size.width <= cache_texture_size - row.width {
                region = Some(Region::new(row.width, row_bottom, glyph_size.width, glyph_size.height));
                row.width += glyph_cache_size.width;
                break;
            }
            row_bottom += row.height;
        }
        if region.is_none() {
            if glyph_cache_size.height <= cache_texture_size - row_bottom {
                region = Some(Region::new(0, row_bottom, glyph_size.width, glyph_size.height));
                cache.rows.push(glyph_cache_size);
            }
        }
        if let Some(region) = region {
            let (_, bitmap) = self.font.rasterize(c, px);
            let mut pixels = Vec::with_capacity(bitmap.len() * 4);
            for alpha in bitmap {
                pixels.push(255);
                pixels.push(255);
                pixels.push(255);
                pixels.push(alpha);
            }
            cache.texture.update_pixels(region, Some(&pixels))
                .expect("update font cache texture error");
            let uv = {
                let texture_size = {
                    let texture_size = cache.texture.size();
                    Size::new(texture_size.width as f32, texture_size.height as f32)
                };
                Region::new(
                    region.x as f32 / texture_size.width,
                    region.y as f32 / texture_size.height,
                    region.width as f32 / texture_size.width,
                    region.height as f32 / texture_size.height,
                )
            };
            cache.draw_infos.insert(cache_key, (metrics, uv));
            let draw_info = GlyphDrawInfo::new(metrics, hidpi_scale_factor, uv);
            Ok(CachedBy::Added(draw_info))
        } else {
            Err(CacheError::NoRoom)
        }
    }

    pub(crate) fn clear_cache(&self) {
        let mut cache = self.cache.borrow_mut();
        cache.draw_infos.clear();
        cache.rows.clear();
    }

    pub(crate) fn resize_cache(&self, cache_texture_size: u32) {
        let mut cache = self.cache.borrow_mut();
        cache.draw_infos.clear();
        cache.rows.clear();
        cache.texture.resize((cache_texture_size, cache_texture_size));
        cache.texture_size = cache_texture_size;
    }

    pub fn filter(&self) -> Filter {
        self.cache.borrow().texture.filter()
    }

    pub fn set_filter(&mut self, filter: Filter) {
        self.cache.borrow_mut().texture.set_filter(filter)
    }

    pub fn hidpi_scale_factor(&self) -> Option<f32> {
        self.hidpi_scale_factor
    }

    pub fn set_hidpi_scale_factor(&mut self, hidpi_scale_factor: Option<f32>) {
        self.hidpi_scale_factor = hidpi_scale_factor;
    }
}