Tainted\\Coders

Bevy Text

Bevy version: 0.19Last updated:

Text can be rendered in two ways:

  1. As part of the UI with a Text component
  2. As part of your games world with a Text2d component

What should you choose? Depends on your use case:

Use caseDecisionReason
Floating combat textText2dThis text would be relative to our entities
Health meterTextWe want this in a fixed position regardless of our viewport
Title screenText OR Text2dWould depend on the effect you are going for

Bevy can also render text for other purposes: the EditableText widget captures keyboard input, and Gizmos::text draws lightweight debug labels in your world.

Fonts

The default font provided by Bevy is Fira Mono and the supported font types are ttf and otf. You load fonts using the AssetServer, like your other assets.

A TextFont identifies which font it wants with a FontSource, which can be one of:

fn spawn_with_font_source(mut commands: Commands, assets: Res<AssetServer>) {
  // Load a font by asset handle, exactly like any other asset.
  commands.spawn((
    Text::new("Loaded by handle"),
    TextFont {
      font: assets.load("fonts/FiraSans-Bold.ttf").into(),
      ..default()
    },
  ));

  // Or select a font by its family name from the font database.
  commands.spawn((
    Text::new("Loaded by family name"),
    TextFont {
      font: FontSource::Family("Fira Mono".into()),
      ..default()
    },
  ));

  // Or select a generic category, resolved to a concrete family by the
  // database.
  commands.spawn((
    Text::new("Generic monospace"),
    TextFont {
      font: FontSource::Monospace,
      ..default()
    },
  ));
}

Family names and generic categories are resolved by Parley's internal font database.

To make installed system fonts available by name, enable the bevy/system_font_discovery feature. Without that feature, FontSource::Family only finds fonts that you have loaded into Bevy as assets.

Each generic family resolves to a configurable default. You can override those defaults through the FontCx resource. These methods return a Result because they fail if the requested family is not in the font database:

fn configure_generic_fonts(mut font_cx: ResMut<FontCx>) -> Result<()> {
  // These return `Err` if the family is not present in the font database.
  font_cx.set_monospace_family("JetBrains Mono")?;
  font_cx.set_serif_family("Merriweather")?;
  Ok(())
}

If you don't specify a font, TextFont::default() uses the default handle, which points at the bundled Fira Mono subset when the default_font feature is enabled (it is on by default).

OpenType features and variable fonts

As of 0.18 Bevy supports OpenType font features to allow fine-grained control over how text is displayed. Features are on/off switches declared by the font, such as ligatures, small caps and fractions, and are configured with FontFeatures:

fn font_features_text(mut commands: Commands) {
  // `FontFeatures` turn OpenType layout features on and off. They only apply
  // when the loaded font supports the corresponding feature.
  commands.spawn((
    Text::new("1/2 of the 1st"),
    TextFont {
      font_features: FontFeatures::builder()
        .enable(FontFeatureTag::FRACTIONS)
        .enable(FontFeatureTag::ORDINALS)
        .build(),
      ..default()
    },
  ));
}

0.19 added support for variable fonts, which expose continuous numeric axes rather than discrete variants. The TextFont fields weight, width and style pick a face from the font, while FontVariations sets individual axes directly for finer control:

fn variable_font_styles(mut commands: Commands, assets: Res<AssetServer>) {
  let font: FontSource = assets.load("fonts/MonaSans-VariableFont.ttf").into();

  // Variable font faces can be selected by weight, width and style.
  // These only apply when the font actually contains those axes.
  commands.spawn((
    Text::new("Bold, condensed and italic"),
    TextFont {
      font: font.clone(),
      font_size: FontSize::Px(32.0),
      weight: FontWeight::BOLD,
      width: FontWidth::CONDENSED,
      style: FontStyle::Italic,
      ..default()
    },
  ));

  // Fine-grained control over variable axes with `FontVariations`.
  commands.spawn((
    Text::new("Weight axis 700"),
    TextFont {
      font,
      font_variations: FontVariations::builder()
        .set(FontVariationTag::WEIGHT, 700.0)
        .build(),
      ..default()
    },
  ));
}

