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();
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,
}
}
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
}
}