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
41fn main() {
42    let mut app = App::new();
43
44    app.add_plugins((
45        DefaultPlugins.set(WindowPlugin {
46            primary_window: Some(Window {
47                present_mode: PresentMode::AutoNoVsync,
48                resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
49                ..default()
50            }),
51            ..default()
52        }),
53        FrameTimeDiagnosticsPlugin::default(),
54        LogDiagnosticsPlugin::default(),
55    ))
56    .insert_resource(WinitSettings::continuous())
57    .add_systems(Startup, (setup_camera, setup_text));
58
59    // `from_env` panics on the web
60    #[cfg(not(target_arch = "wasm32"))]
61    let args: Args = argh::from_env();
62    #[cfg(target_arch = "wasm32")]
63    let args = Args::from_args(&[], &[]).unwrap();
64
65    if args.set_font_changed {
66        app.add_systems(Update, set_changed::<TextFont>);
67    }
68
69    if args.respawn {
70        app.add_systems(Update, (despawn_layout, setup_text).chain());
71    }
72
73    if args.clear_font_atlases {
74        app.add_systems(Update, clear_all_font_atlases);
75    }
76
77    app.add_systems(Update, update_lorem_text);
78
79    app.add_systems(Update, update_number_text);
80
81    app.run();
82}
83
84#[derive(Component)]
85struct ManyTextRoot;
86
87fn setup_camera(mut commands: Commands) {
88    warn!(include_str!("warning_string.txt"));
89    commands.spawn(Camera2d);
90}
91
92fn setup_text(mut commands: Commands, asset_server: Res<AssetServer>) {
93    commands
94        .spawn((
95            Node {
96                display: Display::Grid,
97                grid_template_columns: RepeatedGridTrack::flex(4, 1.0),
98                width: percent(100),
99                height: percent(100),
100                column_gap: px(2.),
101                ..default()
102            },
103            ManyTextRoot,
104        ))
105        .with_children(|parent| {
106            for font_path in [
107                "fonts/EBGaramond12-Regular.otf",
108                "fonts/FiraMono-Medium.ttf",
109                "fonts/FiraSans-Bold.ttf",
110                "fonts/MonaSans-VariableFont.ttf",
111            ] {
112                let font = asset_server.load(font_path);
113                let text_font = TextFont {
114                    font: font.into(),
115                    font_size: px(10).into(),
116                    ..default()
117                };
118                parent
119                    .spawn((
120                        Node {
121                            flex_direction: FlexDirection::Column,
122                            border: px(1.).all(),
123                            padding: px(1.).all(),
124                            row_gap: px(2.),
125                            ..default()
126                        },
127                        BorderColor::all(Color::WHITE),
128                    ))
129                    .with_children(|parent| {
130                        parent.spawn((Text(font_path.to_string()), text_font.clone()));
131                        for justify in [
132                            Justify::Left,
133                            Justify::Center,
134                            Justify::Right,
135                            Justify::Justified,
136                        ] {
137                            for linebreak in [LineBreak::AnyCharacter, LineBreak::WordBoundary] {
138                                parent.spawn((
139                                    Text(format!("Justify::{justify:?}, LineBreak::{linebreak:?}")),
140                                    text_font.clone(),
141                                    TextColor::from(bevy::color::palettes::css::YELLOW),
142                                ));
143                                let layout = TextLayout { justify, linebreak };
144                                parent.spawn((
145                                    Text::new(LOREM_TEXT_1),
146                                    Lorem(false),
147                                    layout,
148                                    text_font.clone().with_font_size(10.),
149                                    TextColor::from(bevy::color::palettes::css::NAVY),
150                                ));
151                                parent.spawn((
152                                    Text::new(LOREM_TEXT_2),
153                                    Lorem(true),
154                                    layout,
155                                    text_font.clone().with_font_size(11.),
156                                    TextColor::from(bevy::color::palettes::css::PALE_GREEN),
157                                ));
158                            }
159                        }
160
161                        parent
162                            .spawn((
163                                Text::new(LOREM_TEXT_1),
164                                Lorem(false),
165                                text_font.clone().with_font_size(12.),
166                                TextColor::from(bevy::color::palettes::css::MISTY_ROSE),
167                            ))
168                            .with_child((TextSpan::new(" "), text_font.clone().with_font_size(13.)))
169                            .with_child((
170                                TextSpan::new(LOREM_TEXT_2),
171                                Lorem(true),
172                                text_font.clone().with_font_size(13.),
173                                TextColor::from(bevy::color::palettes::css::MAROON),
174                            ));
175
176                        parent
177                            .spawn((
178                                Text::default(),
179                                TextLayout::linebreak(LineBreak::AnyCharacter),
180                            ))
181                            .with_children(|parent| {
182                                for i in (0..10).cycle().take(100) {
183                                    parent.spawn((
184                                        TextSpan(i.to_string()),
185                                        text_font.clone().with_font_size((6 + i) as f32),
186                                        NumberSpan,
187                                    ));
188                                }
189                            });
190                    });
191            }
192        });
193}
194
195fn set_changed<C: Component<Mutability = Mutable>>(mut component_query: Query<&mut C>) {
196    for mut component in &mut component_query {
197        component.set_changed();
198    }
199}
200
201fn despawn_layout(mut commands: Commands, root_node: Single<Entity, With<ManyTextRoot>>) {
202    commands.entity(*root_node).despawn();
203}
204
205fn clear_all_font_atlases(mut font_atlases: ResMut<FontAtlasSet>) {
206    font_atlases.clear();
207}
208
209fn update_lorem_text(mut lorem_text_query: Query<(&mut Text, &mut Lorem)>) {
210    for (mut text, mut lorem) in &mut lorem_text_query {
211        if lorem.0 {
212            text.0.clear();
213            text.0.push_str(LOREM_TEXT_1);
214        } else {
215            text.0.clear();
216            text.0.push_str(LOREM_TEXT_2);
217        }
218
219        lorem.0 = !lorem.0;
220    }
221}
222
223fn update_number_text(mut n: Local<u32>, mut number_spans: Query<&mut TextSpan, With<NumberSpan>>) {
224    for mut text in &mut number_spans {
225        text.0 = format!("{}", (text.0.parse::<u32>().unwrap() + *n) % 10);
226    }
227
228    *n = (*n + 1) % 10;
229}