Both FontFeatures and variable axes only have an effect when the loaded font actually declares them. weight in particular only works with variable weight fonts. For static fonts you still need to load the specific face you want.

Creating text

Text is either rendered through the UI with a Text component, or inside your game world with a Text2d component.

If we wanted our text to interact with, or be part of our game world, then we should choose Text2d.

If instead we wanted our text positioned in relation to our window (like a HUD, or an inventory screen) then we should use Text and make it part of our UI.

By placing one of these components onto an entity, other systems in bevy_text and the core renderer will spring into action.

We can render the text as part of the scene by assembling the various components on a regular entity:

fn spawn_in_scene(mut commands: Commands, assets: Res<AssetServer>) {
  let font = assets.load("fonts/FiraSans-Bold.ttf");

  commands.spawn((
    Text2d::new("Here is some text in the scene"),
    TextFont::from(font).with_font_size(FontSize::Px(60.0)),
    TextColor::WHITE,
    // `TextLayout::justify` only aligns the lines *within* the block of text.
    TextLayout::justify(Justify::Center),
    // Wrapping and truncation are controlled by `TextBounds`, not by the
    // `Node`.
    TextBounds::new_horizontal(300.0),
    // The `Anchor` controls where the text sits relative to its `Transform`.
    Anchor::TOP_LEFT,
    Text2dShadow::default(),
  ));
}

This text will be positioned just like your other components and rendered normally in the scene.

Text2d adds a set of components for us:

Text instead adds:

By spawning our own modified versions of these components we are overriding the defaults.

Note that for Text2d the justify field of TextLayout only affects the internal alignment of a block of text. The text's position relative to its Transform is controlled by the Anchor component, and wrapping is controlled by TextBounds.

Lets say we have some text with two styles we want to keep together like:

"Here is some text. But this part is bold"

This is ultimately a single text block, but each part needs a separate style. We can represent this by creating a parent with the base Text/Text2d component and a child with a TextSpan component:

fn spawn_in_ui(mut commands: Commands, assets: Res<AssetServer>) {
  let regular_font_handle: Handle<Font> = Default::default();
  let bold_font_handle: Handle<Font> = assets.load("fonts/Roboto-Bold.ttf");

  commands.spawn((
    Node {
      position_type: PositionType::Absolute,
      bottom: Val::Px(5.0),
      right: Val::Px(5.0),
      ..default()
    },
    Text::new("Here is some text. But "),
    TextFont::from(regular_font_handle.clone()),
    children![(
      TextSpan::new("this part is bold"),
      TextFont::from(bold_font_handle.clone()),
    )],
  ));
}

This will allow Bevy's text layout engine to place these in the proper position next to each other and according to the rules of our Node. Spans can also have their own TextFont, TextColor, and decoration components, and may be nested to build up richer layouts.

Styling text

To style your text there are a number of components we can add to Text, Text2d or TextSpan entities:

  1. TextLayout controls alignment (justify) and line breaking (linebreak).
  2. TextFont controls the font source, size, weight, width, style, smoothing, OpenType features and variable axes.
  3. TextColor sets the color of the text.
  4. LineHeight sets the spacing between lines.
  5. LetterSpacing sets the spacing between characters.
  6. TextBounds sets a maximum width and/or height, causing text to wrap and overflow to be truncated.
  7. TextBackgroundColor draws a colored background behind the text.
  8. Underline/UnderlineColor and Strikethrough/StrikethroughColor add text decorations.
  9. TextShadow (UI) and Text2dShadow (scene) draw a shadow behind the text.
  10. FontHinting controls whether glyphs are hinted during rasterization. It is enabled by default for UI text and disabled for Text2d.

Each one has fields for changing the style of your text. By adding them to an entity that has a Text or Text2d component you are overriding the defaults for that particular node.

fn style_text(mut commands: Commands) {
  commands.spawn((
    Text::new("Styled text"),
    TextFont::from_font_size(FontSize::Px(32.0)),
    TextColor(Color::srgb(0.9, 0.9, 0.9)),
    LineHeight::RelativeToFont(1.5),
    LetterSpacing::Px(2.0),
    TextBackgroundColor(Color::BLACK),
    Underline,
    // `Text2d`/`Text`/`TextSpan` entities can also add `Strikethrough`.
  ));
}

