Bevy Text
Text can be rendered in two ways:
- As part of the UI with a
Textcomponent - As part of your games world with a
Text2dcomponent
What should you choose? Depends on your use case:
| Use case | Decision | Reason |
|---|---|---|
| Floating combat text | Text2d | This text would be relative to our entities |
| Health meter | Text | We want this in a fixed position regardless of our viewport |
| Title screen | Text OR Text2d | Would 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:
FontSource::Handle(Handle<Font>)- a Bevy font asset loaded from disk.FontSource::Family(name)- a font family resolved from the font database.- A generic category such as
FontSource::Serif,FontSource::SansSerif,FontSource::Monospace,FontSource::SystemUi,FontSource::EmojiorFontSource::Math.
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:
AnchorFontHinting::DisabledLetterSpacingLineHeightTextBoundsTextColorTextFontTextLayoutTransformVisibility
Text instead adds:
FontHinting::EnabledLetterSpacingLineHeightNodeTextColorTextFontTextLayout
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:
TextLayoutcontrols alignment (justify) and line breaking (linebreak).TextFontcontrols the font source, size, weight, width, style, smoothing, OpenType features and variable axes.TextColorsets the color of the text.LineHeightsets the spacing between lines.LetterSpacingsets the spacing between characters.TextBoundssets a maximum width and/or height, causing text to wrap and overflow to be truncated.TextBackgroundColordraws a colored background behind the text.Underline/UnderlineColorandStrikethrough/StrikethroughColoradd text decorations.TextShadow(UI) andText2dShadow(scene) draw a shadow behind the text.FontHintingcontrols whether glyphs are hinted during rasterization. It is enabled by default for UI text and disabled forText2d.
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:
- Input filtering is available through
EditableTextFilter, and focus behavior throughSelectAllOnFocusandEditableText::max_characters. - Clipboard operations use an in-process buffer by default. Enable the
bevy/system_clipboardfeature to integrate with the OS clipboard. - Combine
EditableTextwith theTabNavigationPluginfrombevy::input_focusso users can move focus between inputs.
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:
ComputedTextBlocktracks aneeds_rerenderflag, so a block is only re-shaped when its text, spans,TextFontorTextLayoutactually change.- The
FontCxandLayoutCxresources wrap Parley's font and layout contexts, whileScaleCxwraps swash's rasterization context. - Rasterized glyphs are cached in a
FontAtlas. AFontAtlasSetkeeps oneFontAtlasper combination of font face, size, variation coordinates, hinting and smoothing. - A
FontAtlascontains one or more textureImages plus aTextureAtlasLayout, and caches each glyph at several subpixel offsets so text can be positioned smoothly. Missing glyphs are rasterized with swash and packed into the atlas.
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.