From 9fb125eeb9b716c42d820c1ee0fd0785fa68b952 Mon Sep 17 00:00:00 2001 From: TimJentzsch Date: Fri, 26 Jul 2024 13:51:35 +0200 Subject: [PATCH] Improved font handling (#6) * Add system for conveniently loading fonts * Use the font plugin to only specify a font family, not each font individually * Simplify querying for a font handle * Improve structure of font module and exports * Improve syntax for configuring font folder and make it optional * Example setup doesn't need the AssetServer anymore --- Cargo.lock | 61 ++++++++++++++++++++++++++- Cargo.toml | 3 ++ assets/CREDITS.md | 5 +++ assets/{fonts => }/OFL.txt | 0 examples/dynamic.rs | 9 ++-- examples/static.rs | 11 ++--- src/bevy/bbcode.rs | 29 ++----------- src/bevy/conversion.rs | 37 +++++++++------- src/bevy/font/mod.rs | 5 +++ src/bevy/font/plugin.rs | 30 +++++++++++++ src/bevy/font/registry.rs | 86 ++++++++++++++++++++++++++++++++++++++ src/bevy/mod.rs | 5 +++ src/bevy/plugin.rs | 50 ++++++++++++++++++++-- src/lib.rs | 3 +- 14 files changed, 274 insertions(+), 60 deletions(-) create mode 100644 assets/CREDITS.md rename assets/{fonts => }/OFL.txt (100%) create mode 100644 src/bevy/font/mod.rs create mode 100644 src/bevy/font/plugin.rs create mode 100644 src/bevy/font/registry.rs diff --git a/Cargo.lock b/Cargo.lock index 1647118..4ac0def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,8 +754,11 @@ dependencies = [ name = "bevy_mod_bbcode" version = "0.1.0" dependencies = [ + "ab_glyph", "bevy", + "fontdb", "nom", + "tinyvec", ] [[package]] @@ -1466,6 +1469,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +dependencies = [ + "libm", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1741,6 +1753,29 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontconfig-parser" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f874d3f04ab8bf6f591358b03e4f1e084e396c4263a1c29ebbaa0feff7ba99" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1847,7 +1882,7 @@ dependencies = [ "vec_map", "wasm-bindgen", "web-sys", - "windows 0.52.0", + "windows 0.54.0", ] [[package]] @@ -2259,6 +2294,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.0.2" @@ -2341,6 +2382,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + [[package]] name = "metal" version = "0.28.0" @@ -3131,6 +3181,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3501,6 +3557,9 @@ name = "ttf-parser" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" +dependencies = [ + "core_maths", +] [[package]] name = "twox-hash" diff --git a/Cargo.toml b/Cargo.toml index cb4ea1d..7880341 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,10 @@ categories = ["game-development"] exclude = ["assets/**/*", ".github/**/*"] [dependencies] +ab_glyph = "0.2.28" +fontdb = "0.20.0" nom = "7.1.3" +tinyvec = "1.8.0" [dependencies.bevy] version = "0.14" diff --git a/assets/CREDITS.md b/assets/CREDITS.md new file mode 100644 index 0000000..fad6cc2 --- /dev/null +++ b/assets/CREDITS.md @@ -0,0 +1,5 @@ +# Credits + +| Path | License | +| ------- | --------- | +| `fonts` | `OFL.txt` | diff --git a/assets/fonts/OFL.txt b/assets/OFL.txt similarity index 100% rename from assets/fonts/OFL.txt rename to assets/OFL.txt diff --git a/examples/dynamic.rs b/examples/dynamic.rs index 2e384c4..492496f 100644 --- a/examples/dynamic.rs +++ b/examples/dynamic.rs @@ -10,21 +10,18 @@ struct TimeMarker; fn main() { App::new() - .add_plugins((DefaultPlugins, BbcodePlugin)) + .add_plugins((DefaultPlugins, BbcodePlugin::new().with_fonts("fonts"))) .add_systems(Startup, setup) .add_systems(Update, update) .run(); } -fn setup(mut commands: Commands, asset_server: Res) { +fn setup(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); commands.spawn(BbcodeBundle::from_content( "Time passed: [m=time]0.0[/m] s", - BbcodeSettings::new(40., Color::WHITE) - .with_regular_font(asset_server.load("fonts/FiraSans-Regular.ttf")) - .with_bold_font(asset_server.load("fonts/FiraSans-Bold.ttf")) - .with_italic_font(asset_server.load("fonts/FiraSans-Italic.ttf")) + BbcodeSettings::new("Fira Sans", 40., Color::WHITE) // Register the marker component for the `m=time` tag .with_marker("time", TimeMarker), )); diff --git a/examples/static.rs b/examples/static.rs index 0f44822..213cb23 100644 --- a/examples/static.rs +++ b/examples/static.rs @@ -3,19 +3,16 @@ use bevy_mod_bbcode::{BbcodeBundle, BbcodePlugin, BbcodeSettings}; fn main() { App::new() - .add_plugins((DefaultPlugins, BbcodePlugin)) + .add_plugins((DefaultPlugins, BbcodePlugin::new().with_fonts("fonts"))) .add_systems(Startup, setup) .run(); } -fn setup(mut commands: Commands, asset_server: Res) { +fn setup(mut commands: Commands) { commands.spawn(Camera2dBundle::default()); commands.spawn(BbcodeBundle::from_content( - "test [b]bold[/b] with [i]italic[/i] and [c=#ff00ff]color[/c]", - BbcodeSettings::new(40., Color::WHITE) - .with_regular_font(asset_server.load("fonts/FiraSans-Regular.ttf")) - .with_bold_font(asset_server.load("fonts/FiraSans-Bold.ttf")) - .with_italic_font(asset_server.load("fonts/FiraSans-Italic.ttf")), + "test [b]bold with [i]italic[/i][/b] and [c=#ff00ff]color[/c]", + BbcodeSettings::new("Fira Sans", 40., Color::WHITE), )); } diff --git a/src/bevy/bbcode.rs b/src/bevy/bbcode.rs index 7a0e574..27b6f26 100644 --- a/src/bevy/bbcode.rs +++ b/src/bevy/bbcode.rs @@ -18,46 +18,23 @@ pub(crate) struct Modifiers { #[derive(Clone, Component)] pub struct BbcodeSettings { + pub font_family: String, pub font_size: f32, pub color: Color, - pub(crate) regular_font: Option>, - pub(crate) bold_font: Option>, - pub(crate) italic_font: Option>, - pub(crate) modifiers: Modifiers, } impl BbcodeSettings { - pub fn new(font_size: f32, color: Color) -> Self { + pub fn new>(font_family: F, font_size: f32, color: Color) -> Self { Self { + font_family: font_family.into(), font_size, color, - regular_font: None, - bold_font: None, - italic_font: None, modifiers: Default::default(), } } - /// Add a font to use for regular text. - pub fn with_regular_font(mut self, handle: Handle) -> Self { - self.regular_font = Some(handle); - self - } - - /// Add a font to use for bold text. - pub fn with_bold_font(mut self, handle: Handle) -> Self { - self.bold_font = Some(handle); - self - } - - /// Add a font to use for italic text. - pub fn with_italic_font(mut self, handle: Handle) -> Self { - self.italic_font = Some(handle); - self - } - /// Register a marker component for the `[m]` tag. pub fn with_marker, M: Component + Clone>( mut self, diff --git a/src/bevy/conversion.rs b/src/bevy/conversion.rs index 430f223..4741ae8 100644 --- a/src/bevy/conversion.rs +++ b/src/bevy/conversion.rs @@ -4,7 +4,10 @@ use bevy::{ecs::system::EntityCommands, prelude::*}; use crate::bbcode::{parser::parse_bbcode, BbcodeNode, BbcodeTag}; -use super::bbcode::{Bbcode, BbcodeSettings}; +use super::{ + bbcode::{Bbcode, BbcodeSettings}, + font::FontRegistry, +}; #[derive(Debug, Clone)] struct BbcodeContext { @@ -69,9 +72,10 @@ impl BbcodeContext { pub fn convert_bbcode( mut commands: Commands, bbcode_query: Query<(Entity, Ref, Ref)>, + font_registry: Res, ) { for (entity, bbcode, settings) in bbcode_query.iter() { - if !bbcode.is_changed() && !settings.is_changed() { + if !bbcode.is_changed() && !settings.is_changed() && !font_registry.is_changed() { continue; } @@ -100,6 +104,7 @@ pub fn convert_bbcode( }, &settings, &nodes, + font_registry.as_ref(), ) } } @@ -109,23 +114,26 @@ fn construct_recursively( context: BbcodeContext, settings: &BbcodeSettings, nodes: &Vec>, + font_registry: &FontRegistry, ) { - let default_font = settings.regular_font.clone().unwrap_or_default(); - for node in nodes { match **node { BbcodeNode::Text(ref text) => { - let font = match (context.is_bold, context.is_italic) { - (true, _) => default_font.clone(), - (_, true) => settings - .italic_font - .clone() - .unwrap_or_else(|| default_font.clone()), - (false, false) => settings - .regular_font - .clone() - .unwrap_or_else(|| default_font.clone()), + let font_query = fontdb::Query { + families: &[fontdb::Family::Name(&settings.font_family)], + weight: if context.is_bold { + fontdb::Weight::BOLD + } else { + fontdb::Weight::NORMAL + }, + stretch: fontdb::Stretch::Normal, + style: if context.is_italic { + fontdb::Style::Italic + } else { + fontdb::Style::Normal + }, }; + let font = font_registry.query_handle(&font_query).unwrap_or_default(); entity_commands.with_children(|builder| { let mut text_commands = builder.spawn(TextBundle::from_section( @@ -151,6 +159,7 @@ fn construct_recursively( context.apply_tag(tag), settings, tag.children(), + font_registry, ), } } diff --git a/src/bevy/font/mod.rs b/src/bevy/font/mod.rs new file mode 100644 index 0000000..6c120f7 --- /dev/null +++ b/src/bevy/font/mod.rs @@ -0,0 +1,5 @@ +mod plugin; +mod registry; + +pub use plugin::FontPlugin; +pub use registry::FontRegistry; diff --git a/src/bevy/font/plugin.rs b/src/bevy/font/plugin.rs new file mode 100644 index 0000000..2a53920 --- /dev/null +++ b/src/bevy/font/plugin.rs @@ -0,0 +1,30 @@ +use bevy::prelude::*; + +use super::registry::FontRegistry; + +#[derive(Debug)] +pub struct FontPlugin; + +impl Plugin for FontPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, update_font_registry); + } +} + +/// Track when fonts are loaded, modified or removed and update the font registry accordingly. +fn update_font_registry( + mut font_registry: ResMut, + mut font_changes: EventReader>, + font_assets: Res>, +) { + for change in font_changes.read() { + match *change { + AssetEvent::Added { id } => font_registry.add(id, font_assets.as_ref()), + AssetEvent::Modified { id } => font_registry.update(id, font_assets.as_ref()), + AssetEvent::Removed { id } => font_registry.remove(id), + AssetEvent::Unused { id: _ } => {} + AssetEvent::LoadedWithDependencies { id: _ } => {} + } + } +} diff --git a/src/bevy/font/registry.rs b/src/bevy/font/registry.rs new file mode 100644 index 0000000..1153853 --- /dev/null +++ b/src/bevy/font/registry.rs @@ -0,0 +1,86 @@ +use std::{ops::Deref, sync::Arc}; + +use ab_glyph::Font as _; +use bevy::{prelude::*, utils::HashMap}; +use tinyvec::TinyVec; + +#[derive(Debug, Default, Resource)] +pub struct FontRegistry { + /// A mapping from asset IDs to font IDs. + /// + /// Needed to properly clean up removed fonts. + asset_to_font_id: HashMap, TinyVec<[fontdb::ID; 8]>>, + + /// A mapping from font IDs to asset IDs. + /// + /// Needed to determine which handle to use for a font query. + font_to_asset_id: HashMap>, + + /// The internal database used to query fonts. + font_db: fontdb::Database, +} + +impl FontRegistry { + /// Add the font associated with the given asset ID. + pub fn add(&mut self, asset_id: AssetId, font_assets: impl Deref>) { + let Some(font) = font_assets.get(asset_id) else { + return; + }; + + let data = font.font.font_data().to_vec(); + + // Insert the font into the DB + let font_ids = self + .font_db + .load_font_source(fontdb::Source::Binary(Arc::new(data))); + + // Update the ID maps + for font_id in &font_ids { + self.font_to_asset_id.insert(*font_id, asset_id); + } + self.asset_to_font_id.insert(asset_id, font_ids); + } + + /// Remove the font associated with the given asset ID. + pub fn remove(&mut self, asset_id: AssetId) { + let Some(font_ids) = self.asset_to_font_id.get(&asset_id) else { + return; + }; + + // Remove the font from the DB + for font_id in font_ids { + self.font_db.remove_face(*font_id); + } + + // Update the ID maps + for font_id in font_ids { + self.font_to_asset_id.remove(font_id); + } + self.asset_to_font_id.remove(&asset_id); + } + + /// Update the font associated with the given asset ID. + pub fn update( + &mut self, + asset_id: AssetId, + font_assets: impl Deref>, + ) { + self.remove(asset_id); + self.add(asset_id, font_assets); + } + + /// Find the best matching font asset for the query and return its [`AssetId`]. + pub fn query_id(&self, query: &fontdb::Query) -> Option> { + let font_id = self.font_db.query(query); + font_id + .and_then(|font_id| self.font_to_asset_id.get(&font_id)) + .copied() + } + + /// Find the best matching font asset for the query and return its [`Handle`]. + /// + /// Note that this returns a *weak* handle to the font. + pub fn query_handle(&self, query: &fontdb::Query) -> Option> { + self.query_id(query).map(Handle::Weak) + } +} diff --git a/src/bevy/mod.rs b/src/bevy/mod.rs index a88f475..99fcfbc 100644 --- a/src/bevy/mod.rs +++ b/src/bevy/mod.rs @@ -1,3 +1,8 @@ pub(crate) mod bbcode; pub(crate) mod conversion; +pub(crate) mod font; pub(crate) mod plugin; + +pub use bbcode::{Bbcode, BbcodeBundle, BbcodeSettings}; +pub use font::*; +pub use plugin::BbcodePlugin; diff --git a/src/bevy/plugin.rs b/src/bevy/plugin.rs index 2f80156..e7da9f1 100644 --- a/src/bevy/plugin.rs +++ b/src/bevy/plugin.rs @@ -1,11 +1,53 @@ -use bevy::prelude::*; +use bevy::{ + asset::{AssetPath, LoadedFolder}, + prelude::*, +}; -use super::conversion::convert_bbcode; +use super::{conversion::convert_bbcode, font::FontPlugin}; -pub struct BbcodePlugin; +#[derive(Debug, Default)] +pub struct BbcodePlugin { + /// The path to a folder containing the fonts to use. + font_folder_path: Option>, +} + +impl BbcodePlugin { + /// Create a new BBCode plugin. + /// + /// You probably also want to load fonts to make the text formatting work, see [`BbcodePlugin::with_fonts`]. + pub fn new() -> Self { + Self { + font_folder_path: None, + } + } + + /// Load the fonts in the given directory. + /// The directory should only contain font files and nothing else. + pub fn with_fonts>>(mut self, folder_path: P) -> Self { + self.font_folder_path = Some(folder_path.into()); + self + } +} impl Plugin for BbcodePlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, convert_bbcode); + app.add_plugins(FontPlugin) + .add_systems(Update, convert_bbcode); + + let asset_server = app.world().resource::(); + + if let Some(folder_path) = &self.font_folder_path { + // Load all fonts in the provided folder + // `FontPlugin` will react to the asset events + let handle = asset_server.load_folder(folder_path.clone()); + // We still need to store the handle to keep the fonts loaded + app.insert_resource(FontFolder { _handle: handle }); + } } } + +#[derive(Debug, Resource)] +struct FontFolder { + /// Keep the assets loaded by storing the strong handle + _handle: Handle, +} diff --git a/src/lib.rs b/src/lib.rs index 52dc610..d555ab9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub(crate) mod bbcode; pub(crate) mod bevy; -pub use bevy::bbcode::{Bbcode, BbcodeBundle, BbcodeSettings}; -pub use bevy::plugin::BbcodePlugin; +pub use bevy::*;