text_debug/
text_debug.rs

1//! Shows various text layout options.
2
3use 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}