Skip to content

Commit e5d4dfd

Browse files
Minimal Font Families, Font Queries, Collections, System Fonts, Stretch, and Slant support (#22156)
# Objective Implement support for the remaining missing text features with minimal changes. ## Solution `TextFont` has been expanded to include new fields: ```rust pub struct TextFont { pub font: FontSource, pub font_size: f32, pub weight: FontWeight, pub width: FontWidth, pub style: FontStyle, pub font_smoothing: FontSmoothing, pub font_features: FontFeatures, } ``` FontSource has two variants: Handle, which identifies a font by asset handle, and Family, which selects a font by its family name. `FontWidth` is a newtype struct representing OpenType font stretch classifications ranging from ULTRA_CONDENSED (50%) to ULTRA_EXPANDED (200%). `FontStyle` is an enum used to set the slant style of a font, either `Normal`, `Italic`, or `Oblique`. The system font support is very barebones. You load them using the `CosmicFontSystem` resource: ```rust font_system.db_mut().load_system_fonts() ``` Then they are available to be selected by family name using `FontSource::Family`. ### Other changes * `TextPipelines`'s `glyph_info` field has been removed. There is no need to collect the section infos or perform any querys during text layout updates, so that code has been removed as well. * `update_text_layout_info` used some `try_for_each` with some nested closures which was unnecessarily complicated again. They've been replaced with a regular for loop. * After font assets are loaded there's a new system `load_font_assets_into_fontdb_system` that automatically adds them to cosmic text's font database. Then they are available to be looked up by family name as well as by asset handle. * There aren't are performance motivated changes but layout updates seem to be overall significantly more efficient now, with a slight regression for very large numbers of short, single section text entities. * Font texture atlases are no longer automatically cleared when the font asset they were generated from is removed. There is no way to remove individual fonts from cosmic text's `FontSystem`, so the font is still accessible using the family name with `FontSource::family` and removing the text atlases naively could cause a panic since rendering expects them to be present. ## Testing ``` cargo run --example font_query ``` --- ## Showcase <img width="1229" height="591" alt="font-query" src="https://github.com/user-attachments/assets/23f5aaa2-fdb8-4448-9b4e-9d65d6431107" /> --------- Co-authored-by: Thierry Berger <[email protected]>
1 parent 8733d25 commit e5d4dfd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+821
-446
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,6 +3688,17 @@ description = "Demonstrates dragging and dropping UI nodes"
36883688
category = "UI (User Interface)"
36893689
wasm = true
36903690

3691+
[[example]]
3692+
name = "font_query"
3693+
path = "examples/ui/font_query.rs"
3694+
doc-scrape-examples = true
3695+
3696+
[package.metadata.example.font_query]
3697+
name = "Font Queries"
3698+
description = "Demonstrates font querying"
3699+
category = "UI (User Interface)"
3700+
wasm = true
3701+
36913702
[[example]]
36923703
name = "display_and_visibility"
36933704
path = "examples/ui/display_and_visibility.rs"

crates/bevy_feathers/src/font_styles.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub(crate) fn on_changed_font(
5858
}
5959
{
6060
commands.entity(insert.entity).insert(Propagate(TextFont {
61-
font,
61+
font: font.into(),
6262
font_size: style.font_size,
6363
..Default::default()
6464
}));

crates/bevy_sprite/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,11 @@ impl Plugin for SpritePlugin {
8484
PostUpdate,
8585
(
8686
bevy_text::detect_text_needs_rerender::<Text2d>,
87-
update_text2d_layout
88-
.after(bevy_camera::CameraUpdateSystems)
89-
.after(bevy_text::free_unused_font_atlases_system),
87+
update_text2d_layout.after(bevy_camera::CameraUpdateSystems),
9088
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
9189
)
9290
.chain()
91+
.after(bevy_text::load_font_assets_into_fontdb_system)
9392
.in_set(bevy_text::Text2dUpdateSystems)
9493
.after(bevy_app::AnimationSystems),
9594
);

crates/bevy_sprite/src/text2d.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ pub fn update_text2d_layout(
179179
&mut ComputedTextBlock,
180180
Ref<FontHinting>,
181181
)>,
182-
text_font_query: Query<&TextFont>,
183182
mut text_reader: Text2dReader,
184183
mut font_system: ResMut<CosmicFontSystem>,
185184
mut swash_cache: ResMut<SwashCache>,
@@ -275,8 +274,6 @@ pub fn update_text2d_layout(
275274

276275
match text_pipeline.update_text_layout_info(
277276
&mut text_layout_info,
278-
text_font_query,
279-
scale_factor as f64,
280277
&mut font_atlas_set,
281278
&mut texture_atlases,
282279
&mut textures,

crates/bevy_text/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cosmic-text = { version = "0.16", features = ["shape-run-cache"] }
3434
thiserror = { version = "2", default-features = false }
3535
serde = { version = "1", features = ["derive"] }
3636
smallvec = { version = "1", default-features = false }
37+
smol_str = { version = "0.2", default-features = false }
3738
sys-locale = "0.3.0"
3839
tracing = { version = "0.1", default-features = false, features = ["std"] }
3940

crates/bevy_text/src/font.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
use alloc::sync::Arc;
22

33
use bevy_asset::Asset;
4+
use bevy_asset::AssetEvent;
5+
use bevy_asset::Assets;
6+
use bevy_ecs::message::MessageReader;
7+
use bevy_ecs::system::Query;
8+
use bevy_ecs::system::ResMut;
49
use bevy_reflect::TypePath;
10+
use cosmic_text::fontdb::ID;
511
use cosmic_text::skrifa::raw::ReadError;
612
use cosmic_text::skrifa::FontRef;
13+
use smallvec::SmallVec;
14+
15+
use crate::ComputedTextBlock;
16+
use crate::CosmicFontSystem;
717

818
/// An [`Asset`] that contains the data for a loaded font, if loaded as an asset.
919
///
@@ -21,6 +31,8 @@ use cosmic_text::skrifa::FontRef;
2131
pub struct Font {
2232
/// Content of a font file as bytes
2333
pub data: Arc<Vec<u8>>,
34+
/// Ids for fonts in font file
35+
pub ids: SmallVec<[ID; 8]>,
2436
}
2537

2638
impl Font {
@@ -29,6 +41,38 @@ impl Font {
2941
let _ = FontRef::from_index(&font_data, 0)?;
3042
Ok(Self {
3143
data: Arc::new(font_data),
44+
ids: SmallVec::new(),
3245
})
3346
}
3447
}
48+
49+
/// Add new font assets to the font system's database.
50+
pub fn load_font_assets_into_fontdb_system(
51+
mut fonts: ResMut<Assets<Font>>,
52+
mut events: MessageReader<AssetEvent<Font>>,
53+
mut cosmic_font_system: ResMut<CosmicFontSystem>,
54+
mut text_block_query: Query<&mut ComputedTextBlock>,
55+
) {
56+
let mut new_fonts_added = false;
57+
let font_system = &mut cosmic_font_system.0;
58+
for event in events.read() {
59+
if let AssetEvent::Added { id } = event
60+
&& let Some(font) = fonts.get_mut(*id)
61+
{
62+
let data = Arc::clone(&font.data);
63+
font.ids = font_system
64+
.db_mut()
65+
.load_font_source(cosmic_text::fontdb::Source::Binary(data))
66+
.into_iter()
67+
.collect();
68+
new_fonts_added = true;
69+
}
70+
}
71+
72+
// Whenever new fonts are added, update all text blocks so they use the new fonts.
73+
if new_fonts_added {
74+
for mut block in text_block_query.iter_mut() {
75+
block.needs_rerender = true;
76+
}
77+
}
78+
}
Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,22 @@
1-
use crate::{Font, FontAtlas, FontSmoothing, TextFont};
2-
use bevy_asset::{AssetEvent, AssetId};
1+
use crate::{FontAtlas, FontSmoothing};
32
use bevy_derive::{Deref, DerefMut};
4-
use bevy_ecs::{message::MessageReader, resource::Resource, system::ResMut};
3+
use bevy_ecs::resource::Resource;
54
use bevy_platform::collections::HashMap;
5+
use cosmic_text::fontdb::ID;
66

77
/// Identifies the font atlases for a particular font in [`FontAtlasSet`]
88
///
99
/// Allows an `f32` font size to be used as a key in a `HashMap`, by its binary representation.
1010
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
1111
pub struct FontAtlasKey {
1212
/// Font asset id
13-
pub id: AssetId<Font>,
13+
pub id: ID,
1414
/// Font size via `f32::to_bits`
1515
pub font_size_bits: u32,
1616
/// Antialiasing method
1717
pub font_smoothing: FontSmoothing,
1818
}
1919

20-
impl From<&TextFont> for FontAtlasKey {
21-
fn from(font: &TextFont) -> Self {
22-
Self {
23-
id: font.font.id(),
24-
font_size_bits: font.font_size.to_bits(),
25-
font_smoothing: font.font_smoothing,
26-
}
27-
}
28-
}
29-
3020
/// Set of rasterized fonts stored in [`FontAtlas`]es.
3121
#[derive(Debug, Default, Resource, Deref, DerefMut)]
3222
pub struct FontAtlasSet(HashMap<FontAtlasKey, Vec<FontAtlas>>);
@@ -38,15 +28,3 @@ impl FontAtlasSet {
3828
.is_some_and(|font_atlas| font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)))
3929
}
4030
}
41-
42-
/// A system that automatically frees unused texture atlases when a font asset is removed.
43-
pub fn free_unused_font_atlases_system(
44-
mut font_atlas_sets: ResMut<FontAtlasSet>,
45-
mut font_events: MessageReader<AssetEvent<Font>>,
46-
) {
47-
for event in font_events.read() {
48-
if let AssetEvent::Removed { id } = event {
49-
font_atlas_sets.retain(|key, _| key.id != *id);
50-
}
51-
}
52-
}

crates/bevy_text/src/lib.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod pipeline;
4242
mod text;
4343
mod text_access;
4444

45+
use bevy_asset::AssetEventSystems;
4546
pub use bounds::*;
4647
pub use error::*;
4748
pub use font::*;
@@ -59,13 +60,14 @@ pub use text_access::*;
5960
pub mod prelude {
6061
#[doc(hidden)]
6162
pub use crate::{
62-
Font, FontHinting, FontWeight, Justify, LineBreak, Strikethrough, StrikethroughColor,
63-
TextColor, TextError, TextFont, TextLayout, TextSpan, Underline, UnderlineColor,
63+
Font, FontHinting, FontSource, FontStyle, FontWeight, FontWidth, Justify, LineBreak,
64+
Strikethrough, StrikethroughColor, TextColor, TextError, TextFont, TextLayout, TextSpan,
65+
Underline, UnderlineColor,
6466
};
6567
}
6668

6769
use bevy_app::prelude::*;
68-
use bevy_asset::{AssetApp, AssetEventSystems};
70+
use bevy_asset::AssetApp;
6971
use bevy_ecs::prelude::*;
7072

7173
/// The raw data for the default font used by `bevy_text`
@@ -94,7 +96,7 @@ impl Plugin for TextPlugin {
9496
.init_resource::<TextIterScratch>()
9597
.add_systems(
9698
PostUpdate,
97-
free_unused_font_atlases_system.before(AssetEventSystems),
99+
load_font_assets_into_fontdb_system.after(AssetEventSystems),
98100
)
99101
.add_systems(Last, trim_cosmic_cache);
100102

0 commit comments

Comments
 (0)