Skip to main content

many_text/
many_text.rs

1//! UI text benchmark
2
3use argh::FromArgs;
4use bevy::{
5    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
6    ecs::component::Mutable,
7    prelude::*,
8    text::FontAtlasSet,
9    window::{PresentMode, WindowResolution},
10    winit::WinitSettings,
11};
12
13const LOREM_TEXT_1: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do \
14eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis \
15nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.";
16const LOREM_TEXT_2: &str = "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \
17Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
18
19#[derive(Component)]
20struct Lorem(bool);
21
22#[derive(Component)]
23struct NumberSpan;
24
25#[derive(FromArgs, Resource)]
26/// `many_text` UI text stress test
27struct Args {
28    /// whether to set the font changed each frame
29    #[argh(switch)]
30    set_font_changed: bool,
31
32    /// at the start of each frame despawn any existing UI nodes and spawn a new UI tree
33    #[argh(switch)]
34    respawn: bool,
35
36    /// at the start of each frame clear all font atlases
37    #[argh(switch)]
38    clear_font_atlases: bool,
39
40    /// update the text each frame
41    #[argh(switch)]
42    animate: bool,
43}
44
45fn main() {
46    let mut app = App::new();
47
48    app.add_plugins((
49        DefaultPlugins.set(WindowPlugin {
50            primary_window: Some(Window {
51                present_mode: PresentMode::AutoNoVsync,
52                resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
53                ..default()
54            }),
55            ..default()
56        }),
57        FrameTimeDiagnosticsPlugin::default(),
58        LogDiagnosticsPlugin::default(),
59    ))
60    .insert_resource(WinitSettings::continuous())
61    .add_systems(Startup, (setup_camera, setup_text));
62
63    // `from_env` panics on the web
64    #[cfg(not(target_arch = "wasm32"))]
65    let args: Args = argh::from_env();
66    #[cfg(target_arch = "wasm32")]
67    let args = Args::from_args(&[], &[]).unwrap();
68
69    if args.set_font_changed {
70        app.add_systems(Update, set_changed::<TextFont>);
71    }
72
73    if args.respawn {
74        app.add_systems(Update, (despawn_layout, setup_text).chain());
75    }
76
77    if args.clear_font_atlases {
78        app.add_systems(Update, clear_all_font_atlases);
79    }
80
81    if args.animate {
82        app.add_systems(Update, update_lorem_text);
83        app.add_systems(Update, update_number_text);
84    }
85
86    app.run();
87}
88
89#[derive(Component)]
90struct ManyTextRoot;
91
92fn setup_camera(mut commands: Commands) {
93    warn!(include_str!("warning_string.txt"));
94    commands.spawn(Camera2d);
95}
96
97fn setup_text(mut commands: Commands, asset_server: Res<AssetServer>) {
98    commands
99        .spawn((
100            Node {
101                display: Display::Grid,
102                grid_template_columns: RepeatedGridTrack::flex(4, 1.0),
103                width: percent(100),
104                height: percent(100),
105                column_gap: px(2.),
106                ..default()
107            },
108            ManyTextRoot,
109        ))
110        .with_children(|parent| {
111            for font_path in [
112                "fonts/EBGaramond12-Regular.otf",
113                "fonts/FiraMono-Medium.ttf",
114                "fonts/FiraSans-Bold.ttf",
115                "fonts/MonaSans-VariableFont.ttf",
116            ] {
117                let font = asset_server.load(font_path);
118                let text_font = TextFont {
119                    font: font.into(),
120                    font_size: px(10).into(),
121                    ..default()
122                };
123                parent
124                    .spawn((
125                        Node {
126                            flex_direction: FlexDirection::Column,
127                            border: px(1.).all(),
128                            padding: px(1.).all(),
129                            row_gap: px(2.),
130                            ..default()
131                        },
132                        BorderColor::all(Color::WHITE),
133                    ))
134                    .with_children(|parent| {
135                        parent.spawn((Text(font_path.to_string()), text_font.clone()));
136                        for justify in [
137                            Justify::Left,
138                            Justify::Center,
139                            Justify::Right,
140                            Justify::Justified,
141                        ] {
142                            for linebreak in [LineBreak::AnyCharacter, LineBreak::WordBoundary] {
143                                parent.spawn((
144                                    Text(format!("Justify::{justify:?}, LineBreak::{linebreak:?}")),
145                                    text_font.clone(),
146                                    TextColor::from(bevy::color::palettes::css::YELLOW),
147                                ));
148                                let layout = TextLayout { justify, linebreak };
149                                parent.spawn((
150                                    Text::new(LOREM_TEXT_1),
151                                    Lorem(false),
152                                    layout,
153                                    text_font.clone().with_font_size(10.),
154                                    TextColor::from(bevy::color::palettes::css::NAVY),
155                                ));
156                                parent.spawn((
157                                    Text::new(LOREM_TEXT_2),
158                                    Lorem(true),
159                                    layout,
160                                    text_font.clone().with_font_size(11.),
161                                    TextColor::from(bevy::color::palettes::css::PALE_GREEN),
162                                ));
163                            }
164                        }
165
166                        parent
167                            .spawn((
168                                Text::new(LOREM_TEXT_1),
169                                Lorem(false),
170                                text_font.clone().with_font_size(12.),
171                                TextColor::from(bevy::color::palettes::css::MISTY_ROSE),
172                            ))
173                            .with_child((TextSpan::new(" "), text_font.clone().with_font_size(13.)))
174                            .with_child((
175                                TextSpan::new(LOREM_TEXT_2),
176                                Lorem(true),
177                                text_font.clone().with_font_size(13.),
178                                TextColor::from(bevy::color::palettes::css::MAROON),
179                            ));
180
181                        parent
182                            .spawn((
183                                Text::default(),
184                                TextLayout::linebreak(LineBreak::AnyCharacter),
185                            ))
186                            .with_children(|parent| {
187                                for i in (0..10).cycle().take(100) {
188                                    parent.spawn((
189                                        TextSpan(i.to_string()),
190                                        text_font.clone().with_font_size((6 + i) as f32),
191                                        NumberSpan,
192                                    ));
193                                }
194                            });
195                    });
196            }
197        });
198}
199
200fn set_changed<C: Component<Mutability = Mutable>>(mut component_query: Query<&mut C>) {
201    for mut component in &mut component_query {
202        component.set_changed();
203    }
204}
205
206fn despawn_layout(mut commands: Commands, root_node: Single<Entity, With<ManyTextRoot>>) {
207    commands.entity(*root_node).despawn();
208}
209
210fn clear_all_font_atlases(mut font_atlases: ResMut<FontAtlasSet>) {
211    font_atlases.clear();
212}
213
214fn update_lorem_text(mut lorem_text_query: Query<(&mut Text, &mut Lorem)>) {
215    for (mut text, mut lorem) in &mut lorem_text_query {
216        if lorem.0 {
217            text.0.clear();
218            text.0.push_str(LOREM_TEXT_1);
219        } else {
220            text.0.clear();
221            text.0.push_str(LOREM_TEXT_2);
222        }
223
224        lorem.0 = !lorem.0;
225    }
226}
227
228fn update_number_text(mut n: Local<u32>, mut number_spans: Query<&mut TextSpan, With<NumberSpan>>) {
229    for mut text in &mut number_spans {
230        text.0 = format!("{}", (text.0.parse::<u32>().unwrap() + *n) % 10);
231    }
232
233    *n = (*n + 1) % 10;
234}