Font Aliases and Font Registration
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
## What problem does this solve or what need does it fill?
Using fonts in Bevy is currently a cumbersome process. Fonts are treated much like any other asset (images, sounds, etc.) but there's a problem of scale: each individual text span entity needs to specify the font, via a font handle. This is a problem because there are often large numbers of individual text spans scattered throughout code. Acquiring a handle requires injecting the asset server in a lot of places, as well as knowing the precise name of the asset, something that is not checked at compile time.
It gets even worse when you have rich text: the only way to render bold or italic text is to load the appropriate font asset with the correctly-spelled asset name; sadly, font files (such as those available from Google Fonts or any of the many other open font repositories) don't adhere to a common standard naming convention for styles and weights. It's easy for the user to say "-Bold" when what they should have said was "_bold".
Feathers attempted to address this problem by adding text style inheritance as part of its theming framework, but this is only a partial solution.
Ideally, what we would like is something inspired by CSS, in particular the `@font-face` rule which allows the user to specify an entire family of font variations under a single common name.
## What solution would you like?
I propose the introduction of "registered font aliases". The user can register fonts by name during app initialization, and then refer to the fonts by name within individual text spans, instead of having to pass around asset handles. The conversion from font name to asset handle is performed automatically by a fuzzy matching algorithm (details below).
This is partly inspired by the Feathers theming system and its design tokens.
### Font Alias Names
A font alias has a name which is defined by the `FontAlias(SmolStr)` type. Using `SmolStr` allows for cheap cloning of immutable strings, and can be initialized as a const:
```rust
pub const BODY_TEXT: FontAlias = FontAlias::new_static("mylibrary.body_text");
```
### Font Alias Registry
Using this name, we can then register one or more `FontFaceSpec` entries with that name:
```rust
app.register_font_alias(BODY_TEXT, &[
FontFaceSpec("fonts/FiraSans.ttf"),
FontFaceSpec("fonts/FiraSans-Bold.ttf").with_weight(FontWeight::Bold),
FontFaceSpec("fonts/FiraSans-Italic.ttf").with_style(FontStyle::Italic),
FontFaceSpec("fonts/FiraSans-BoldItalic.ttf").with_weight(FontWeight::Bold).with_style(FontStyle::Italic),
]);
```
When a font name is used, the app will attempt to find the closest matching font in the registry, and return a handle to that font. This matching process will compare all of the attributes of the `FontFaceSpec` with the incoming `FontRequirements`. This search operation will be cached, using a `HashMap` keyed by `FontRequirements`, so that if multiple text spans use identical requirements, we don't have to do the expensive search multiple times.
### FontFaceSpec
The FontFaceSpec struct will look something like this:
```rust
struct FontFaceSpec {
pub weight: FontWeight,
pub style: FontStyle,
pub stretch: FontStretch,
pub features: HashSet,
pub src: AssetPath,
// There may be additional attributes
}
```
The FontRequirements struct looks similar:
```rust
#[derive(Component)]
struct FontRequirements {
pub name: FontAlias,
pub weight: FontWeight,
pub style: FontStyle,
pub stretch: FontStretch,
pub features: HashSet,
}
```
### Font Spec Matching
The `FontFaceSpec` will have methods that compute a "distance" between the requirements and the spec. This will be the weighted sum of the distance for each of the individual properties. For example, `FontWeight::distance(other: &FontWeight) -> f32` computes the numeric difference in the weights.
The matching algorithm will check each registered variant and return the one with the closest match. This will never fail if there is at least one variant registered.
After a successful match, the `FontRequirements` are added to a cache. This cache is cleared whenever new fonts are registered or unregistered.
### Font Fallbacks
CSS supports the idea that you can ask for multiple fonts by name, and it will return the first matching one found. However, for Bevy this is likely to be too expensive: storing multiple font names for each text span is probably too much. Instead, we can do something a little lighter weight, which is to associate a fallback with a given alias:
```rust
app.register_font_fallback(BODY_TEXT, MONOSPACE);
```
This means that if a text span attempts to use `BODY_TEXT` and there's no such alias registered, the `MONOSPACE` alias will be used instead.
### Font Sizes, Lifetimes, Atlases
The font alias registry only includes properties that affect the choice of which font asset handle to select; this means that it does not include the point size of the font. This means that the a single cache entry may map to multiple font atlases representing different sizes.
We could theoretically have a second cache that manages font atlases. Or we could use some other means of managing atlas lifetimes, such as #20966 . That issue is probably out of scope for this proposal, although it is important.
## What alternative(s) have you considered?
* Doing something more like CSS with a comma-separated list of font names
* Continuing to use font handles and assign them to individual spans
## Additional context
* [MDN page on `@font-face`](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face)
@alice-i-cecile @ickshonpe
Contributor guide
Assessment
This issue has not been assessed yet.