many_buttons/
many_buttons.rs

1//! General UI benchmark that stress tests layouting, text, interaction and rendering
2
3use argh::FromArgs;
4use bevy::{
5    color::palettes::css::ORANGE_RED,
6    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
7    prelude::*,
8    text::TextColor,
9    window::{PresentMode, WindowResolution},
10    winit::WinitSettings,
11};
12
13const FONT_SIZE: f32 = 7.0;
14
15#[derive(FromArgs, Resource)]
16/// `many_buttons` general UI benchmark that stress tests layouting, text, interaction and rendering
17struct Args {
18    /// whether to add labels to each button
19    #[argh(switch)]
20    text: bool,
21
22    /// whether to add borders to each button
23    #[argh(switch)]
24    no_borders: bool,
25
26    /// whether to perform a full relayout each frame
27    #[argh(switch)]
28    relayout: bool,
29
30    /// whether to recompute all text each frame (if text enabled)
31    #[argh(switch)]
32    recompute_text: bool,
33
34    /// how many buttons per row and column of the grid.
35    #[argh(option, default = "110")]
36    buttons: usize,
37
38    /// change the button icon every nth button, if `0` no icons are added.
39    #[argh(option, default = "4")]
40    image_freq: usize,
41
42    /// use the grid layout model
43    #[argh(switch)]
44    grid: bool,
45
46    /// at the start of each frame despawn any existing UI nodes and spawn a new UI tree
47    #[argh(switch)]
48    respawn: bool,
49
50    /// set the root node to display none, removing all nodes from the layout.
51    #[argh(switch)]
52    display_none: bool,
53
54    /// spawn the layout without a camera
55    #[argh(switch)]
56    no_camera: bool,
57
58    /// a layout with a separate camera for each button
59    #[argh(switch)]
60    many_cameras: bool,
61}
62
63/// This example shows what happens when there is a lot of buttons on screen.
64fn main() {
65    // `from_env` panics on the web
66    #[cfg(not(target_arch = "wasm32"))]
67    let args: Args = argh::from_env();
68    #[cfg(target_arch = "wasm32")]
69    let args = Args::from_args(&[], &[]).unwrap();
70
71    warn!(include_str!("warning_string.txt"));
72
73    let mut app = App::new();
74
75    app.add_plugins((
76        DefaultPlugins.set(WindowPlugin {
77            primary_window: Some(Window {
78                present_mode: PresentMode::AutoNoVsync,
79                resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
80                ..default()
81            }),
82            ..default()
83        }),
84        FrameTimeDiagnosticsPlugin::default(),
85        LogDiagnosticsPlugin::default(),
86    ))
87    .insert_resource(WinitSettings::continuous())
88    .add_systems(Update, (button_system, set_text_colors_changed));
89
90    if !args.no_camera {
91        app.add_systems(Startup, |mut commands: Commands| {
92            commands.spawn(Camera2d);
93        });
94    }
95
96    if args.many_cameras {
97        app.add_systems(Startup, setup_many_cameras);
98    } else if args.grid {
99        app.add_systems(Startup, setup_grid);
100    } else {
101        app.add_systems(Startup, setup_flex);
102    }
103
104    if args.relayout {
105        app.add_systems(Update, |mut nodes: Query<&mut Node>| {
106            nodes.iter_mut().for_each(|mut node| node.set_changed());
107        });
108    }
109
110    if args.recompute_text {
111        app.add_systems(Update, |mut text_query: Query<&mut Text>| {
112            text_query
113                .iter_mut()
114                .for_each(|mut text| text.set_changed());
115        });
116    }
117
118    if args.respawn {
119        if args.grid {
120            app.add_systems(Update, (despawn_ui, setup_grid).chain());
121        } else {
122            app.add_systems(Update, (despawn_ui, setup_flex).chain());
123        }
124    }
125
126    app.insert_resource(args).run();
127}
128
129fn set_text_colors_changed(mut colors: Query<&mut TextColor>) {
130    for mut text_color in colors.iter_mut() {
131        text_color.set_changed();
132    }
133}
134
135#[derive(Component)]
136struct IdleColor(Color);
137
138fn button_system(
139    mut interaction_query: Query<
140        (&Interaction, &mut BackgroundColor, &IdleColor),
141        Changed<Interaction>,
142    >,
143) {
144    for (interaction, mut color, &IdleColor(idle_color)) in interaction_query.iter_mut() {
145        *color = match interaction {
146            Interaction::Hovered => ORANGE_RED.into(),
147            _ => idle_color.into(),
148        };
149    }
150}
151
152fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
153    let images = if 0 < args.image_freq {
154        Some(vec![
155            asset_server.load("branding/icon.png"),
156            asset_server.load("textures/Game Icons/wrench.png"),
157        ])
158    } else {
159        None
160    };
161
162    let buttons_f = args.buttons as f32;
163    let border = if args.no_borders {
164        UiRect::ZERO
165    } else {
166        UiRect::all(vmin(0.05 * 90. / buttons_f))
167    };
168
169    let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
170    commands
171        .spawn(Node {
172            display: if args.display_none {
173                Display::None
174            } else {
175                Display::Flex
176            },
177            flex_direction: FlexDirection::Column,
178            justify_content: JustifyContent::Center,
179            align_items: AlignItems::Center,
180            width: percent(100),
181            height: percent(100),
182            ..default()
183        })
184        .with_children(|commands| {
185            for column in 0..args.buttons {
186                commands.spawn(Node::default()).with_children(|commands| {
187                    for row in 0..args.buttons {
188                        let color = as_rainbow(row % column.max(1));
189                        let border_color = Color::WHITE.with_alpha(0.5).into();
190                        spawn_button(
191                            commands,
192                            color,
193                            buttons_f,
194                            column,
195                            row,
196                            args.text,
197                            border,
198                            border_color,
199                            images.as_ref().map(|images| {
200                                images[((column + row) / args.image_freq) % images.len()].clone()
201                            }),
202                        );
203                    }
204                });
205            }
206        });
207}
208
209fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
210    let images = if 0 < args.image_freq {
211        Some(vec![
212            asset_server.load("branding/icon.png"),
213            asset_server.load("textures/Game Icons/wrench.png"),
214        ])
215    } else {
216        None
217    };
218
219    let buttons_f = args.buttons as f32;
220    let border = if args.no_borders {
221        UiRect::ZERO
222    } else {
223        UiRect::all(vmin(0.05 * 90. / buttons_f))
224    };
225
226    let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
227    commands
228        .spawn(Node {
229            display: if args.display_none {
230                Display::None
231            } else {
232                Display::Grid
233            },
234            width: percent(100),
235            height: percent(100),
236            grid_template_columns: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
237            grid_template_rows: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
238            ..default()
239        })
240        .with_children(|commands| {
241            for column in 0..args.buttons {
242                for row in 0..args.buttons {
243                    let color = as_rainbow(row % column.max(1));
244                    let border_color = Color::WHITE.with_alpha(0.5).into();
245                    spawn_button(
246                        commands,
247                        color,
248                        buttons_f,
249                        column,
250                        row,
251                        args.text,
252                        border,
253                        border_color,
254                        images.as_ref().map(|images| {
255                            images[((column + row) / args.image_freq) % images.len()].clone()
256                        }),
257                    );
258                }
259            }
260        });
261}
262
263fn spawn_button(
264    commands: &mut ChildSpawnerCommands,
265    background_color: Color,
266    buttons: f32,
267    column: usize,
268    row: usize,
269    spawn_text: bool,
270    border: UiRect,
271    border_color: BorderColor,
272    image: Option<Handle<Image>>,
273) {
274    let width = vw(90.0 / buttons);
275    let height = vh(90.0 / buttons);
276    let margin = UiRect::axes(width * 0.05, height * 0.05);
277    let mut builder = commands.spawn((
278        Button,
279        Node {
280            width,
281            height,
282            margin,
283            align_items: AlignItems::Center,
284            justify_content: JustifyContent::Center,
285            border,
286            ..default()
287        },
288        BackgroundColor(background_color),
289        border_color,
290        IdleColor(background_color),
291    ));
292
293    if let Some(image) = image {
294        builder.insert(ImageNode::new(image));
295    }
296
297    if spawn_text {
298        builder.with_children(|parent| {
299            // These labels are split to stress test multi-span text
300            parent
301                .spawn((
302                    Text(format!("{column}, ")),
303                    TextFont {
304                        font_size: FONT_SIZE,
305                        ..default()
306                    },
307                    TextColor(Color::srgb(0.5, 0.2, 0.2)),
308                ))
309                .with_child((
310                    TextSpan(format!("{row}")),
311                    TextFont {
312                        font_size: FONT_SIZE,
313                        ..default()
314                    },
315                    TextColor(Color::srgb(0.2, 0.2, 0.5)),
316                ));
317        });
318    }
319}
320
321fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<ChildOf>)>) {
322    commands.entity(*root_node).despawn();
323}
324
325fn setup_many_cameras(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
326    let images = if 0 < args.image_freq {
327        Some(vec![
328            asset_server.load("branding/icon.png"),
329            asset_server.load("textures/Game Icons/wrench.png"),
330        ])
331    } else {
332        None
333    };
334
335    let buttons_f = args.buttons as f32;
336    let border = if args.no_borders {
337        UiRect::ZERO
338    } else {
339        UiRect::all(vmin(0.05 * 90. / buttons_f))
340    };
341
342    let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
343    for column in 0..args.buttons {
344        for row in 0..args.buttons {
345            let color = as_rainbow(row % column.max(1));
346            let border_color = Color::WHITE.with_alpha(0.5).into();
347            let camera = commands
348                .spawn((
349                    Camera2d,
350                    Camera {
351                        order: (column * args.buttons + row) as isize + 1,
352                        ..Default::default()
353                    },
354                ))
355                .id();
356            commands
357                .spawn((
358                    Node {
359                        display: if args.display_none {
360                            Display::None
361                        } else {
362                            Display::Flex
363                        },
364                        flex_direction: FlexDirection::Column,
365                        justify_content: JustifyContent::Center,
366                        align_items: AlignItems::Center,
367                        width: percent(100),
368                        height: percent(100),
369                        ..default()
370                    },
371                    UiTargetCamera(camera),
372                ))
373                .with_children(|commands| {
374                    commands
375                        .spawn(Node {
376                            position_type: PositionType::Absolute,
377                            top: vh(column as f32 * 100. / buttons_f),
378                            left: vw(row as f32 * 100. / buttons_f),
379                            ..Default::default()
380                        })
381                        .with_children(|commands| {
382                            spawn_button(
383                                commands,
384                                color,
385                                buttons_f,
386                                column,
387                                row,
388                                args.text,
389                                border,
390                                border_color,
391                                images.as_ref().map(|images| {
392                                    images[((column + row) / args.image_freq) % images.len()]
393                                        .clone()
394                                }),
395                            );
396                        });
397                });
398        }
399    }
400}