Skip to main content

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