bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
35.23k stars 3.47k forks source link

Better abstraction for dealing with font styles and variants #9725

Open viridia opened 1 year ago

viridia commented 1 year ago

What problem does this solve or what need does it fill?

Currently the way text styles in Bevy work is that you have to supply a font handle for each span of text. If you want to have a "bold" or "italic" section, you have to choose a different font resource, one that represents that particular style. Worse, not all fonts work this way, "variable" fonts are able to encode multiple styles in a single resource. So the usage is inconsistent - the code will be different depending on what type of font asset you have.

In CSS, there's a layer of abstraction between the raw font files and the available text styles, which is provided by the @font-face rule. This is used to register each font family name and style flags in an internal database.

When you specify a font style in a CSS rule, you give it the font name, and optionally style flags such as bold, italic, oblique, and so on. These parameters are then used to query the internal database of font faces that have been registered, and select the proper font resource.

What solution would you like?

The idea would be to define a new type of asset which corresponds to the CSS @font-face. This could be a JSON or RON file which contains a list of references to font files (possibly using relative asset paths), as well as the various style flags. So something like:

[
  {
    "font": "./Rubik-Medium.ttf",
    "weight": 400,
  },
  {
    "font": "./Rubik-Bold.ttf",
    "weight": 700,
  }
]

When this asset is loaded, the resulting asset handle could be supplied to the text span instead of a raw font handle. The text span would have style properties (weight, italic, etc.) that would be used to select the font during rendering. Alternatively, the lookup could be done when the span is created, caching the actual font handle in a private field.

This system would also support variable fonts. It might not be quite as automatic as CSS, I can envision that it might be necessary extra properties to the font-face resource to give hints to the Bevy renderer that this is a variable font.

What alternative(s) have you considered?

There's a couple of different approaches that could be taken:

1) Leave TextSection alone, and instead have a global resource that produces a Font handle from a FontFace handle. In other words, the code creating the text span would be responsible for calling the method that looks up the font face.

The problem with this approach is that I don't think it will handle variable fonts. To get bold or italic out of a variable font, I suspect you need something more than just a font handle - you likely need some additional rendering parameters which need to be stored in the text span.

This of course assuming that Bevy supports variable fonts at all.

2) Store only the font name in the text section, or in the Style object, rather than a font-face handle. This is more like the way CSS works. However, this now requires some sort of global registry of fonts that is associated with the Style or with the UI node tree.

Additional context

To make migration easier, we could have a function which takes a raw font handle, and returns a handle to a minimal font-face object. Users who are migrating could create an asset meta file that allows a raw font file to be loaded as if it were a font-face asset with only a single font variation. This would allow them to use their existing assets unchanged.

viridia commented 1 year ago

Also note that this fits in nicely with Asset V2's ability to have recursive dependencies registered in the asset loader. The font-face asset would have the actual font files as dependencies.

viridia commented 9 months ago

Bikeshedding a bit on serialization: let's say for simplicity's sake that font-faces are defined in a .toml file, which would be a Bevy asset. Also, for simplicity's sake, let's assume:

So a typical file might look like:

[[font-face]]
src="./Exo2/static/Exo2-Medium.ttf"
weight=400

[[font-face]]
src="./Exo2/static/Exo2-Bold.ttf"
weight=700

[[font-face]]
src="./Exo2/static/Exo2-MediumItalic.ttf"
weight=400
font-style="italic"

[[font-face]]
src="./Exo2/static/Exo2-BoldItalic.ttf"
weight=700
font-style="italic"

When loaded, this produces an asset which has the following structure:

struct FontFaceEntry {
    src: Handle<Font>,
    weight: FontWeight,
    style: FontStyle,
    // whatever other properties we decide to support
}

struct FontFaceAsset(Vec<FontFaceEntry>);

Ideally, the handle to the FontFaceAsset is what gets passed around rather than individual font handles. So when creating a Text node, for example, you give it a Handle<FontFaceAsset> rather than a Handle<Font>. It's up to the renderer to choose the closest matching font-face depending on whether the text is bold, italic, or whatever.

Alternatively, we could create a global registry of font faces and then simply reference font faces by name. So in this case "Exo2" would be the name used.

In either case, user could would almost never need to reference a font asset directly; it would always be a selection of font-face + style flags.

Note that CSS supports the "src" being a list of urls with fallback; however I don't see the need for this since presumably the person writing this file will make sure that all the referenced font files are present.

alice-i-cecile commented 3 months ago

@nicoburns suggests that both cosmic-text and parley offer an abstraction for this by default that we should consider hooking into.

nicoburns commented 3 months ago

They both make you specify a font by family name (e.g. just "Rubik"), and the specific font (e.g. "Rubik-Bold") is chosen by the system on the basis of the styles (bold, italic, etc) applied to a given span of text.

viridia commented 3 months ago

Sure, but we still need a way to map that to Bevy's asset system.

One approach would be to pre-register all of the font variants at app startup. This means however that the app code needs to know all of the correct font file names.

A slightly better approach would be to use an asset processor to scan the asset directory and discover the file names - this could be done at build time. But we'd need to leave behind some artifact that contains the resulting filenames, assuming we don't want to do a directory scan every time the game runs (or perhaps that's ok?)

That index of font names could be an asset in itself, which simplifies life for the developer: it means they can dynamically register and unregister whole families of fonts as needed.