From 1cc075858d056e5f81ed1fc6d89c3e340264c2b3 Mon Sep 17 00:00:00 2001 From: b-guild <148972828+b-guild@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:02:23 -0700 Subject: [PATCH] Font fallbacks --- editor/src/lib.rs | 3 + editor/src/plugins/inspector/editors/mod.rs | 5 + fyrox-impl/src/engine/mod.rs | 2 +- fyrox-ui/src/font/loader.rs | 54 +- fyrox-ui/src/font/mod.rs | 578 +++++++++++++++----- project-manager/src/main.rs | 3 + 6 files changed, 502 insertions(+), 143 deletions(-) diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 063c21754..7cf428809 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -819,6 +819,9 @@ impl Editor { *font_state_data = Font::from_memory( include_bytes!("../resources/Roboto-Regular.ttf").as_slice(), 1024, + None, + None, + Vec::default(), ) .unwrap(); } diff --git a/editor/src/plugins/inspector/editors/mod.rs b/editor/src/plugins/inspector/editors/mod.rs index c6afcc9f0..0a7ff4d56 100644 --- a/editor/src/plugins/inspector/editors/mod.rs +++ b/editor/src/plugins/inspector/editors/mod.rs @@ -169,6 +169,7 @@ use crate::{ }, }, }; +use fyrox::gui::font::Font; use fyrox::gui::style::resource::StyleResource; use fyrox::gui::style::Style; use fyrox::scene::base::SceneNodeId; @@ -294,6 +295,7 @@ pub fn make_property_editors_container( container.insert(InheritablePropertyEditorDefinition::::new()); container.insert(InheritablePropertyEditorDefinition::>::new()); container.insert(InheritablePropertyEditorDefinition::>::new()); + container.register_inheritable_vec_collection::>(); container.register_inheritable_vec_collection::>(); container.register_inheritable_vec_collection::>(); @@ -337,6 +339,9 @@ pub fn make_property_editors_container( container.register_inheritable_vec_collection::(); } + container.insert(ResourceFieldPropertyEditorDefinition::::new( + sender.clone(), + )); container.insert(ResourceFieldPropertyEditorDefinition::::new( sender.clone(), )); diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index 12bb3e29d..a1c1518ff 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -1374,7 +1374,7 @@ pub(crate) fn initialize_resource_manager_loaders( loaders.set(MaterialLoader { resource_manager: resource_manager.clone(), }); - loaders.set(FontLoader::default()); + loaders.set(FontLoader::new(resource_manager.clone())); loaders.set(UserInterfaceLoader { resource_manager: resource_manager.clone(), }); diff --git a/fyrox-ui/src/font/loader.rs b/fyrox-ui/src/font/loader.rs index 21a54ef60..508261bb0 100644 --- a/fyrox-ui/src/font/loader.rs +++ b/fyrox-ui/src/font/loader.rs @@ -22,12 +22,15 @@ use crate::{ core::{reflect::prelude::*, uuid::Uuid, TypeUuidProvider}, - font::Font, + font::{Font, FontResource}, }; use fyrox_resource::{ io::ResourceIo, - loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader}, - options::{try_get_import_settings, ImportOptions}, + loader::{BoxedImportOptionsLoaderFuture, BoxedLoaderFuture, LoaderPayload, ResourceLoader}, + manager::ResourceManager, + options::{ + try_get_import_settings, try_get_import_settings_opaque, BaseImportOptions, ImportOptions, + }, state::LoadError, }; use serde::{Deserialize, Serialize}; @@ -37,16 +40,31 @@ fn default_page_size() -> usize { 1024 } +/// Options to control how a font is imported, allowing data to be included beyond +/// what is stored in the font file. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Reflect, Eq)] pub struct FontImportOptions { + /// The size of each page of the atlas where glyphs are copied before they are rendered. + /// Each page is a square `page_size` x `page_size` large, and no glyph may be so large + /// that it cannot fit in that square or else it will fail to render. #[serde(default = "default_page_size")] pub page_size: usize, + /// The bold version of this font. + pub bold: Option, + /// The italic version of this font. + pub italic: Option, + /// Fallback fonts are used for rendering special characters that do not have glyphs in this + /// font. + pub fallbacks: Vec>, } impl Default for FontImportOptions { fn default() -> Self { Self { page_size: default_page_size(), + bold: None, + italic: None, + fallbacks: Vec::default(), } } } @@ -54,11 +72,22 @@ impl Default for FontImportOptions { impl ImportOptions for FontImportOptions {} /// Default implementation for font loading. -#[derive(Default)] pub struct FontLoader { + /// Resource manager to allow fallback font loading. + pub resource_manager: ResourceManager, default_import_options: FontImportOptions, } +impl FontLoader { + /// Create an instance of the font loader using the given resource manager. + pub fn new(resource_manager: ResourceManager) -> Self { + Self { + resource_manager, + default_import_options: FontImportOptions::default(), + } + } +} + impl ResourceLoader for FontLoader { fn extensions(&self) -> &[&str] { &["ttf", "otf"] @@ -68,8 +97,23 @@ impl ResourceLoader for FontLoader { Font::type_uuid() } + fn default_import_options(&self) -> Option> { + Some(Box::new(self.default_import_options.clone())) + } + + fn try_load_import_settings( + &self, + resource_path: PathBuf, + io: Arc, + ) -> BoxedImportOptionsLoaderFuture { + Box::pin(async move { + try_get_import_settings_opaque::(&resource_path, &*io).await + }) + } + fn load(&self, path: PathBuf, io: Arc) -> BoxedLoaderFuture { let default_import_options = self.default_import_options.clone(); + let resource_manager = self.resource_manager.clone(); Box::pin(async move { let io = io.as_ref(); @@ -77,7 +121,7 @@ impl ResourceLoader for FontLoader { .await .unwrap_or(default_import_options); - let font = Font::from_file(&path, import_options.page_size, io) + let font = Font::from_file(&path, import_options, io, &resource_manager) .await .map_err(LoadError::new)?; Ok(LoaderPayload::new(font)) diff --git a/fyrox-ui/src/font/mod.rs b/fyrox-ui/src/font/mod.rs index b168eaadd..6aeb888e9 100644 --- a/fyrox-ui/src/font/mod.rs +++ b/fyrox-ui/src/font/mod.rs @@ -18,15 +18,22 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +//! A font resource allows [`FormattedText`](crate::formatted_text::FormattedText) +//! to render text as a series of glyphs taken from a font file such as a ttf file +//! or an otf file. + #![allow(clippy::unnecessary_to_owned)] // false-positive 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::uuid; +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, @@ -43,24 +50,65 @@ use std::{ pub mod loader; +/// Arbitrarily chosen limit to the number of levels of recursion +/// we will search through fallbacks. In most cases a limit of 1 should +/// be sufficient and if we get to 10 that most likely indicates +/// a cycle in the fallback fonts. +const MAX_FALLBACK_DEPTH: usize = 10; + +enum FontError { + FallbackNotLoaded, + GlyphTooLarge, +} + +/// The geometric data specifying where to find a glyph on a font atlas +/// texture for rendering text. #[derive(Debug, Clone)] pub struct FontGlyph { + /// The vertical position of the glyph relative to other glyphs on the line, measured in font pixels. + /// This would be 0 for a glyph with its bottom directly on the baseline, but may be + /// negative if the glyph extends below the baseline. pub bitmap_top: f32, + /// The horizontal position of the glyph relative to other glyphs on the line, measured in font pixels. + /// This would usually be 0, but a negative value would allow a glyph to extend into the space usually reserved + /// for the previous glyph on the line. pub bitmap_left: f32, + /// The width of the glyph as measured in pixels. This may be more or less than `advance` depending on how much + /// this glyph crowds into the space of the glyphs to the left and right of it on the line. pub bitmap_width: f32, + /// The height of the glyph as measured in pixels. pub bitmap_height: f32, + /// The horizontal distance between the start of this glyph and the start of the next glyph, measured in pixels. pub advance: f32, + /// The position of the texture data of this glyph on the atlas page. + /// Each corner of the quad containing the glyph is given a UV coordinate. pub tex_coords: [Vector2; 4], + /// The index of the atlas page. pub page_index: usize, + /// The position of the gylph measured to sub-pixel precision. + /// It is like `bitmap_top`, `bitmap_left`, `bitmap_width`, and `bitmap_height`, + /// except that those measure the whole pixels that the font needs for rendering while + /// this rect contains the exact outline of the glyph, which is potentially smaller. pub bounds: Rect, } /// Page is a storage for rasterized glyphs. #[derive(Clone)] pub struct Page { + /// The texture data for rendering some glyphs. + /// When new glyphs are required, this data may be modified if space + /// is available. pub pixels: Vec, + /// An embedded texture containing a copy of `pixels`. + /// This is used by the GPU to render the glyph, and it must be updated + /// whenever `pixels` changes. pub texture: Option, + /// A structure that contains the occupied rectangles within the page texture, + /// allowing new glyphs to be added to the texture by finding unoccupied space, + /// until the page is full. pub rect_packer: RectPacker, + /// True if one or more glyphs have been added to the page, but + /// `texture` has not yet been updated by copying the content of `pixels`. pub modified: bool, } @@ -78,148 +126,236 @@ impl Debug for Page { /// store the rasterized glyphs. #[derive(Default, Clone, Debug)] pub struct Atlas { + /// The geometric data used for rendering glyphs from the pages of this atlas, + /// such as the size of each glyph, the index of its page, and the UVs of the corners + /// of its quad. pub glyphs: Vec, + /// A map to lookup the index of the glyph for a particular char in the `glyphs` array. pub char_map: FxHashMap, + /// The list of pages the contain the glyphs of this atlas. Each page is a texture + /// that can be used to render glyphs based on the UV data stored in `glyphs`. pub pages: Vec, } impl Atlas { + fn render_glyph( + &mut self, + font: &'_ fontdue::Font, + unicode: char, + char_index: u16, + height: FontHeight, + page_size: usize, + ) -> Result { + let border = 2; + let (metrics, glyph_raster) = font.rasterize_indexed(char_index, height.0); + + // Find a page, that is capable to fit the new character or create a new + // page and put the character there. + 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)) + }); + + // No space for the character in any of the existing pages, so create a new page. + 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(); + + // Raise a flag to notify users that the content of the page has changed, and + // it should be re-uploaded to GPU (if needed). + 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; + + // Copy glyph pixels to the atlas pixels + 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); + + // Map the new glyph to its unicode position. + 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], ) -> Option<&FontGlyph> { - let border = 2; - match self.char_map.get(&unicode) { Some(glyph_index) => self.glyphs.get(*glyph_index), None => { - // Char might be missing, because it wasn't requested earlier. Try to find + // Char might be missing because it wasn't requested earlier. Try to find // it in the inner font and render/pack it. - - if let Some(char_index) = font.chars().get(&unicode) { - if !height.0.is_finite() || height.0 <= f32::EPSILON { - return None; - } - let (metrics, glyph_raster) = - font.rasterize_indexed(char_index.get(), height.0); - - // Find a page, that is capable to fit the new character or create a new - // page and put the character there. - 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)) - }); - - // No space for the character in any of the existing pages, create a new page. - 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(); - - match page - .rect_packer - .find_free(metrics.width + border, metrics.height + border) - { - Some(bounds) => { - placement_info = Some((page_index, bounds)); - - self.pages.push(page); - } - None => { - // No free space in the given page size (requested glyph is too big). - return None; - } - } - } - - let (page_index, placement_rect) = placement_info?; - let page = &mut self.pages[page_index]; - let glyph_index = self.glyphs.len(); - - // Raise a flag to notify users that the content of the page has changed, and - // it should be re-uploaded to GPU (if needed). - 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; - - // Copy glyph pixels to the atlas pixels - 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); - - // Map the new glyph to its unicode position. - self.char_map.insert(unicode, glyph_index); - - self.glyphs.get(glyph_index) + 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 { - None - } + // Otherwise, search the fallback fonts for a glyph to add to the atlas. + match self.fallback_glyph( + MAX_FALLBACK_DEPTH, + fallbacks, + unicode, + height, + page_size, + ) { + Ok(Some(glyph_index)) => Some(glyph_index), + Ok(None) | Err(FontError::GlyphTooLarge) => { + // We have failed to find the character in the inner font and the fallbacks. + // Every font's default character is supposed to be at index 0, so add that to the atlas + // in the place of the character. + self.render_glyph(font, unicode, 0, height, page_size).ok() + } + Err(FontError::FallbackNotLoaded) => { + // If a fallback is not loaded successfully, do not write anything to the + // atlas and hope that the fallbacks will be ready next time. + None + } + } + }; + glyph_index.and_then(|i| self.glyphs.get(i)) } } } + /// Attempt to render and return the index of the given char using the fallback fonts. + /// Return the index if the glyph was found and rendered using a fallback font. + /// Return None if the glyph was not found in any fallback font. + fn fallback_glyph( + &mut self, + depth: usize, + fonts: &[Option], + unicode: char, + height: FontHeight, + page_size: usize, + ) -> Result, 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) + } } +/// A font resource and the associated data required for rendering glyphs from the font. #[derive(Default, Clone, Debug, Reflect, Visit)] -#[reflect(hide_all)] pub struct Font { + /// The source font data, such as might come from a ttf file. + #[reflect(hidden)] #[visit(skip)] pub inner: Option, + /// The atlases containing textures that allow the GPU to render glyphs from this font, + /// or from its fallbacks. Each font size gets its own atlas. + #[reflect(hidden)] #[visit(skip)] pub atlases: FxHashMap, + /// The size of each atlas page in font pixels. Each page is a square measuring `page_size` x `page_size`. + #[reflect(hidden)] #[visit(skip)] pub page_size: usize, + /// A font representing the bold version of this font. + #[visit(skip)] + pub bold: Option, + /// A font representing the italic version of this font. + #[visit(skip)] + pub italic: Option, + /// Fallback fonts are used for rendering special characters that do not have glyphs in this + /// font. + #[visit(skip)] + pub fallbacks: Vec>, } uuid_provider!(Font = "692fec79-103a-483c-bb0b-9fc3a349cb48"); @@ -242,6 +378,9 @@ impl ResourceData for Font { } } +/// The size the text that a font is supposed to render. +/// Each distinct size gets its own atlas page, and `FontHeight` +/// allows a f32 to be hashed to lookup the page. #[derive(Copy, Clone, Default, Debug)] pub struct FontHeight(pub f32); @@ -267,9 +406,11 @@ impl Hash for FontHeight { } } +/// A resource that allows a font to be loaded. pub type FontResource = Resource; lazy_static! { + /// Fyrox's default build-in font for rendering text when no other font is specified. pub static ref BUILT_IN_FONT: BuiltInResource = BuiltInResource::new( "__BUILT_IN_FONT__", embedded_data_source!("./built_in_font.ttf"), @@ -277,34 +418,129 @@ lazy_static! { FontResource::new_ok( uuid!("77260e8e-f6fa-429c-8009-13dda2673925"), ResourceKind::External, - Font::from_memory(data.to_vec(), 1024).unwrap(), + Font::from_memory(data.to_vec(), 1024, None, None, Vec::default()).unwrap(), ) } ); } +/// Wait for all subfonts of this font to be completely loaded, including recursively searching +/// through the fallback fonts, and the fallbacks of the fallbacks, to ensure that this font is +/// fully ready to be used. In the event that any of the fonts failed to load, or a cycle is found +/// in the fallbacks, then an error is returned. +pub async fn wait_for_subfonts(font: FontResource) -> Result { + 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(); + } + wait_for_fallbacks(font, &mut stack).await +} + +fn write_font_names(fonts: &[FontResource], out: &mut W) -> std::fmt::Result { + fn write_name(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(()) +} + +/// Recursively wait for all fallback fonts of the given font to be loaded, +/// so that this font is fully ready to be rendered, and use the given stack +/// to prevent infinite recursion due to a fallback cycle. An error is returned +/// if a cycle is detected. +async fn wait_for_fallbacks( + font: FontResource, + stack: &mut Vec, +) -> Result { + 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::>(); + for fallback in fallbacks { + Box::pin(wait_for_fallbacks(fallback, stack)).await?; + } + _ = stack.pop(); + Ok(font) +} + impl Font { + /// The name of the font, if available. + pub fn name(&self) -> Option<&str> { + self.inner.as_ref().and_then(|f| f.name()) + } + + /// Create a font from a u8 array of font data such as one might get from a font file. pub fn from_memory( data: impl Deref, page_size: usize, + bold: Option, + italic: Option, + fallbacks: Vec>, ) -> Result { let fontdue_font = fontdue::Font::from_bytes(data, fontdue::FontSettings::default())?; Ok(Font { inner: Some(fontdue_font), atlases: Default::default(), page_size, + bold, + italic, + fallbacks, }) } + /// Asynchronously read font data from the file at the given path to construct a font. pub async fn from_file>( path: P, - page_size: usize, + options: FontImportOptions, io: &dyn ResourceIo, - ) -> Result { + resource_manager: &ResourceManager, + ) -> Result { if let Ok(file_content) = io.load_file(path.as_ref()).await { - Self::from_memory(file_content, page_size) + let page_size = options.page_size; + let mut bold = options.bold; + let mut italic = options.italic; + if let Some(bold) = &mut bold { + resource_manager.request_resource(bold); + } + if let Some(italic) = &mut italic { + resource_manager.request_resource(italic); + } + let mut fallbacks = options.fallbacks; + for font in fallbacks.iter_mut().flatten() { + resource_manager.request_resource(font); + } + Self::from_memory(file_content, page_size, bold, italic, fallbacks) + .map_err(LoadError::new) } else { - Err("Unable to read file") + Err(LoadError::new("Unable to read file")) } } @@ -317,23 +553,24 @@ impl Font { /// in the atlas could be rendered at any page in the atlas. #[inline] pub fn glyph(&mut self, unicode: char, height: f32) -> Option<&FontGlyph> { - self.atlases - .entry(FontHeight(height)) - .or_insert_with(|| Atlas { - glyphs: Default::default(), - char_map: Default::default(), - pages: Default::default(), - }) - .glyph( - self.inner - .as_ref() - .expect("Font reader must be initialized!"), - unicode, - FontHeight(height), - self.page_size, - ) + 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, + ) } + /// The highest point of any glyph of this font above the baseline, usually positive. #[inline] pub fn ascender(&self, height: f32) -> f32 { self.inner @@ -344,6 +581,7 @@ impl Font { .unwrap_or_default() } + /// The lowest point of any glyph of this font below the baseline, usually negative. #[inline] pub fn descender(&self, height: f32) -> f32 { self.inner @@ -354,6 +592,10 @@ impl Font { .unwrap_or_default() } + /// The horizontal scaled kerning value for the given two characters, if available, + /// scaled to the given text height. The kerning value is usually negative, and it is added + /// to the advance of the left glyph to bring the right glyph closer when appropriate for some + /// pairs of glyphs. #[inline] pub fn horizontal_kerning(&self, height: f32, left: char, right: char) -> Option { self.inner @@ -362,11 +604,14 @@ impl Font { .horizontal_kern(left, right, height) } + /// The size of each atlas page in font pixels. Each page is a square measuring `page_size` x `page_size`. #[inline] pub fn page_size(&self) -> usize { self.page_size } + /// The horizontal distance between the start of the glyph for the given character and the start of the next + /// glyph, when rendered for the given text height. #[inline] pub fn glyph_advance(&mut self, unicode: char, height: f32) -> f32 { self.glyph(unicode, height) @@ -376,13 +621,64 @@ impl Font { /// Font builder allows you to load fonts in declarative manner. pub struct FontBuilder { + /// The size of each atlas page in font pixels. Each page is a square measuring `page_size` x `page_size`. page_size: usize, + bold: Option, + italic: Option, + fallbacks: Vec>, } impl FontBuilder { /// Creates a default FontBuilder. pub fn new() -> Self { - Self { page_size: 1024 } + Self { + page_size: 1024, + bold: None, + italic: None, + fallbacks: Vec::default(), + } + } + + /// The size of each atlas page in font pixels. Each page is a square measuring `page_size` x `page_size`. + pub fn with_page_size(mut self, size: usize) -> Self { + self.page_size = size; + self + } + + /// The bold version of this font. + pub fn with_bold(mut self, font: FontResource) -> Self { + self.bold = Some(font); + self + } + + /// The italic version of this font. + pub fn with_italic(mut self, font: FontResource) -> Self { + self.italic = Some(font); + self + } + + /// A fallback font to supply glyphs for special characters that are not represented in + /// this font. + pub fn with_fallback(mut self, font: FontResource) -> Self { + self.fallbacks.push(Some(font)); + self + } + + /// A list of fallback fonts to supply glyphs for special characters that are not represented + /// in this font, replacing any existing fallbacks for this font. + pub fn with_fallbacks(mut self, fallbacks: Vec>) -> Self { + self.fallbacks = fallbacks; + self + } + + /// Build the options object for this font. + fn into_options(self) -> FontImportOptions { + FontImportOptions { + page_size: self.page_size, + bold: self.bold, + italic: self.italic, + fallbacks: self.fallbacks, + } } /// Creates a new font from the data at the specified path. @@ -390,12 +686,20 @@ impl FontBuilder { self, path: impl AsRef, io: &dyn ResourceIo, - ) -> Result { - Font::from_file(path, self.page_size, io).await + resource_manager: &ResourceManager, + ) -> Result { + Font::from_file(path, self.into_options(), io, resource_manager).await } /// Creates a new font from bytes in memory. - pub fn build_from_memory(self, data: impl Deref) -> Result { - Font::from_memory(data, self.page_size) + pub fn build_from_memory( + mut self, + data: impl Deref, + resource_manager: &ResourceManager, + ) -> Result { + for font in self.fallbacks.iter_mut().flatten() { + resource_manager.request_resource(font); + } + Font::from_memory(data, self.page_size, self.bold, self.italic, self.fallbacks) } } diff --git a/project-manager/src/main.rs b/project-manager/src/main.rs index 942bf50ae..a0b872c9a 100644 --- a/project-manager/src/main.rs +++ b/project-manager/src/main.rs @@ -103,6 +103,9 @@ fn main() { Font::from_memory( include_bytes!("../resources/Roboto-Regular.ttf").to_vec(), 1024, + None, + None, + Vec::default(), ) .unwrap(), );