Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds basic support for working with system fonts #16365

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ debug_glam_assert = ["bevy_internal/debug_glam_assert"]
# Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase
default_font = ["bevy_internal/default_font"]

# Enable loading system fonts at startup. This increases startup time, and will be a no-op on unsupported platforms.
system_font = ["bevy_internal/system_font"]

# Enable support for shaders in GLSL
shader_format_glsl = ["bevy_internal/shader_format_glsl"]

Expand Down Expand Up @@ -695,10 +698,23 @@ description = "Generates text in 2D"
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 = "Demonstrates using system fonts"
category = "2D Rendering"
# Wasm compiles, but there are no system fonts available
wasm = false

[[example]]
name = "texture_atlas"
path = "examples/2d/texture_atlas.rs"
doc-scrape-examples = true
required-features = ["system_font"]

[package.metadata.example.texture_atlas]
name = "Texture Atlas"
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,12 @@ glam_assert = ["bevy_math/glam_assert"]
# Enable assertions in debug builds to check the validity of parameters passed to glam
debug_glam_assert = ["bevy_math/debug_glam_assert"]

# Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase
default_font = ["bevy_text?/default_font"]

# Enable loading system fonts at startup. This increases startup time, and will be a no-op on unsupported platforms.
system_font = ["bevy_text?/system_font"]

# Enables the built-in asset processor for processed assets.
asset_processor = ["bevy_asset?/asset_processor"]

Expand Down
1 change: 1 addition & 0 deletions crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ keywords = ["bevy"]

[features]
default_font = []
system_font = []

[dependencies]
# bevy
Expand Down
50 changes: 50 additions & 0 deletions crates/bevy_text/src/font_library.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use alloc::sync::Arc;

use bevy_asset::{Assets, Handle};
use bevy_ecs::system::{Res, ResMut, SystemParam};
use cosmic_text::fontdb::FaceInfo;

use crate::{CosmicFontSystem, Font, TextPipeline};

/// Provides a method for finding [fonts](`Font`) based on their [`FaceInfo`].
///
/// Note that this is most useful with the `system_font` feature, which exposes
/// fonts installed on the end-users device. Without this feature, the only fonts
/// available are ones explicitly added within Bevy anyway.
#[derive(SystemParam)]
pub struct FontLibrary<'w> {
text_pipeline: ResMut<'w, TextPipeline>,
font_system: Res<'w, CosmicFontSystem>,
font_assets: ResMut<'w, Assets<Font>>,
}

impl FontLibrary<'_> {
/// Find a [`Font`] based on the provided criteria.
/// You are given access to the font's [`FaceInfo`] to aid with selection.
pub fn find(&mut self, mut f: impl FnMut(&FaceInfo) -> bool) -> Option<Handle<Font>> {
self.font_system.db().faces().find_map(|face_info| {
if !f(face_info) {
return None;
};

let face_id = face_info.id;

// TODO: If multiple families are present, should all be added?
let family_name = Arc::from(face_info.families[0].0.as_str());

let font = Font {
// TODO: The binary data isn't accessible (or required) for fonts loaded
// from the system. Perhaps an enum should be used to indicate this
// is deliberately empty, but still represents a valid font.
data: Arc::default(),
};

let font = self.font_assets.add(font);

self.text_pipeline
.register_font(font.id(), face_id, family_name);

Some(font)
})
}
}
2 changes: 2 additions & 0 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod error;
mod font;
mod font_atlas;
mod font_atlas_set;
mod font_library;
mod font_loader;
mod glyph;
mod pipeline;
Expand All @@ -50,6 +51,7 @@ pub use error::*;
pub use font::*;
pub use font_atlas::*;
pub use font_atlas_set::*;
pub use font_library::*;
pub use font_loader::*;
pub use glyph::*;
pub use pipeline::*;
Expand Down
30 changes: 26 additions & 4 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,22 @@ pub struct CosmicFontSystem(pub cosmic_text::FontSystem);

impl Default for CosmicFontSystem {
fn default() -> 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)
Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
#[cfg(feature = "system_font")]
return {
let mut font_system = cosmic_text::FontSystem::new();

// This is a no-op on unsupported platforms.
font_system.db_mut().load_system_fonts();

Self(font_system)
};

#[cfg(not(feature = "system_font"))]
return {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
let db = cosmic_text::fontdb::Database::new();
Self(cosmic_text::FontSystem::new_with_locale_and_db(locale, db))
};
}
}

Expand Down Expand Up @@ -78,6 +90,16 @@ pub struct TextPipeline {
}

impl TextPipeline {
pub(crate) fn register_font(
&mut self,
asset_id: AssetId<Font>,
cosmic_id: cosmic_text::fontdb::ID,
family_name: Arc<str>,
) {
self.map_handle_to_font_id
.insert(asset_id, (cosmic_id, family_name));
}

/// Utilizes [`cosmic_text::Buffer`] to shape and layout text
///
/// Negative or 0.0 font sizes will not be laid out.
Expand Down
1 change: 1 addition & 0 deletions docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ The default feature set enables most of the expected features of a game engine,
|symphonia-isomp4|MP4 audio format support (through symphonia)|
|symphonia-vorbis|OGG/VORBIS audio format support (through symphonia)|
|symphonia-wav|WAV audio format support (through symphonia)|
|system_font|Enable loading system fonts at startup. This increases startup time, and will be a no-op on unsupported platforms.|
|tga|TGA image format support|
|tiff|TIFF image format support|
|trace|Tracing support|
Expand Down
74 changes: 74 additions & 0 deletions examples/2d/system_fonts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Demonstrates searching for a system font at runtime.
//!
//! This example cycles through all available fonts, showing their name in said
//! font.
//! Pressing the spacebar key will select the next font.

use bevy::{prelude::*, text::FontLibrary, utils::HashSet};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update)
.run();
}

/// Marker for the text we will update.
#[derive(Component)]
struct FontName;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
Text2d::new("Press Space to change the font"),
TextFont {
// We load a fall-back font, since we don't know that every platform
// even has system fonts to use.
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 50.0,
..default()
},
TextLayout::new_with_justify(JustifyText::Center),
FontName,
));
}

fn update(
// FontLibrary provides access to all fonts loaded by the cosmic_text backend.
// With the `system_font` feature enabled, this includes fonts installed on
// the end-user's device.
mut fonts: FontLibrary,
mut query: Single<(&mut Text2d, &mut TextFont), With<FontName>>,
keyboard_input: Res<ButtonInput<KeyCode>>,
// Want to make sure we don't show the same font twice
mut used: Local<HashSet<Box<str>>>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
let mut name = default();

// The primary way to find a font is through FontLibrary::find,
// which iterates over all loaded fonts, and returns the first that
// satisfies the provided predicate.
// In this example, we're just looking for any font we haven't already
// shown.

let Some(font) = fonts.find(|font| {
let family_name = font.families[0].0.as_str();

if used.contains(family_name) {
return false;
}

used.insert(Box::from(family_name));
name = String::from(family_name);
true
}) else {
*query.0 = Text2d::new("No more fonts to show!");
return;
};

*query.0 = Text2d::new(name);
query.1.font = font;
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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) | Demonstrates using system fonts
[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
[Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d
Expand Down
Loading