#![allow(clippy::unnecessary_to_owned)]
use crate::core::{
algebra::Vector2, rectpack::RectPacker, reflect::prelude::*, uuid::Uuid, uuid_provider,
visitor::prelude::*, TypeUuidProvider,
};
use crate::font::loader::FontImportOptions;
use fxhash::FxHashMap;
use fyrox_core::math::Rect;
use fyrox_core::{err, uuid};
use fyrox_resource::manager::ResourceManager;
use fyrox_resource::state::LoadError;
use fyrox_resource::untyped::ResourceKind;
use fyrox_resource::{
embedded_data_source, io::ResourceIo, manager::BuiltInResource, untyped::UntypedResource,
Resource, ResourceData,
};
use std::sync::LazyLock;
use std::{
error::Error,
fmt::{Debug, Formatter},
hash::{Hash, Hasher},
ops::Deref,
path::Path,
};
pub mod loader;
const MAX_FALLBACK_DEPTH: usize = 10;
enum FontError {
FallbackNotLoaded,
GlyphTooLarge,
}
#[derive(Debug, Clone)]
pub struct FontGlyph {
pub bitmap_top: f32,
pub bitmap_left: f32,
pub bitmap_width: f32,
pub bitmap_height: f32,
pub advance: f32,
pub tex_coords: [Vector2<f32>; 4],
pub page_index: usize,
pub bounds: Rect<f32>,
}
#[derive(Clone)]
pub struct Page {
pub pixels: Vec<u8>,
pub texture: Option<UntypedResource>,
pub rect_packer: RectPacker<usize>,
pub modified: bool,
}
impl Debug for Page {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Page")
.field("Pixels", &self.pixels)
.field("Texture", &self.texture)
.field("Modified", &self.modified)
.finish()
}
}
#[derive(Default, Clone, Debug)]
pub struct Atlas {
pub glyphs: Vec<FontGlyph>,
pub char_map: FxHashMap<char, usize>,
pub pages: Vec<Page>,
}
impl Atlas {
fn render_glyph(
&mut self,
font: &'_ fontdue::Font,
unicode: char,
char_index: u16,
height: FontHeight,
page_size: usize,
) -> Result<usize, FontError> {
let border = 2;
let (metrics, glyph_raster) = font.rasterize_indexed(char_index, height.0);
let mut placement_info =
self.pages
.iter_mut()
.enumerate()
.find_map(|(page_index, page)| {
page.rect_packer
.find_free(metrics.width + border, metrics.height + border)
.map(|bounds| (page_index, bounds))
});
if placement_info.is_none() {
let mut page = Page {
pixels: vec![0; page_size * page_size],
texture: None,
rect_packer: RectPacker::new(page_size, page_size),
modified: true,
};
let page_index = self.pages.len();
if let Some(bounds) = page
.rect_packer
.find_free(metrics.width + border, metrics.height + border)
{
placement_info = Some((page_index, bounds));
self.pages.push(page);
}
}
let Some((page_index, placement_rect)) = placement_info else {
err!(
"Font error: The atlas page size is too small for a requested glyph at font size {}.\
Glyph width: {}, height: {}, atlas page size: {}",
height.0,
metrics.width + border,
metrics.height + border,
page_size,
);
return Err(FontError::GlyphTooLarge);
};
let page = &mut self.pages[page_index];
let glyph_index = self.glyphs.len();
page.modified = true;
let mut glyph = FontGlyph {
bitmap_left: metrics.xmin as f32,
bitmap_top: metrics.ymin as f32,
advance: metrics.advance_width,
tex_coords: Default::default(),
bitmap_width: metrics.width as f32,
bitmap_height: metrics.height as f32,
bounds: Rect::new(
metrics.bounds.xmin,
metrics.bounds.ymin,
metrics.bounds.width,
metrics.bounds.height,
),
page_index,
};
let k = 1.0 / page_size as f32;
let bw = placement_rect.w().saturating_sub(border);
let bh = placement_rect.h().saturating_sub(border);
let bx = placement_rect.x() + border / 2;
let by = placement_rect.y() + border / 2;
let tw = bw as f32 * k;
let th = bh as f32 * k;
let tx = bx as f32 * k;
let ty = by as f32 * k;
glyph.tex_coords[0] = Vector2::new(tx, ty);
glyph.tex_coords[1] = Vector2::new(tx + tw, ty);
glyph.tex_coords[2] = Vector2::new(tx + tw, ty + th);
glyph.tex_coords[3] = Vector2::new(tx, ty + th);
let row_end = by + bh;
let col_end = bx + bw;
for (src_row, row) in (by..row_end).enumerate() {
for (src_col, col) in (bx..col_end).enumerate() {
page.pixels[row * page_size + col] = glyph_raster[src_row * bw + src_col];
}
}
self.glyphs.push(glyph);
self.char_map.insert(unicode, glyph_index);
Ok(glyph_index)
}
fn glyph(
&mut self,
font: &fontdue::Font,
unicode: char,
height: FontHeight,
page_size: usize,
fallbacks: &[Option<FontResource>],
) -> Option<&FontGlyph> {
match self.char_map.get(&unicode) {
Some(glyph_index) => self.glyphs.get(*glyph_index),
None => {
let glyph_index = if let Some(char_index) = font.chars().get(&unicode) {
self.render_glyph(font, unicode, char_index.get(), height, page_size)
.ok()
} else {
match self.fallback_glyph(
MAX_FALLBACK_DEPTH,
fallbacks,
unicode,
height,
page_size,
) {
Ok(Some(glyph_index)) => Some(glyph_index),
Ok(None) | Err(FontError::GlyphTooLarge) => {
self.render_glyph(font, unicode, 0, height, page_size).ok()
}
Err(FontError::FallbackNotLoaded) => {
None
}
}
};
glyph_index.and_then(|i| self.glyphs.get(i))
}
}
}
fn fallback_glyph(
&mut self,
depth: usize,
fonts: &[Option<FontResource>],
unicode: char,
height: FontHeight,
page_size: usize,
) -> Result<Option<usize>, FontError> {
let Some(depth) = depth.checked_sub(1) else {
return Ok(None);
};
for font in fonts.iter().flatten() {
if !font.is_ok() {
return Err(FontError::FallbackNotLoaded);
}
let font = font.data_ref();
let inner = font
.inner
.as_ref()
.expect("Fallback font reader must be initialized!");
if let Some(char_index) = inner.chars().get(&unicode) {
return self
.render_glyph(inner, unicode, char_index.get(), height, page_size)
.map(Some);
} else if let Some(glyph_index) =
self.fallback_glyph(depth, &font.fallbacks, unicode, height, page_size)?
{
return Ok(Some(glyph_index));
}
}
Ok(None)
}
}
#[derive(Default, Clone, Debug, Reflect, Visit)]
pub struct Font {
#[reflect(hidden)]
#[visit(skip)]
pub inner: Option<fontdue::Font>,
#[reflect(hidden)]
#[visit(skip)]
pub atlases: FxHashMap<FontHeight, Atlas>,
#[reflect(hidden)]
#[visit(skip)]
pub page_size: usize,
#[visit(skip)]
pub bold: Option<FontResource>,
#[visit(skip)]
pub italic: Option<FontResource>,
#[visit(skip)]
pub bold_italic: Option<FontResource>,
#[visit(skip)]
pub fallbacks: Vec<Option<FontResource>>,
}
uuid_provider!(Font = "692fec79-103a-483c-bb0b-9fc3a349cb48");
impl ResourceData for Font {
fn type_uuid(&self) -> Uuid {
<Self as TypeUuidProvider>::type_uuid()
}
fn save(&mut self, _path: &Path) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn can_be_saved(&self) -> bool {
false
}
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
Some(Box::new(self.clone()))
}
}
#[derive(Copy, Clone, Default, Debug)]
pub struct FontHeight(pub f32);
impl From<f32> for FontHeight {
fn from(value: f32) -> Self {
Self(value)
}
}
impl PartialEq for FontHeight {
fn eq(&self, other: &Self) -> bool {
fyrox_core::value_as_u8_slice(&self.0) == fyrox_core::value_as_u8_slice(&other.0)
}
}
impl Eq for FontHeight {}
impl Hash for FontHeight {
fn hash<H: Hasher>(&self, state: &mut H) {
fyrox_core::hash_as_bytes(&self.0, state)
}
}
pub type FontResource = Resource<Font>;
pub static BOLD_ITALIC: LazyLock<BuiltInResource<Font>> = LazyLock::new(|| {
BuiltInResource::new(
"Bold Italic Font",
embedded_data_source!("./bold_italic.ttf"),
|data| {
FontResource::new_ok(
uuid!("f5b02124-9601-452a-9368-3fa2a9703ecd"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, FontStyles::default(), Vec::default())
.unwrap(),
)
},
)
});
pub static BUILT_IN_ITALIC: LazyLock<BuiltInResource<Font>> = LazyLock::new(|| {
BuiltInResource::new(
"Italic Font",
embedded_data_source!("./built_in_italic.ttf"),
|data| {
let bold = Some(BOLD_ITALIC.resource());
let styles = FontStyles {
bold,
..FontStyles::default()
};
FontResource::new_ok(
uuid!("1cd79487-6c76-4370-91c2-e6e1e728950a"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
},
)
});
pub static BUILT_IN_BOLD: LazyLock<BuiltInResource<Font>> = LazyLock::new(|| {
BuiltInResource::new(
"Bold Font",
embedded_data_source!("./built_in_bold.ttf"),
|data| {
let italic = Some(BOLD_ITALIC.resource());
let styles = FontStyles {
italic,
..FontStyles::default()
};
FontResource::new_ok(
uuid!("8a471243-2466-4241-a4cb-c341ce8e844a"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
},
)
});
pub static BUILT_IN_FONT: LazyLock<BuiltInResource<Font>> = LazyLock::new(|| {
BuiltInResource::new(
"Default Font",
embedded_data_source!("./built_in_font.ttf"),
|data| {
let styles = FontStyles {
bold: Some(BUILT_IN_BOLD.resource()),
italic: Some(BUILT_IN_ITALIC.resource()),
bold_italic: Some(BOLD_ITALIC.resource()),
};
FontResource::new_ok(
uuid!("77260e8e-f6fa-429c-8009-13dda2673925"),
ResourceKind::External,
Font::from_memory(data.to_vec(), 1024, styles, Vec::default()).unwrap(),
)
},
)
});
pub async fn wait_for_subfonts(font: FontResource) -> Result<FontResource, LoadError> {
let mut stack = Vec::new();
let font = font.await?;
let bold = font.data_ref().bold.clone();
if let Some(bold) = bold {
wait_for_fallbacks(bold, &mut stack).await?;
stack.clear();
}
let italic = font.data_ref().italic.clone();
if let Some(italic) = italic {
wait_for_fallbacks(italic, &mut stack).await?;
stack.clear();
}
let bold_italic = font.data_ref().bold_italic.clone();
if let Some(bold_italic) = bold_italic {
wait_for_fallbacks(bold_italic, &mut stack).await?;
stack.clear();
}
wait_for_fallbacks(font, &mut stack).await
}
fn write_font_names<W: std::fmt::Write>(fonts: &[FontResource], out: &mut W) -> std::fmt::Result {
fn write_name<W: std::fmt::Write>(font: &FontResource, out: &mut W) -> std::fmt::Result {
if font.is_ok() {
out.write_str(font.data_ref().name().unwrap_or("unnamed"))
} else {
out.write_str("unnamed")
}
}
if let Some((first, rest)) = fonts.split_first() {
write_name(first, out)?;
for font in rest {
out.write_str(" > ")?;
write_name(font, out)?;
}
}
Ok(())
}
async fn wait_for_fallbacks(
font: FontResource,
stack: &mut Vec<FontResource>,
) -> Result<FontResource, LoadError> {
if stack.contains(&font) {
let mut err = "Cyclic fallback fonts at: ".to_string();
write_font_names(stack, &mut err).unwrap();
return Err(LoadError::new(err));
}
stack.push(font.clone());
let font = font.await?;
let fallbacks = font
.data_ref()
.fallbacks
.iter()
.flatten()
.cloned()
.collect::<Vec<_>>();
for fallback in fallbacks {
Box::pin(wait_for_fallbacks(fallback, stack)).await?;
}
_ = stack.pop();
Ok(font)
}
#[derive(Default, Debug, Clone)]
pub struct FontStyles {
pub bold: Option<FontResource>,
pub italic: Option<FontResource>,
pub bold_italic: Option<FontResource>,
}
impl Font {
pub fn name(&self) -> Option<&str> {
self.inner.as_ref().and_then(|f| f.name())
}
pub fn from_memory(
data: impl Deref<Target = [u8]>,
page_size: usize,
styles: FontStyles,
fallbacks: Vec<Option<FontResource>>,
) -> Result<Self, &'static str> {
let fontdue_font = fontdue::Font::from_bytes(data, fontdue::FontSettings::default())?;
Ok(Font {
inner: Some(fontdue_font),
atlases: Default::default(),
page_size,
bold: styles.bold,
italic: styles.italic,
bold_italic: styles.bold_italic,
fallbacks,
})
}
pub async fn from_file<P: AsRef<Path>>(
path: P,
options: FontImportOptions,
io: &dyn ResourceIo,
resource_manager: &ResourceManager,
) -> Result<Self, LoadError> {
if let Ok(file_content) = io.load_file(path.as_ref()).await {
let page_size = options.page_size;
let mut bold = options.bold;
let mut italic = options.italic;
let mut bold_italic = options.bold_italic;
if let Some(bold) = &mut bold {
resource_manager.request_resource(bold);
}
if let Some(italic) = &mut italic {
resource_manager.request_resource(italic);
}
if let Some(bold_italic) = &mut bold_italic {
resource_manager.request_resource(bold_italic);
}
let mut fallbacks = options.fallbacks;
for font in fallbacks.iter_mut().flatten() {
resource_manager.request_resource(font);
}
let styles = FontStyles {
bold,
italic,
bold_italic,
};
Self::from_memory(file_content, page_size, styles, fallbacks).map_err(LoadError::new)
} else {
Err(LoadError::new("Unable to read file"))
}
}
#[inline]
pub fn glyph(&mut self, unicode: char, height: f32) -> Option<&FontGlyph> {
if !height.is_finite() || height <= f32::EPSILON {
return None;
}
let height = FontHeight(height);
let inner = self
.inner
.as_ref()
.expect("Font reader must be initialized!");
self.atlases.entry(height).or_default().glyph(
inner,
unicode,
height,
self.page_size,
&self.fallbacks,
)
}
#[inline]
pub fn ascender(&self, height: f32) -> f32 {
self.inner
.as_ref()
.unwrap()
.horizontal_line_metrics(height)
.map(|m| m.ascent)
.unwrap_or_default()
}
#[inline]
pub fn descender(&self, height: f32) -> f32 {
self.inner
.as_ref()
.unwrap()
.horizontal_line_metrics(height)
.map(|m| m.descent)
.unwrap_or_default()
}
#[inline]
pub fn horizontal_kerning(&self, height: f32, left: char, right: char) -> Option<f32> {
self.inner
.as_ref()
.unwrap()
.horizontal_kern(left, right, height)
}
#[inline]
pub fn page_size(&self) -> usize {
self.page_size
}
#[inline]
pub fn glyph_advance(&mut self, unicode: char, height: f32) -> f32 {
self.glyph(unicode, height)
.map_or(height, |glyph| glyph.advance)
}
}
pub struct FontBuilder {
page_size: usize,
bold: Option<FontResource>,
italic: Option<FontResource>,
bold_italic: Option<FontResource>,
fallbacks: Vec<Option<FontResource>>,
}
impl FontBuilder {
pub fn new() -> Self {
Self {
page_size: 1024,
bold: None,
italic: None,
bold_italic: None,
fallbacks: Vec::default(),
}
}
pub fn with_page_size(mut self, size: usize) -> Self {
self.page_size = size;
self
}
pub fn with_bold(mut self, font: FontResource) -> Self {
self.bold = Some(font);
self
}
pub fn with_italic(mut self, font: FontResource) -> Self {
self.italic = Some(font);
self
}
pub fn with_bold_italic(mut self, font: FontResource) -> Self {
self.bold_italic = Some(font);
self
}
pub fn with_fallback(mut self, font: FontResource) -> Self {
self.fallbacks.push(Some(font));
self
}
pub fn with_fallbacks(mut self, fallbacks: Vec<Option<FontResource>>) -> Self {
self.fallbacks = fallbacks;
self
}
fn into_options(self) -> FontImportOptions {
FontImportOptions {
page_size: self.page_size,
bold: self.bold,
italic: self.italic,
bold_italic: self.bold_italic,
fallbacks: self.fallbacks,
}
}
pub async fn build_from_file(
self,
path: impl AsRef<Path>,
io: &dyn ResourceIo,
resource_manager: &ResourceManager,
) -> Result<Font, LoadError> {
Font::from_file(path, self.into_options(), io, resource_manager).await
}
pub fn build_from_memory(
mut self,
data: impl Deref<Target = [u8]>,
resource_manager: &ResourceManager,
) -> Result<Font, &'static str> {
for font in self.fallbacks.iter_mut().flatten() {
resource_manager.request_resource(font);
}
let styles = FontStyles {
bold: self.bold,
italic: self.italic,
bold_italic: self.bold_italic,
};
Font::from_memory(data, self.page_size, styles, self.fallbacks)
}
}