Skip to content

Adds basic support for working with system fonts#16365

Open
bushrat011899 wants to merge 10 commits intobevyengine:mainfrom
bushrat011899:SystemFonts
Open

Adds basic support for working with system fonts#16365
bushrat011899 wants to merge 10 commits intobevyengine:mainfrom
bushrat011899:SystemFonts

Conversation

@bushrat011899
Copy link
Copy Markdown
Contributor

@bushrat011899 bushrat011899 commented Nov 13, 2024

Objective

Solution

  • Created a new feature, system_font, which will instruct cosmic_text to begin loading system fonts on startup.
  • Added a new system parameter, FontLibrary, which allows for convenient searching of all loaded fonts, returning a Handle<Font> if you have found a suitable candidate.
  • Added a new example, system_fonts, which demonstrates searching for and using system fonts.

Testing

  • CI
  • system_fonts example

Showcase

Users now have access to the FontLibrary system parameter, which allows searching for available fonts. This helps with using the new system_fonts feature, which will load system fonts at runtime.

In the below snippets, fonts is of type FontLibrary

// Find a monospaced font
let font: Option<Handle<Font>> = fonts.find(|font| font.monospaced);

// Find a font with a particular name
let font: Option<Handle<Font>> = fonts.find(|font| {
    let family_name = &font.families[0].0;
    family_name.contains("Courier")
});

system_font_demo

Notes

  • Loading system fonts increases startup time by a few seconds. I suspect this is platform dependent, and is largely out of our control. The startup cost is now negligible (I cannot measure the difference, much less than a second), since moving system font loading onto a background task. I do believe it's worth keeping this behind a feature gate as it's already organised this way, and there may be a performance cost on some platforms. More options for the user is better IMO.
  • If the system_fonts feature is enabled on a platform cosmic_text does not support, it is simply a no-op, making the feature safe to enable in all cases.
  • The FontLibrary system param could use some more methods, such as an iterator. I have deliberately only exposed a find method since we need to do some work with the Assets<Font> to ensure it's available for the TextPipeline.
  • The Font type assumes every font will have binary data associated with it (the data field). For system fonts this is not the case. I'm simply inserting no data (empty slice) for these fonts. It might be prudent to update Font to be an enum instead to make this more clear.
  • Font families are still handled the same way Bevy currently does, only selecting index 0. Changing this is out of scope for this PR.

@bushrat011899 bushrat011899 added this to the 0.16 milestone Nov 13, 2024
@bushrat011899 bushrat011899 added C-Feature A new feature, making something new possible A-Text Rendering and layout for characters X-Contentious There are nontrivial implications that should be thought through D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-SME This is currently waiting for an SME to resolve something controversial labels Nov 13, 2024
@TurtIeSocks
Copy link
Copy Markdown
Contributor

Awesome! I swapped to this branch locally with my playground example and every locale now displays without the need to load any external font files 🎊

I suppose this does increase the need for users to have the ability to control cosmic_text fallbacks. I should have time later in the week to dig into that and see what's viable.

@Bcompartment
Copy link
Copy Markdown

If it's enabled by default, add an example that shows how to disable this feature. Reasoning is simple: if an app is shipped with a font, why waste time loading all system fonts? IMO add something like: 
fn setup(....) {    ....    let system_fonts: SystemFonts = asset_loader.load_sys_fonts(); .... }

So user will have simple way, and control when to load.

@bushrat011899
Copy link
Copy Markdown
Contributor Author

If it's enabled by default...

I've left it is a feature that's disabled by default. To enable system fonts, you enable the system_font feature in Bevy. This feature does have a startup time cost and I completely agree that the functionality should be opt-in, at least while it has a noticeable impact.

@TurtIeSocks
Copy link
Copy Markdown
Contributor

I've just noticed #14150, was that effort abandoned?

@bushrat011899
Copy link
Copy Markdown
Contributor Author

I never saw that! Looks like it wasn't linked to the original issue. I'm happy to incorporate changes from that PR, or close this out in favour of that one, I'll leave that up to the reviewers.

@rparrett
Copy link
Copy Markdown
Contributor

rparrett commented Feb 26, 2025

Most likely not a Bevy issue, but the example hangs on my macbook when it eventually tries to load Noto Nastaliq Urdu after spending a while mashing the space bar.

edit: I think this reproduces in bare cosmic, but was fixed in an unreleased commit. I'll make an issue to beg for a release over there.

