docs/bevy/examples/ui/text/text.rs 6.7 K · 202 lines · raw · history

1 //! This example illustrates how to create UI text and update it in a system.
2 //!
3 //! It displays the current FPS in the top left corner, as well as text that changes color
4 //! in the bottom right. For text within a scene, please see the text2d example.
5
6 use bevy::{
7 color::palettes::css::GOLD,
8 diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
9 prelude::*,
10 text::{FontFeatureTag, FontFeatures, FontSize, Underline},
11 };
12
13 fn main() {
14 let mut app = App::new();
15 app.add_plugins((DefaultPlugins, FrameTimeDiagnosticsPlugin::default()))
16 .add_systems(Startup, setup)
17 .add_systems(Update, (text_update_system, text_color_system));
18 app.run();
19 }
20
21 // Marker struct to help identify the FPS UI component, since there may be many Text components
22 #[derive(Component)]
23 struct FpsText;
24
25 // Marker struct to help identify the color-changing Text component
26 #[derive(Component)]
27 struct AnimatedText;
28
29 fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
30 // UI camera
31 commands.spawn(Camera2d);
32 // Text with one section
33 commands.spawn((
34 // Accepts a `String` or any type that converts into a `String`, such as `&str`
35 Text::new("hello\nbevy!"),
36 Underline,
37 TextFont {
38 // This font is loaded and will be used instead of the default font.
39 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
40 // The size of the text will be 20% of the viewport height.
41 font_size: FontSize::Vh(20.0),
42 ..default()
43 },
44 TextShadow::default(),
45 // Set the justification of the Text
46 TextLayout::justify(Justify::Center),
47 // Set the style of the Node itself.
48 Node {
49 position_type: PositionType::Absolute,
50 bottom: px(5),
51 right: px(5),
52 ..default()
53 },
54 AnimatedText,
55 ));
56
57 // Text with multiple sections
58 commands
59 .spawn((
60 // Create a Text with multiple child spans.
61 Text::new("FPS: "),
62 TextFont {
63 // This font is loaded and will be used instead of the default font.
64 font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
65 font_size: FontSize::Px(42.0),
66 ..default()
67 },
68 ))
69 .with_child((
70 TextSpan::default(),
71 (
72 TextFont {
73 // If the "default_font" feature is unavailable, load a font to use instead.
74 #[cfg(not(feature = "default_font"))]
75 font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
76 font_size: FontSize::Px(33.0),
77 ..Default::default()
78 },
79 TextColor(GOLD.into()),
80 ),
81 FpsText,
82 ));
83
84 // Text with OpenType features
85 let opentype_font_handle: FontSource =
86 asset_server.load("fonts/EBGaramond12-Regular.otf").into();
87 commands
88 .spawn((
89 Node {
90 margin: UiRect::all(px(12.0)),
91 position_type: PositionType::Absolute,
92 top: px(5.0),
93 right: px(5.0),
94 ..default()
95 },
96 Text::new("Opentype features:\n"),
97 TextFont {
98 font: opentype_font_handle.clone(),
99 font_size: FontSize::Px(32.0),
100 ..default()
101 },
102 ))
103 .with_children(|parent| {
104 let text_rows = [
105 ("Smallcaps: ", FontFeatureTag::SMALL_CAPS, "Hello World"),
106 (
107 "Ligatures: ",
108 FontFeatureTag::STANDARD_LIGATURES,
109 "fi fl ff ffi ffl",
110 ),
111 ("Fractions: ", FontFeatureTag::FRACTIONS, "12/134"),
112 ("Superscript: ", FontFeatureTag::SUPERSCRIPT, "Up here!"),
113 ("Subscript: ", FontFeatureTag::SUBSCRIPT, "Down here!"),
114 (
115 "Oldstyle figures: ",
116 FontFeatureTag::OLDSTYLE_FIGURES,
117 "1234567890",
118 ),
119 (
120 "Lining figures: ",
121 FontFeatureTag::LINING_FIGURES,
122 "1234567890",
123 ),
124 ];
125
126 for (title, feature, text) in text_rows {
127 parent.spawn((
128 TextSpan::new(title),
129 TextFont {
130 font: opentype_font_handle.clone(),
131 font_size: FontSize::Px(24.0),
132 ..default()
133 },
134 ));
135 parent.spawn((
136 TextSpan::new(format!("{text}\n")),
137 TextFont {
138 font: opentype_font_handle.clone(),
139 font_size: FontSize::Px(24.0),
140 font_features: FontFeatures::builder().enable(feature).build(),
141 ..default()
142 },
143 ));
144 }
145 });
146
147 #[cfg(feature = "default_font")]
148 commands.spawn((
149 // Here we are able to call the `From` method instead of creating a new `TextSection`.
150 // This will use the default font (a minimal subset of FiraMono) and apply the default styling.
151 Text::new("From an &str into a Text with the default font!"),
152 Node {
153 position_type: PositionType::Absolute,
154 bottom: px(5),
155 left: px(15),
156 ..default()
157 },
158 ));
159
160 #[cfg(not(feature = "default_font"))]
161 commands.spawn((
162 Text::new("Default font disabled"),
163 TextFont {
164 font: asset_server.load("fonts/FiraMono-Medium.ttf"),
165 ..default()
166 },
167 Node {
168 position_type: PositionType::Absolute,
169 bottom: px(5),
170 left: px(15),
171 ..default()
172 },
173 ));
174 }
175
176 fn text_color_system(time: Res<Time>, mut query: Query<&mut TextColor, With<AnimatedText>>) {
177 for mut text_color in &mut query {
178 let seconds = time.elapsed_secs();
179
180 // Update the color of the ColorText span.
181 text_color.0 = Color::srgb(
182 ops::sin(1.25 * seconds) / 2.0 + 0.5,
183 ops::sin(0.75 * seconds) / 2.0 + 0.5,
184 ops::sin(0.50 * seconds) / 2.0 + 0.5,
185 );
186 //t.set_changed();
187 }
188 }
189
190 fn text_update_system(
191 diagnostics: Res<DiagnosticsStore>,
192 mut query: Query<&mut TextSpan, With<FpsText>>,
193 ) {
194 for mut span in &mut query {
195 if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS)
196 && let Some(value) = fps.smoothed()
197 {
198 // Update the value of the second section
199 **span = format!("{value:.2}");
200 }
201 }
202 }