ombre 0.6.7

Shadowy game and graphics library for Rust
Documentation
//! Bitmap font encodings.
use std::array::TryFromSliceError;
use std::fmt;

use crate::gfx::pixels::PixelsMut;
use crate::gfx::*;
use crate::math::*;

/// Font error.
#[derive(Debug, Clone)]
pub enum Error {
    TryFromSlice(TryFromSliceError),
    TileCount(usize, usize),
    ByteLength(usize),
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::TryFromSlice(e) => write!(f, "invalid font: {e}"),
            Self::TileCount(a, e) => write!(f, "invalid tile count `{a}`, expected `{e}`"),
            Self::ByteLength(l) => write!(f, "invalid font byte length '{l}'"),
        }
    }
}

/// Font format.
#[derive(Debug, Clone, Copy)]
pub enum FontFormat {
    UF1,
    UF2,
}

impl FontFormat {
    /// Text line height.
    pub fn line_height(&self) -> f32 {
        match self {
            Self::UF1 => 8.,
            Self::UF2 => 16.,
        }
    }
}

/// Bitmap font.
#[derive(Debug, Clone, Copy)]
pub struct Font<T> {
    /// Glyph widths.
    pub widths: [u8; 256],
    /// Font texture or image.
    pub image: T,
    /// Font glyph size.
    pub format: FontFormat,
}

impl<T> Font<T> {
    /// Decode a font from a byte slice.
    pub fn decode(bytes: &[u8], format: FontFormat) -> Result<Font<Image>, Error> {
        // Tile width and height. For UF2 fonts, each glyph is represented by four tiles.
        const T: usize = 8;
        // Glyph count. Represents the ASCII range.
        const N: usize = 256;
        // Number of tiles per glyph.
        const G: usize = 2 * 2;

        assert!(
            matches!(format, FontFormat::UF2),
            "Only UF2 fonts are currently supported"
        );

        let (widths, glyphs) = bytes.split_at(N);
        let (head, tiles, tail) = unsafe { glyphs.align_to::<[u8; T]>() };

        if !head.is_empty() || !tail.is_empty() {
            return Err(Error::ByteLength(glyphs.len()));
        }
        if tiles.len() != N * G {
            return Err(Error::TileCount(tiles.len(), N * G));
        }

        // Rasterize the font into a 256x256 texture.
        let size = Size::new(N, N);
        let widths: [u8; N] = widths.try_into().map_err(Error::TryFromSlice)?;
        let mut texels = vec![Rgba8::ZERO; size.area()];
        let mut pixels = PixelsMut::new(&mut texels, size.w, size.h);

        // Each glyph is a 2x2 grid of tiles encoded in the following order:
        //
        //   0 2
        //   1 3
        //
        // We loop through the tiles in chunks of 2, where each iteration renders half a glyph
        // into the texture.
        //
        let v = Rgba8::WHITE;
        let (mut x, mut y) = (0, 0);

        for window in tiles.chunks(G / 2) {
            if let &[a, b] = window {
                pixels.icn(a, x, y, v);
                pixels.icn(b, x, y + T, v);

                x += T;

                if x == size.w {
                    x = 0;
                    y += T + T;
                }
            }
        }

        Ok(Font {
            widths,
            format,
            image: Image::new(texels, size),
        })
    }

    /// Glyph width of given character.
    pub fn glyph_width(&self, c: u8) -> f32 {
        self.widths[c as usize] as f32
    }

    /// Width of the given text string.
    pub fn text_width(&self, text: &str) -> f32 {
        text.bytes().map(|c| self.glyph_width(c)).sum()
    }

    /// Line height.
    pub fn line_height(&self) -> f32 {
        self.format.line_height()
    }
}