roast2d_internal 0.3.6

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

use anyhow::{Result, anyhow};
use glam::{UVec2, Vec2};
use hashbrown::HashMap;

use crate::{
    handle::Handle,
    text::{Text, TextSection},
};
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
use image::{DynamicImage, ImageBuffer, Rgba};

const DEFAULT_FONT_SIZE: f32 = 40.0;

#[derive(Clone)]
pub struct Font {
    pub 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)
    }
}

/// Font config
pub struct FontConfig {
    pub scale: f32,
    pub offset: Vec2,
    pub oversample_scale: f32,
}

impl Default for FontConfig {
    fn default() -> Self {
        Self {
            scale: 1.0,
            offset: Vec2::ZERO,
            oversample_scale: 2.0,
        }
    }
}

/// Fonts
#[derive(Default)]
pub struct FontManager {
    pub(crate) fonts: HashMap<u64, Font>,
    pub(crate) default_font: Option<Handle>,
    // Font config
    pub(crate) font_config: FontConfig,
}

impl FontManager {
    pub fn get_font(&self, handle: Option<&Handle>) -> Option<&Font> {
        let h = handle.or(self.default_font.as_ref())?;
        self.fonts.get(&h.id())
    }

    pub fn add_font(&mut self, handle_id: u64, font: Font) {
        self.fonts.insert(handle_id, font);
    }

    pub fn remove_font(&mut self, handle_id: u64) {
        self.fonts.remove(&handle_id);
    }

    pub(crate) fn set_default_font(&mut self, handle: Handle) {
        self.default_font.replace(handle);
    }

    pub(crate) fn set_font_config(&mut self, config: FontConfig) {
        self.font_config = config;
    }

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

        let sample_scale_factor = dpi_scale_factor * self.font_config.oversample_scale;
        let inv_sample_scale_factor = sample_scale_factor.recip();

        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
        layout.reset(&LayoutSettings {
            x: 0.0,
            y: 0.0,
            max_height: max_height.map(|h| h * sample_scale_factor),
            max_width: max_width.map(|w| w * sample_scale_factor),
            wrap_style: (*wrap_style).into(),
            ..Default::default()
        });

        let mut fonts = Vec::with_capacity(sections.len());

        for TextSection {
            text,
            font,
            scale,
            color,
        } in sections
        {
            let Some(font) = self.get_font(font.as_ref()) else {
                log::error!("Cannot find font {:?} when render text", font.as_ref());
                continue;
            };
            let font_index = fonts.len();
            fonts.push(&font.inner);

            // Apply DPI factor
            let scale = scale * self.font_config.scale * sample_scale_factor;
            layout.append(
                &fonts,
                &TextStyle::with_user_data(text, scale, font_index, color),
            );
        }

        let glyphs = layout.glyphs();

        let height = glyphs
            .iter()
            .filter(|g| !g.parent.is_control())
            .map(|g| (g.y as usize) + g.height + 1) // +1 for rounding safety
            .max()
            .map(|h| h as f32)
            .unwrap_or(0.0)
            .max(layout.height().ceil()) as u32;

        let mut width = glyphs
            .iter()
            .filter(|g| !g.parent.is_control())
            .map(|g| (g.x as usize) + g.width + 1) // +1 for rounding safety
            .max()
            .map(|w| w as f32)
            .unwrap_or(0.0) as u32;
        if width == 0 {
            width = 1;
        }
        let mut height = height;
        if height == 0 {
            height = 1;
        }
        let mut image = DynamicImage::new_rgba8(width, height).to_rgba8();

        // Render the layout to the image
        for glyph in glyphs.iter().filter(|g| !g.parent.is_control()) {
            let font = &fonts[glyph.font_index];
            let (metrics, bitmap) = font.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) = glyph.user_data.to_rgba_u8();
                        *pixel = Rgba([r, g, b, alpha]);
                    }
                }
            }
        }

        // real size without dpi affection
        // Use ceil to ensure we don't truncate pixels when scaling down
        let size = (UVec2::new(image.width(), image.height()).as_vec2() * inv_sample_scale_factor)
            .ceil()
            .as_uvec2();
        (image, size)
    }
}