roast2d_internal 0.3.0-alpha.1

Roast2D internal crate
Documentation
use std::{fs, hash::Hash, path::Path};

use crate::{color::Color, handle::Handle};
use anyhow::{Result, anyhow};
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
use image::{DynamicImage, ImageBuffer, Rgba};

const DEFAULT_FONT_SIZE: f32 = 40.0;

#[derive(Clone)]
pub struct Font {
    inner: fontdue::Font,
}

impl Font {
    pub fn from_bytes(bytes: Vec<u8>) -> Option<Self> {
        Self::from_bytes_with_size(bytes, DEFAULT_FONT_SIZE)
    }

    pub fn from_bytes_with_size(bytes: Vec<u8>, font_size: f32) -> Option<Self> {
        let inner = fontdue::Font::from_bytes(
            bytes,
            fontdue::FontSettings {
                scale: font_size,
                ..Default::default()
            },
        )
        .ok()?;
        Some(Self { inner })
    }

    pub fn open_with_size<P: AsRef<Path>>(path: P, font_size: f32) -> Result<Self> {
        let bytes = fs::read(path)?;
        let font =
            Self::from_bytes_with_size(bytes, font_size).ok_or_else(|| anyhow!("Invalid font"))?;
        Ok(font)
    }

    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
        Self::open_with_size(path, DEFAULT_FONT_SIZE)
    }

    pub(crate) fn render_text_texture(&self, text: &Text) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
        let Text {
            text,
            scale,
            color,
            max_width,
            max_height,
            wrap_style,
            ..
        } = text;

        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
        layout.reset(&LayoutSettings {
            x: 0.0,
            y: 0.0,
            max_height: *max_height,
            max_width: *max_width,
            wrap_style: (*wrap_style).into(),
            ..Default::default()
        });
        layout.append(&[&self.inner], &TextStyle::new(text, *scale, 0));
        let height = layout.height().ceil() as u32;
        let width = {
            let g = layout
                .glyphs()
                .iter()
                .max_by_key(|g| g.x.ceil() as usize + g.width)
                .expect("no width");
            g.x.ceil() + g.width as f32
        } as u32;
        let mut image = DynamicImage::new_rgba8(width, height).to_rgba8();

        // Render the layout to the image
        for glyph in layout.glyphs() {
            let (metrics, bitmap) = self.inner.rasterize(glyph.parent, glyph.key.px);
            for y in 0..metrics.height {
                for x in 0..metrics.width {
                    let alpha = bitmap[y * metrics.width + x];
                    let x = (glyph.x as i32 + x as i32).max(0) as u32;
                    let y = (glyph.y as i32 + y as i32).max(0) as u32;
                    if let Some(pixel) = image.get_pixel_mut_checked(x, y) {
                        let (r, g, b, _a) = color.to_rgba_u8();
                        *pixel = Rgba([r, g, b, alpha]);
                    }
                }
            }
        }
        image
    }
}

#[derive(PartialEq, Hash, Clone, Copy, Debug)]
pub enum WrapStyle {
    Word,
    Letter,
}

impl From<WrapStyle> for fontdue::layout::WrapStyle {
    fn from(value: WrapStyle) -> Self {
        match value {
            WrapStyle::Letter => fontdue::layout::WrapStyle::Letter,
            WrapStyle::Word => fontdue::layout::WrapStyle::Word,
        }
    }
}

#[derive(PartialEq, Clone, Debug)]
pub struct Text {
    pub text: String,
    pub font: Option<Handle>,
    pub scale: f32,
    pub color: Color,
    pub max_width: Option<f32>,
    pub max_height: Option<f32>,
    pub wrap_style: WrapStyle,
}

impl Eq for Text {}

impl Hash for Text {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.text.hash(state);
        self.font.hash(state);
        let (r, g, b, a) = self.color.to_rgba_u8();
        state.write_u8(r);
        state.write_u8(g);
        state.write_u8(b);
        state.write_u8(a);
        self.wrap_style.hash(state);
        state.write_u32(self.scale as u32);
        state.write_u32(self.max_width.unwrap_or_default() as u32);
        state.write_u32(self.max_height.unwrap_or_default() as u32);
    }
}

impl Text {
    pub fn new(text: String, scale: f32, color: Color) -> Self {
        Self {
            text,
            scale,
            color,
            font: None,
            max_height: None,
            max_width: None,
            wrap_style: WrapStyle::Word,
        }
    }

    /// Set font
    pub fn font(mut self, font: Handle) -> Self {
        self.font.replace(font);
        self
    }

    pub fn with_max_height(mut self, max_height: f32) -> Self {
        self.max_height.replace(max_height);
        self
    }

    pub fn with_max_width(mut self, max_width: f32) -> Self {
        self.max_width.replace(max_width);
        self
    }

    pub fn with_wrap_style(mut self, wrap_style: WrapStyle) -> Self {
        self.wrap_style = wrap_style;
        self
    }
}