From b35d7bb0eaad83ba5691d89d06e6383737b29630 Mon Sep 17 00:00:00 2001 From: dsgallups Date: Sun, 5 Oct 2025 06:09:33 -0400 Subject: [PATCH 1/5] add ability to load system fonts via fontdb --- crates/bevy_text/src/lib.rs | 15 ++++++++++++--- crates/bevy_text/src/pipeline.rs | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 1e341880e5336..bcb83daac04b1 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -75,8 +75,17 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); /// /// When the `bevy_text` feature is enabled with the `bevy` crate, this /// plugin is included by default in the `DefaultPlugins`. -#[derive(Default)] -pub struct TextPlugin; +pub struct TextPlugin { + /// If true, the [`CosmicFontSystem`] will load system fonts. + pub load_system_fonts: bool, +} +impl Default for TextPlugin { + fn default() -> Self { + Self { + load_system_fonts: true, + } + } +} /// System set in [`PostUpdate`] where all 2d text update systems are executed. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] @@ -92,7 +101,7 @@ impl Plugin for TextPlugin { .init_asset_loader::() .init_resource::() .init_resource::() - .init_resource::() + .insert_resource(CosmicFontSystem::new(self.load_system_fonts)) .init_resource::() .init_resource::() .add_systems( diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 03939d47d16f0..7b60479d8095b 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -30,9 +30,20 @@ pub struct CosmicFontSystem(pub cosmic_text::FontSystem); impl Default for CosmicFontSystem { fn default() -> Self { + Self::new(true) + } +} + +impl CosmicFontSystem { + /// Creates a new, wrapped [`cosmic_text::FontSystem`]. + /// + /// The option to load system fonts is typically provided via [`TextPlugin`](super::TextPlugin). + pub fn new(load_system_fonts: bool) -> Self { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); - let db = cosmic_text::fontdb::Database::new(); - // TODO: consider using `cosmic_text::FontSystem::new()` (load system fonts by default) + let mut db = cosmic_text::fontdb::Database::new(); + if load_system_fonts { + db.load_system_fonts(); + } Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) } } From 653d06151c33c2ccc6e6c14b71ed3467a9d194d0 Mon Sep 17 00:00:00 2001 From: dsgallups Date: Sun, 5 Oct 2025 06:40:43 -0400 Subject: [PATCH 2/5] feat: working system fonts --- crates/bevy_text/src/font.rs | 75 +++++++++++++++++++++++++++++--- crates/bevy_text/src/lib.rs | 5 ++- crates/bevy_text/src/pipeline.rs | 65 ++++++++++++++++++++------- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index b748f4a111fdd..7b8f2bbc19d87 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -16,9 +16,76 @@ use bevy_reflect::TypePath; /// /// Bevy currently loads a single font face as a single `Font` asset. #[derive(Debug, TypePath, Clone, Asset)] -pub struct Font { +pub enum Font { /// Content of a font file as bytes - pub data: Arc>, + Data(Arc>), + /// References a font installed on the system by family, weight, stretch, and style. + System { + /// A list of font families that satisfy this font requirement + families: Vec, + /// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness. + /// + /// See [`cosmic_text::Weight`] for details. + weight: cosmic_text::Weight, + /// A face [width](https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass). + /// + /// See [`cosmic_text::Stretch`] for details. + stretch: cosmic_text::Stretch, + /// Allows italic or oblique faces to be selected. + /// + /// See [`cosmic_text::Style`] for details. + style: cosmic_text::Style, + }, +} + +/// A font family specifier, either by name or generic category. +/// +/// See [`cosmic_text::Family`] for details. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Family { + /// The name of a font family of choice. + /// + /// This must be a *Typographic Family* (ID 16) or a *Family Name* (ID 1) in terms of TrueType. + /// Meaning you have to pass a family without any additional suffixes like _Bold_, _Italic_, + /// _Regular_, etc. + /// + /// Localized names are allowed. + Name(String), + + /// Serif fonts represent the formal text style for a script. + Serif, + + /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low contrast + /// and have stroke endings that are plain — without any flaring, cross stroke, + /// or other ornamentation. + SansSerif, + + /// Glyphs in cursive fonts generally use a more informal script style, + /// and the result looks more like handwritten pen or brush writing than printed letterwork. + Cursive, + + /// Fantasy fonts are primarily decorative or expressive fonts that + /// contain decorative or expressive representations of characters. + Fantasy, + + /// The sole criterion of a monospace font is that all glyphs have the same fixed width. + MonoSpace, +} + +impl Family { + /// References variants to create a [`cosmic_text::Family`]. + /// + /// This is required for querying the underlying [`cosmic_text::fontdb::Database`] + pub fn as_fontdb_family(&self) -> cosmic_text::Family<'_> { + match self { + Family::Name(name) => cosmic_text::Family::Name(name), + Family::Serif => cosmic_text::Family::Serif, + Family::SansSerif => cosmic_text::Family::SansSerif, + Family::Cursive => cosmic_text::Family::Cursive, + Family::Fantasy => cosmic_text::Family::Fantasy, + Family::MonoSpace => cosmic_text::Family::Monospace, + } + } } impl Font { @@ -28,8 +95,6 @@ impl Font { ) -> Result { use cosmic_text::ttf_parser; ttf_parser::Face::parse(&font_data, 0)?; - Ok(Self { - data: Arc::new(font_data), - }) + Ok(Self::Data(Arc::new(font_data))) } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index bcb83daac04b1..89f7e343e6715 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -53,13 +53,16 @@ pub use pipeline::*; pub use text::*; pub use text_access::*; +pub use cosmic_text::{Stretch, Style, Weight}; + /// The text prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, Justify, LineBreak, TextColor, TextError, TextFont, TextLayout, TextSpan, + Family, Font, Justify, LineBreak, Stretch, Style, TextColor, TextError, TextFont, + TextLayout, TextSpan, Weight, }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7b60479d8095b..b1fc626182e0b 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -13,7 +13,7 @@ use bevy_math::{Rect, UVec2, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; +use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, @@ -44,6 +44,7 @@ impl CosmicFontSystem { if load_system_fonts { db.load_system_fonts(); } + Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) } } @@ -151,7 +152,8 @@ impl TextPipeline { font_system, &mut self.map_handle_to_font_id, fonts, - ); + ) + .ok_or(TextError::NoSuchFont)?; // Save spans that aren't zero-sized. if scale_factor <= 0.0 || text_font.font_size <= 0.0 { @@ -507,34 +509,65 @@ pub fn load_font_to_fontdb( font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, fonts: &Assets, -) -> FontFaceInfo { +) -> Option { let font_handle = text_font.font.clone(); - let (face_id, family_name) = map_handle_to_font_id - .entry(font_handle.id()) - .or_insert_with(|| { + + let (face_id, family_name) = match map_handle_to_font_id.get_mut(&font_handle.id()) { + Some((face_id, family_name)) => (face_id, family_name), + None => { let font = fonts.get(font_handle.id()).expect( "Tried getting a font that was not available, probably due to not being loaded yet", ); - let data = Arc::clone(&font.data); - let ids = font_system - .db_mut() - .load_font_source(cosmic_text::fontdb::Source::Binary(data)); - // TODO: it is assumed this is the right font face - let face_id = *ids.last().unwrap(); + let face_id = match font { + Font::Data(data) => { + let data = Arc::clone(data); + + let ids = font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + + // TODO: it is assumed this is the right font face + *ids.last().unwrap() + } + Font::System { + families, + weight, + stretch, + style, + } => { + let families = families + .iter() + .map(|family| family.as_fontdb_family()) + .collect::>(); + let query = cosmic_text::fontdb::Query { + families: &families, + weight: *weight, + stretch: *stretch, + style: *style, + }; + + font_system.db().query(&query)? + } + }; + let face = font_system.db().face(face_id).unwrap(); let family_name = Arc::from(face.families[0].0.as_str()); + map_handle_to_font_id.insert(font_handle.id(), (face_id, family_name)); + let (face_id, family_name) = map_handle_to_font_id.get_mut(&font_handle.id()).unwrap(); (face_id, family_name) - }); + } + }; + let face = font_system.db().face(*face_id).unwrap(); - FontFaceInfo { + Some(FontFaceInfo { stretch: face.stretch, style: face.style, weight: face.weight, family_name: family_name.clone(), - } + }) } /// Translates [`TextFont`] to [`Attrs`]. @@ -547,7 +580,7 @@ fn get_attrs<'a>( ) -> Attrs<'a> { Attrs::new() .metadata(span_index) - .family(Family::Name(&face_info.family_name)) + .family(cosmic_text::Family::Name(&face_info.family_name)) .stretch(face_info.stretch) .style(face_info.style) .weight(face_info.weight) From 616d0e0891db127114cb8a1498344bbeacaeacde Mon Sep 17 00:00:00 2001 From: dsgallups Date: Sun, 5 Oct 2025 07:11:23 -0400 Subject: [PATCH 3/5] feat: example --- Cargo.toml | 13 +++++++++++ examples/2d/system_fonts.rs | 44 +++++++++++++++++++++++++++++++++++++ examples/README.md | 1 + 3 files changed, 58 insertions(+) create mode 100644 examples/2d/system_fonts.rs diff --git a/Cargo.toml b/Cargo.toml index 72c321d5717bd..12f03c0272693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -862,6 +862,19 @@ description = "Renders text to multiple windows with different scale factors usi category = "2D Rendering" wasm = true + +[[example]] +name = "system_fonts" +path = "examples/2d/system_fonts.rs" +doc-scrape-examples = true + +[package.metadata.example.system_fonts] +name = "System Fonts" +description = "Uses a system font to display text" +category = "2D Rendering" +# Loading asset folders is not supported in Wasm, but required to create the atlas. +wasm = false + [[example]] name = "texture_atlas" path = "examples/2d/texture_atlas.rs" diff --git a/examples/2d/system_fonts.rs b/examples/2d/system_fonts.rs new file mode 100644 index 0000000000000..9c2bcceacd7b4 --- /dev/null +++ b/examples/2d/system_fonts.rs @@ -0,0 +1,44 @@ +//! Uses a system font to display text +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, mut fonts: ResMut>) { + commands.spawn(Camera2d); + + let font = fonts.add(Font::System { + families: vec![ + Family::Name("Liberation Sans".to_string()), + Family::Name("Ubuntu".to_string()), + Family::Name("Noto Sans".to_string()), + ], + weight: Weight::NORMAL, + stretch: Stretch::Normal, + style: Style::Normal, + }); + + commands.spawn(( + Node { + width: percent(100.), + height: percent(100.), + display: Display::Flex, + padding: UiRect::all(px(20.)), + justify_content: JustifyContent::Center, + ..default() + }, + children![( + Text::new("System Font UI Text"), + TextFont::default().with_font(font.clone()), + )], + )); + + commands.spawn(( + Text2d::new("System Font 2D Text"), + TextFont::default().with_font(font), + )); +} diff --git a/examples/README.md b/examples/README.md index 5ad5ef2fa69c6..82fe93cd78993 100644 --- a/examples/README.md +++ b/examples/README.md @@ -128,6 +128,7 @@ Example | Description [Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite [Sprite Slice](../examples/2d/sprite_slice.rs) | Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique [Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid +[System Fonts](../examples/2d/system_fonts.rs) | Uses a system font to display text [Text 2D](../examples/2d/text2d.rs) | Generates text in 2D [Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites [Tilemap Chunk](../examples/2d/tilemap_chunk.rs) | Renders a tilemap chunk From 24782f109009b9fb2f7515f94c9f541bee043846 Mon Sep 17 00:00:00 2001 From: dsgallups Date: Sun, 5 Oct 2025 07:29:46 -0400 Subject: [PATCH 4/5] fix: update documentation, add some customization TextPlugin now includes general family face overrides --- crates/bevy_text/src/font.rs | 8 +++++--- crates/bevy_text/src/lib.rs | 24 ++++++++++++++++++++++-- crates/bevy_text/src/pipeline.rs | 31 +++++++++++++++++++++++++------ examples/2d/system_fonts.rs | 2 +- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 7b8f2bbc19d87..99b9186e16e5a 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -19,9 +19,11 @@ use bevy_reflect::TypePath; pub enum Font { /// Content of a font file as bytes Data(Arc>), - /// References a font installed on the system by family, weight, stretch, and style. - System { - /// A list of font families that satisfy this font requirement + /// References a font inserted into the font database by family, weight, stretch, and style. + /// + /// This can include system fonts, if enabled in [`super::TextPlugin`], or previously loaded fonts via [`Font::Data`]. + Query { + /// A list of font families that satisfy this font requirement. families: Vec, /// Specifies the weight of glyphs in the font, their degree of blackness or stroke thickness. /// diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 89f7e343e6715..f7a3366721179 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -79,13 +79,33 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); /// When the `bevy_text` feature is enabled with the `bevy` crate, this /// plugin is included by default in the `DefaultPlugins`. pub struct TextPlugin { - /// If true, the [`CosmicFontSystem`] will load system fonts. + /// If `true`, the [`CosmicFontSystem`] will load system fonts. + /// + /// Supports Windows, Linux, and MacOS. + /// + /// See [`cosmic_text::fontdb::Database::load_system_fonts`] for details. pub load_system_fonts: bool, + + /// Override the family identifier for the system general Serif font + pub family_serif: Option, + /// Override the default identifier for the general Sans-Serif font + pub family_sans_serif: Option, + /// Override the default identifier for the general Cursive font + pub family_cursive: Option, + /// Override the default identifier for the general Fantasy font + pub family_fantasy: Option, + /// Override the default identifier for the general Monospace font + pub family_monospace: Option, } impl Default for TextPlugin { fn default() -> Self { Self { load_system_fonts: true, + family_serif: None, + family_sans_serif: None, + family_cursive: None, + family_fantasy: None, + family_monospace: None, } } } @@ -104,7 +124,7 @@ impl Plugin for TextPlugin { .init_asset_loader::() .init_resource::() .init_resource::() - .insert_resource(CosmicFontSystem::new(self.load_system_fonts)) + .insert_resource(CosmicFontSystem::new_with_settings(self)) .init_resource::() .init_resource::() .add_systems( diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index b1fc626182e0b..49d64656e95ce 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -17,7 +17,7 @@ use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap}; use crate::{ error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, - PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, TextPlugin, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -30,20 +30,39 @@ pub struct CosmicFontSystem(pub cosmic_text::FontSystem); impl Default for CosmicFontSystem { fn default() -> Self { - Self::new(true) + Self::new_with_settings(&TextPlugin::default()) } } impl CosmicFontSystem { /// Creates a new, wrapped [`cosmic_text::FontSystem`]. /// - /// The option to load system fonts is typically provided via [`TextPlugin`](super::TextPlugin). - pub fn new(load_system_fonts: bool) -> Self { + /// The option to load system fonts is typically provided via the values in [`TextPlugin`]. + pub fn new_with_settings(plugin_settings: &TextPlugin) -> Self { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); let mut db = cosmic_text::fontdb::Database::new(); - if load_system_fonts { + if plugin_settings.load_system_fonts { db.load_system_fonts(); } + if let Some(family_serif) = &plugin_settings.family_serif { + db.set_serif_family(family_serif.to_string()); + } + + if let Some(family_sans_serif) = &plugin_settings.family_sans_serif { + db.set_serif_family(family_sans_serif.to_string()); + } + + if let Some(family_cursive) = &plugin_settings.family_cursive { + db.set_serif_family(family_cursive.to_string()); + } + + if let Some(family_fantasy) = &plugin_settings.family_fantasy { + db.set_serif_family(family_fantasy.to_string()); + } + + if let Some(family_monospace) = &plugin_settings.family_monospace { + db.set_serif_family(family_monospace.to_string()); + } Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db)) } @@ -530,7 +549,7 @@ pub fn load_font_to_fontdb( // TODO: it is assumed this is the right font face *ids.last().unwrap() } - Font::System { + Font::Query { families, weight, stretch, diff --git a/examples/2d/system_fonts.rs b/examples/2d/system_fonts.rs index 9c2bcceacd7b4..da6bb6942fe23 100644 --- a/examples/2d/system_fonts.rs +++ b/examples/2d/system_fonts.rs @@ -11,7 +11,7 @@ fn main() { fn setup(mut commands: Commands, mut fonts: ResMut>) { commands.spawn(Camera2d); - let font = fonts.add(Font::System { + let font = fonts.add(Font::Query { families: vec![ Family::Name("Liberation Sans".to_string()), Family::Name("Ubuntu".to_string()), From 9a134a4ff16c903166eb3acb7f08aba0f13cccad Mon Sep 17 00:00:00 2001 From: dsgallups Date: Sun, 5 Oct 2025 07:59:25 -0400 Subject: [PATCH 5/5] update example to reference loaded font --- crates/bevy_text/src/lib.rs | 2 +- examples/2d/system_fonts.rs | 46 +++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index f7a3366721179..445439bdbaed2 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -81,7 +81,7 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); pub struct TextPlugin { /// If `true`, the [`CosmicFontSystem`] will load system fonts. /// - /// Supports Windows, Linux, and MacOS. + /// Supports Windows, Linux, and macOS. /// /// See [`cosmic_text::fontdb::Database::load_system_fonts`] for details. pub load_system_fonts: bool, diff --git a/examples/2d/system_fonts.rs b/examples/2d/system_fonts.rs index da6bb6942fe23..36a3a8046cf2a 100644 --- a/examples/2d/system_fonts.rs +++ b/examples/2d/system_fonts.rs @@ -1,17 +1,23 @@ //! Uses a system font to display text -use bevy::prelude::*; +use std::time::Duration; + +use bevy::{prelude::*, time::common_conditions::once_after_delay}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) + .add_systems( + Update, + add_default_font_text.run_if(once_after_delay(Duration::from_secs(1))), + ) .run(); } -fn setup(mut commands: Commands, mut fonts: ResMut>) { +fn setup(mut commands: Commands, mut fonts: ResMut>, asset_server: Res) { commands.spawn(Camera2d); - let font = fonts.add(Font::Query { + let system_font = fonts.add(Font::Query { families: vec![ Family::Name("Liberation Sans".to_string()), Family::Name("Ubuntu".to_string()), @@ -23,22 +29,28 @@ fn setup(mut commands: Commands, mut fonts: ResMut>) { }); commands.spawn(( - Node { - width: percent(100.), - height: percent(100.), - display: Display::Flex, - padding: UiRect::all(px(20.)), - justify_content: JustifyContent::Center, - ..default() - }, - children![( - Text::new("System Font UI Text"), - TextFont::default().with_font(font.clone()), - )], + Text2d::new("System Font Text"), + TextFont::default().with_font(system_font), + Transform::from_xyz(0., 100., 0.), )); commands.spawn(( - Text2d::new("System Font 2D Text"), - TextFont::default().with_font(font), + Text2d::new("Fira Sans Bold Text"), + TextFont::default().with_font(asset_server.load("fonts/FiraSans-Bold.ttf")), + )); +} + +fn add_default_font_text(mut commands: Commands, mut fonts: ResMut>) { + let default_font = fonts.add(Font::Query { + families: vec![Family::Name("Fira Sans".to_string())], + weight: Weight::BOLD, + stretch: Stretch::Normal, + style: Style::Normal, + }); + + commands.spawn(( + Text2d::new("Queried Fira Sans Text"), + TextFont::default().with_font(default_font), + Transform::from_xyz(0., -100., 0.), )); }