Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
77 changes: 72 additions & 5 deletions crates/bevy_text/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>,
Data(Arc<Vec<u8>>),
/// 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 {
Copy link
Contributor

@ickshonpe ickshonpe Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though it's unlikely to be confusing to anyone, Query is so ubiquitous in Bevy that I think it's probably wise to disambiguate this a bit:

Suggested change
Query {
DbQuery {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting point. If it's decided we proceed on this PR, I'll update all the references for this variant.

/// A list of font families that satisfy this font requirement.
families: Vec<Family>,
/// 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 {
Expand All @@ -28,8 +97,6 @@ impl Font {
) -> Result<Self, cosmic_text::ttf_parser::FaceParsingError> {
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)))
}
}
40 changes: 36 additions & 4 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand All @@ -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<String>,
/// Override the default identifier for the general Sans-Serif font
pub family_sans_serif: Option<String>,
/// Override the default identifier for the general Cursive font
pub family_cursive: Option<String>,
/// Override the default identifier for the general Fantasy font
pub family_fantasy: Option<String>,
/// Override the default identifier for the general Monospace font
pub family_monospace: Option<String>,
}
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)]
Expand All @@ -92,7 +124,7 @@ impl Plugin for TextPlugin {
.init_asset_loader::<FontLoader>()
.init_resource::<FontAtlasSets>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.insert_resource(CosmicFontSystem::new_with_settings(self))
.init_resource::<SwashCache>()
.init_resource::<TextIterScratch>()
.add_systems(
Expand Down
101 changes: 82 additions & 19 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
Expand All @@ -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))
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -496,34 +528,65 @@ pub fn load_font_to_fontdb(
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
fonts: &Assets<Font>,
) -> FontFaceInfo {
) -> Option<FontFaceInfo> {
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::<Vec<_>>();
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`].
Expand All @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions examples/2d/system_fonts.rs
Original file line number Diff line number Diff line change
@@ -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<Assets<Font>>, asset_server: Res<AssetServer>) {
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<Assets<Font>>) {
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.),
));
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down