1use std::{collections::VecDeque, time::Duration};
4
5use bevy::{
6 color::palettes::css::*,
7 diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
8 prelude::*,
9 ui::widget::TextUiWriter,
10 window::PresentMode,
11};
12
13fn main() {
14 App::new()
15 .add_plugins((
16 DefaultPlugins.set(WindowPlugin {
17 primary_window: Some(Window {
18 present_mode: PresentMode::AutoNoVsync,
19 ..default()
20 }),
21 ..default()
22 }),
23 FrameTimeDiagnosticsPlugin::default(),
24 ))
25 .add_systems(Startup, infotext_system)
26 .add_systems(Update, change_text_system)
27 .run();
28}
29
30#[derive(Component)]
31struct TextChanges;
32
33fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
34 let font = asset_server.load("fonts/FiraSans-Bold.ttf");
35 let background_color = MAROON.into();
36 commands.spawn(Camera2d);
37
38 let root_uinode = commands
39 .spawn(Node {
40 width: percent(100),
41 height: percent(100),
42 justify_content: JustifyContent::SpaceBetween,
43 ..default()
44 })
45 .id();
46
47 let left_column = commands
48 .spawn(Node {
49 flex_direction: FlexDirection::Column,
50 justify_content: JustifyContent::SpaceBetween,
51 align_items: AlignItems::Start,
52 flex_grow: 1.,
53 margin: UiRect::axes(px(15), px(5)),
54 ..default()
55 }).with_children(|builder| {
56 builder.spawn((
57 Text::new("This is\ntext with\nline breaks\nin the top left."),
58 TextFont {
59 font: font.clone(),
60 font_size: 25.0,
61 ..default()
62 },
63 BackgroundColor(background_color)
64 ));
65 builder.spawn((
66 Text::new(
67 "This text is right-justified. The `Justify` component controls the horizontal alignment of the lines of multi-line text relative to each other, and does not affect the text node's position in the UI layout.",
68 ),
69 TextFont {
70 font: font.clone(),
71 font_size: 25.0,
72 ..default()
73 },
74 TextColor(YELLOW.into()),
75 TextLayout::new_with_justify(Justify::Right),
76 Node {
77 max_width: px(300),
78 ..default()
79 },
80 BackgroundColor(background_color)
81 ));
82 builder.spawn((
83 Text::new(
84 "This\ntext has\nline breaks and also a set width in the bottom left."),
85 TextFont {
86 font: font.clone(),
87 font_size: 25.0,
88 ..default()
89 },
90 Node {
91 max_width: px(300),
92 ..default()
93 },
94 BackgroundColor(background_color)
95 )
96 );
97 }).id();
98
99 let right_column = commands
100 .spawn(Node {
101 flex_direction: FlexDirection::Column,
102 justify_content: JustifyContent::SpaceBetween,
103 align_items: AlignItems::End,
104 flex_grow: 1.,
105 margin: UiRect::axes(px(15), px(5)),
106 ..default()
107 })
108 .with_children(|builder| {
109 builder.spawn((
110 Text::new("This text is very long, has a limited width, is center-justified, is positioned in the top right and is also colored pink."),
111 TextFont {
112 font: font.clone(),
113 font_size: 33.0,
114 ..default()
115 },
116 TextColor(Color::srgb(0.8, 0.2, 0.7)),
117 TextLayout::new_with_justify(Justify::Center),
118 Node {
119 max_width: px(400),
120 ..default()
121 },
122 BackgroundColor(background_color),
123 ));
124
125 builder.spawn((
126 Text::new("This text is left-justified and is vertically positioned to distribute the empty space equally above and below it."),
127 TextFont {
128 font: font.clone(),
129 font_size: 29.0,
130 ..default()
131 },
132 TextColor(YELLOW.into()),
133 TextLayout::new_with_justify(Justify::Left),
134 Node {
135 max_width: px(300),
136 ..default()
137 },
138 BackgroundColor(background_color),
139 ));
140
141 builder.spawn((
142 Text::new("This text is fully justified and is positioned in the same way."),
143 TextFont {
144 font: font.clone(),
145 font_size: 29.0,
146 ..default()
147 },
148 TextLayout::new_with_justify(Justify::Justified),
149 TextColor(GREEN_YELLOW.into()),
150 Node {
151 max_width: px(300),
152 ..default()
153 },
154 BackgroundColor(background_color),
155 ));
156
157 builder
158 .spawn((
159 Text::default(),
160 TextFont {
161 font: font.clone(),
162 font_size: 21.0,
163 ..default()
164 },
165 TextChanges,
166 BackgroundColor(background_color),
167 ))
168 .with_children(|p| {
169 p.spawn((
170 TextSpan::new("\nThis text changes in the bottom right"),
171 TextFont {
172 font: font.clone(),
173 font_size: 21.0,
174 ..default()
175 },
176 ));
177 p.spawn((
178 TextSpan::new(" this text has zero font size"),
179 TextFont {
180 font: font.clone(),
181 font_size: 0.0,
182 ..default()
183 },
184 TextColor(BLUE.into()),
185 ));
186 p.spawn((
187 TextSpan::new("\nThis text changes in the bottom right - "),
188 TextFont {
189 font: font.clone(),
190 font_size: 21.0,
191 ..default()
192 },
193 TextColor(RED.into()),
194 ));
195 p.spawn((
196 TextSpan::default(),
197 TextFont {
198 font: font.clone(),
199 font_size: 21.0,
200 ..default()
201 },
202 TextColor(ORANGE_RED.into()),
203 ));
204 p.spawn((
205 TextSpan::new(" fps, "),
206 TextFont {
207 font: font.clone(),
208 font_size: 10.0,
209 ..default()
210 },
211 TextColor(YELLOW.into()),
212 ));
213 p.spawn((
214 TextSpan::default(),
215 TextFont {
216 font: font.clone(),
217 font_size: 21.0,
218 ..default()
219 },
220 TextColor(LIME.into()),
221 ));
222 p.spawn((
223 TextSpan::new(" ms/frame"),
224 TextFont {
225 font: font.clone(),
226 font_size: 42.0,
227 ..default()
228 },
229 TextColor(BLUE.into()),
230 ));
231 p.spawn((
232 TextSpan::new(" this text has negative font size"),
233 TextFont {
234 font: font.clone(),
235 font_size: -42.0,
236 ..default()
237 },
238 TextColor(BLUE.into()),
239 ));
240 });
241 })
242 .id();
243 commands
244 .entity(root_uinode)
245 .add_children(&[left_column, right_column]);
246}
247
248fn change_text_system(
249 mut fps_history: Local<VecDeque<f64>>,
250 mut time_history: Local<VecDeque<Duration>>,
251 time: Res<Time>,
252 diagnostics: Res<DiagnosticsStore>,
253 query: Query<Entity, With<TextChanges>>,
254 mut writer: TextUiWriter,
255) {
256 time_history.push_front(time.elapsed());
257 time_history.truncate(120);
258 let avg_fps = (time_history.len() as f64)
259 / (time_history.front().copied().unwrap_or_default()
260 - time_history.back().copied().unwrap_or_default())
261 .as_secs_f64()
262 .max(0.0001);
263 fps_history.push_front(avg_fps);
264 fps_history.truncate(120);
265 let fps_variance = std_deviation(fps_history.make_contiguous()).unwrap_or_default();
266
267 for entity in &query {
268 let mut fps = 0.0;
269 if let Some(fps_diagnostic) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS)
270 && let Some(fps_smoothed) = fps_diagnostic.smoothed()
271 {
272 fps = fps_smoothed;
273 }
274
275 let mut frame_time = time.delta_secs_f64();
276 if let Some(frame_time_diagnostic) =
277 diagnostics.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME)
278 && let Some(frame_time_smoothed) = frame_time_diagnostic.smoothed()
279 {
280 frame_time = frame_time_smoothed;
281 }
282
283 *writer.text(entity, 0) =
284 format!("{avg_fps:.1} avg fps, {fps_variance:.1} frametime variance",);
285
286 *writer.text(entity, 1) = format!(
287 "\nThis text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame",
288 );
289
290 *writer.text(entity, 4) = format!("{fps:.1}");
291
292 *writer.text(entity, 6) = format!("{frame_time:.3}");
293 }
294}
295
296fn mean(data: &[f64]) -> Option<f64> {
297 let sum = data.iter().sum::<f64>();
298 let count = data.len();
299
300 match count {
301 positive if positive > 0 => Some(sum / count as f64),
302 _ => None,
303 }
304}
305
306fn std_deviation(data: &[f64]) -> Option<f64> {
307 match (mean(data), data.len()) {
308 (Some(data_mean), count) if count > 0 => {
309 let variance = data
310 .iter()
311 .map(|value| {
312 let diff = data_mean - *value;
313
314 diff * diff
315 })
316 .sum::<f64>()
317 / count as f64;
318
319 Some(variance.sqrt())
320 }
321 _ => None,
322 }
323}