Tainted\\Coders

Bevy Text

Bevy version: 0.14Last updated:

There is a major overhaul coming that will introduce many breaking changes. This will be updated after Bevy 0.15 is released.

Text is provided as a builtin through the bevy_text crate. It adds a plugin that provides text resources, systems, and font assets to your game. It also adds a system to your render app which helps find sprites during your renders extract loop.

The core responsibility of bevy_text is to take our Text components and turn them into a series of positioned graphemes in our render app. A grapheme refers to the smallest unit of a writing system that carries meaning.

When you add a Text component Bevy will use a GlyphBrush to position the letters of text and cache those positions so that on another render pass we don't update them unless we need to.

A font is how we know what glyph to render for each grapheme. They are essentially a bunch of sprites contained inside a group of TextureAtlas called a FontAtlas which helps optimize text rendering. These sprites are then scaled and transformed to be positioned on your screen with a proper size when they are rendered by your GPU.

When you add a font to your FontAtlas it uses a DynamicTextureAtlasBuilder to load at runtime only the exact glyph sprites it needs to display the text on your screen.

The default font provided by Bevy is Fira Mono and the supported font types are ttf and otf.

The text component

The core of the plugin is the Text component which gets extracted and rendered by Bevy onto your window.

#[derive(Component, Debug, Clone, Reflect)]
pub struct Text {
  pub sections: Vec<TextSection>,

  // The text's internal alignment.
  // Should not affect its position within a container.
  pub alignment: TextAlignment,

  // How the text should linebreak when running out of the bounds determined by max_size
  pub linebreak_behavior: BreakLineOn,
}

Text is made up of TextSection which holds the actual content and style of the individual parts of our Text.

Text sections

Realistically this could have been called TextSpan to be more clear. The reason they chose section was to align with a supporting crate.

A section is any grouping of text within your Text component that has a different style.

As an example we may have multiple sections within text where the style differs:

Here is some text. But this part is bold

This is ultimately one Text component with two sections:

  1. Here is some text. But
  2. this part is bold

Each section would have a different TextStyle which controls the look of our section:

#[derive(Clone, Debug, Reflect)]
pub struct TextStyle {
  pub font: Handle<Font>,
  pub font_size: f32,
  pub color: Color,
}

If we were creating it by hand we could spawn one on an entity:

fn spawn_text(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");

  let text = Text::from_sections([
    TextSection::new(
      "Here is some text. But ",
      TextStyle {
        font: regular_font_handle.clone(),
        ..default()
      },
    ),
    TextSection::new(
      "this part is bold",
      TextStyle {
        font: bold_font_handle.clone(),
        ..default()
      },
    ),
  ]);

  commands.spawn(text);
}

Creating text

There are two kinds of rendering environments for our text:

  1. As part of our scene
  2. As part of our UI

We can render the text as part of the scene using Text2dBundle:

fn spawn_in_scene(mut commands: Commands, assets: Res<AssetServer>) {
  let font = assets.load("fonts/FiraSans-Bold.ttf");
  let text_style = TextStyle {
    font: font.clone(),
    font_size: 60.0,
    color: Color::WHITE,
  };
  let text_alignment = JustifyText::Center;
  let text = Text::from_section("translation", text_style.clone())
    .with_justify(text_alignment);

  commands.spawn(Camera2dBundle::default());
  commands.spawn(Text2dBundle { text, ..default() });
}

Doing this will place the Text and supporting components onto an entity that is positioned using a SpatialBundle.

The alternative is to render it as part of the UI with TextBundle:

fn spawn_in_ui(mut commands: Commands, assets: Res<AssetServer>) {
  commands.spawn(
    // Create a TextBundle that has a Text with a single section.
    TextBundle::from_section(
      // Accepts a `String` or any type that converts into a `String`, such as `&str`
      "hello\nbevy!",
      TextStyle {
        font: assets.load("fonts/FiraSans-Bold.ttf"),
        font_size: 100.0,
        color: Color::WHITE,
      },
    ) // Set the alignment of the Text
    .with_text_justify(JustifyText::Center)
    // Set the style of the TextBundle itself.
    .with_style(Style {
      position_type: PositionType::Absolute,
      bottom: Val::Px(5.0),
      right: Val::Px(15.0),
      ..default()
    }),
  );
}

Here our texts position and layout will be calculated according to our Node layout on the page.

If we wanted our text to interact with or be part of our game world then we should choose the first option.

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

One subtle difference here is that the y-axis of the two options is different. Rendering in our scene is BottomToTop where rendering to the UI its TopToBottom.

Changing text

We can change the value of text rendered to the screen at anytime through the Text resource that spawned when you used a Text2dBundle or a TextBundle to your entity:

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

    // Turn the text purple
    text.sections[0].style.color = Color::srgb(1.0, 0.0, 1.0);
  }
}

It does not matter if your Text was added to the UI through a node or just as part of your scene, we still manipulate the components on the entities that spawn the exact same way.

This makes it a powerful way of updating our UI depending on the context of our game:

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

Clicking on text

Actually interacting with text can be much more difficult in Bevy currently.

During the rendering pipeline a TextLayoutInfo component is added to any Text components being rendered.

We could use the TextLayoutInfo to calculate a bounding rectangle of each glyph:

#[derive(Component, Clone, Default, Debug)]
pub struct TextLayoutInfo {
  pub glyphs: Vec<PositionedGlyph>,
  pub size: Vec2,
}

#[derive(Debug, Clone)]
pub struct PositionedGlyph {
  pub position: Vec2,
  pub size: Vec2,
  pub atlas_info: GlyphAtlasInfo,
  pub section_index: usize,
  pub byte_index: usize,
}

Then we could use Window::cursor_position and Camera2d::viewport_to_world as described here.