Also added a mapping between `AssetId<Font>` and `fontdb::ID` to avoid creating new assets and handles when it's not required.
@rparrett
Copy link
Copy Markdown
Contributor

rparrett commented Mar 7, 2025

nit: IMO, the current example could be made more practical for users seeking to use this feature (and also for automated testing). It would be nice to show a more common scenario than cycling through every font one at a time, and to demonstrate how to react to the system fonts becoming available.

github-merge-queue bot pushed a commit that referenced this pull request Mar 12, 2025
# Objective

Upgrade to `cosmic-text` 0.13

https://github.com/pop-os/cosmic-text/releases

This should include some performance improvements for layout and system
font loading.

## Solution

Bump version, fix the one changed API.

## Testing

Tested some examples locally, will invoke the example runner.

## Layout Perf

||main fps|cosmic-13 fps|
|-|-|-|
|many_buttons --recompute-text --no-borders|6.79|9.42 🟩 +38.7%|
|many_text2d --no-frustum-culling --recompute|3.19|4.28 🟩 +34.0%|
|many_glyphs --recompute-text|7.09|11.17 🟩 +57.6%|
|text_pipeline |140.15|139.90 ⬜ -0.2%|

## System Font Loading Perf

I tested on macOS somewhat lazily by adding the following system to the
`system_fonts` example from #16365.

<details>
<summary>Expand code</summary>

```rust
fn exit_on_load(
    mut reader: EventReader<bevy::text::SystemFontsAvailable>,
    mut writer: EventWriter<AppExit>,
) {
    for _evt in reader.read() {
        writer.write(AppExit::Success);
    }
}
```
</details>

And running `hyperfine 'cargo run --release --example system_fonts
--features=system_font'`.

The results were nearly identical with and without this PR cherry-picked
there.

Co-authored-by: Alice Cecile <[email protected]>
Comment on lines +46 to +72
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nitpick - I think there's an idiomatic way to avoid some returns here.

Suggested change
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;
}
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 next_font = fonts.find(|font|{
let family_name = font.families[0].0.as_str();
if used.contains(family_name) {
false
} else {
used.insert(Box::from(family_name));
name = String::from(family_name);
true
}
});
match next_font {
Some(font) => {
*query.0 = Text2d::new(name);
query.1.font = font;
},
None => {
*query.0 = Text2d::new("No more fonts to show!");
},
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually I think for the end, clippy usually prefers if let ... else also but I did this in github comments not my editor XD

Copy link
Copy Markdown
Contributor

@krunchington krunchington Mar 13, 2025

Choose a reason for hiding this comment

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

An example alternate approach that I think ends up a lot more readable, if as in my other comment more info was returned from .find

    if keyboard_input.just_pressed(KeyCode::Space) {
        // 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 next_font = fonts.find(|font|{
            let family_name = font.families[0].0.as_str();
            used.contains(family_name)
        });
        
        if let Some((handle, face_info)) = next_font {
            let family_name = face_info.families[0].0.as_str();
            *query.0 = Text2d::new(family_name);
            query.1.font = handle;
        } else {
            *query.0 = Text2d::new("No more fonts to show!");
        }
    }

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>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be useful to return the FaceInfo as part of the Option also- noticed in the example you had to use a mutable local variable to store data from the FaceInfo. Maybe something like Option<(Handle<Font>, &FaceInfo)>?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also worth noting that this function signature, through the predicate, "leaks" the abstraction of a cosmic_text::fontdb::FaceInfo. That might be intended and fine, but worth pointing out if it is preferable to keep the bevy interface more opaque (which could make it easier to replace cosmic_text in the future if necessary)

@alice-i-cecile alice-i-cecile removed this from the 0.16 milestone Mar 17, 2025
@asgeo1
Copy link
Copy Markdown

asgeo1 commented Jul 30, 2025

Any chance this can be added to bevy 0.17 or 0.18 milestones? Really looking forward to this functionality and hoping it's still on the roadmap.

Copy link
Copy Markdown
Contributor

@dsgallups dsgallups left a comment

Choose a reason for hiding this comment

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

Looks great!

@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Oct 5, 2025
@alice-i-cecile alice-i-cecile added M-Release-Note Work that should be called out in the blog due to impact and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 5, 2025
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Oct 5, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@alice-i-cecile alice-i-cecile removed this from the 0.18 milestone Dec 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Text Rendering and layout for characters C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-SME This is currently waiting for an SME to resolve something controversial X-Contentious There are nontrivial implications that should be thought through

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support using system fonts

10 participants