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/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index b748f4a111fdd..99b9186e16e5a 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -16,9 +16,78 @@ 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 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. + /// + /// 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 +97,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 1e341880e5336..445439bdbaed2 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, }; } @@ -75,8 +78,37 @@ 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. + /// + /// 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, + } + } +} /// System set in [`PostUpdate`] where all 2d text update systems are executed. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] @@ -92,7 +124,7 @@ impl Plugin for TextPlugin { .init_asset_loader::() .init_resource::() .init_resource::() - .init_resource::() + .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 03939d47d16f0..49d64656e95ce 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -13,11 +13,11 @@ 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, - PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, TextPlugin, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -30,9 +30,40 @@ pub struct CosmicFontSystem(pub cosmic_text::FontSystem); impl Default for CosmicFontSystem { fn default() -> Self { + 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 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 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 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)) } } @@ -140,7 +171,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 { @@ -496,34 +528,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::Query { + 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`]. @@ -536,7 +599,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) diff --git a/examples/2d/system_fonts.rs b/examples/2d/system_fonts.rs new file mode 100644 index 0000000000000..36a3a8046cf2a --- /dev/null +++ b/examples/2d/system_fonts.rs @@ -0,0 +1,56 @@ +//! Uses a system font to display text +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>, asset_server: Res) { + commands.spawn(Camera2d); + + let system_font = fonts.add(Font::Query { + 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(( + Text2d::new("System Font Text"), + TextFont::default().with_font(system_font), + Transform::from_xyz(0., 100., 0.), + )); + + commands.spawn(( + 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.), + )); +} 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