Responsive font sizes

TextFont::font_size is a FontSize enum rather than a plain number. This lets text scale with the viewport or with a single shared base value:

fn responsive_text(mut commands: Commands) {
  // 5% of the viewport height.
  commands.spawn((
    Text::new("Viewport sized"),
    TextFont::from_font_size(FontSize::Vh(5.0)),
  ));

  // Relative to the `RemSize` resource, so one value resizes all relative text.
  commands.spawn((
    Text::new("Rem sized"),
    TextFont::from_font_size(FontSize::Rem(1.5)),
  ));
}

The variants mirror CSS: Px, Vw, Vh, VMin, VMax, and Rem. Rem values are multiplied by the RemSize resource, so changing that one resource resizes all relative text at once. A bare f32 still converts into FontSize::Px automatically, so font_size: 60.0 keeps working. Viewport units on Text2d are resolved against the primary window rather than the render target.

Changing text

We can change the value of text rendered to the screen at anytime through the Text, Text2d or TextSpan component we spawned.

For example, if we wanted to create an inventory screen where players could hover over items and we show them the name:

fn handle_hovered_item(
  mut text_query: Query<&mut Text, With<ItemName>>,
  item_query: Query<&Item, Added<Selected>>,
) {
  if let Ok(item_data) = item_query.single() {
    for mut text in text_query.iter_mut() {
      text.0 = item_data.name.clone();
    }
  }
}

It does not matter whether we are using UI Text or world Text2d, we manipulate the components on the entities the same way. We can also iterate over many text entities at once:

fn change_text(mut query: Query<&mut Text>) {
  for mut text in &mut query {
    // Change the content of our text
    text.0 = "Hello World!".to_string();
  }
}

Clicking on text

To click text we can use Bevy's built-in picking triggers using observers.

First we create a handler for the observed event:

fn handle_click(event: On<Pointer<Click>>, mut query: Query<&mut Text2d>) {
  let mut text = query.get_mut(event.entity).unwrap();
  text.0 = "You clicked me!".to_string();
}

Then we can choose the entities we want to observe:

fn spawn_clickable_text(mut commands: Commands) {
  commands
    .spawn(Text2d::new("This is some text in the scene"))
    .observe(handle_click);
}

Now when you click the text it changes to "You clicked me!".

Text input

0.19 added the EditableText component for text entry. Spawning it creates a simple editable region that handles typing, cursor movement, selection, etc.

fn spawn_editable_text(mut commands: Commands) {
  commands.spawn((
    Node {
      width: Val::Px(200.0),
      border: UiRect::all(Val::Px(2.0)),
      padding: UiRect::all(Val::Px(8.0)),
      ..default()
    },
    BorderColor::from(Color::WHITE),
    BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
    EditableText::default(),
    TextFont::from_font_size(FontSize::Px(24.0)),
    TextCursorStyle::default(),
  ));
}

Updates are reported through the TextEditChange event, which is triggered after the edit has been applied:

fn handle_text_edit(_event: On<TextEditChange>, _inputs: Query<&EditableText>) {
  // `TextEditChange` is triggered *after* the edit has been applied.
}

A few things worth knowing:

For quick debug labels rather than real text, Gizmos::text and Gizmos::text_2d draw world-space text with a built-in ASCII stroke font and no setup.

Font rendering

Bevy does not draw glyphs straight from the font file. When the text in a block changes, the TextPipeline resource shapes and lays out the spans with Parley and stores the result in a ComputedTextBlock component. Bevy then walks that layout and produces a list of PositionedGlyphs in TextLayoutInfo, which the renderer draws as textured quads.

Because shaping and rasterization are expensive, Bevy caches aggressively:

The Font asset is just the raw font file bytes plus an alias. The FontAtlas is the rasterized cache that Bevy generates from it.

A Bevy Font is a single font face, not an entire family. The family, weight, width and style associated with a face are what FontSource and the variable font fields use to select it.

Those glyphs are then scaled and transformed on the GPU to their final screen positions and sizes.