Bevy Text
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:
- Here is some text. But
- 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:
- As part of our scene
- 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.