Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_buttons.rs
6592 views
1
//! General UI benchmark that stress tests layouting, text, interaction and rendering
2
3
use argh::FromArgs;
4
use bevy::{
5
color::palettes::css::ORANGE_RED,
6
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
7
prelude::*,
8
text::TextColor,
9
window::{PresentMode, WindowResolution},
10
winit::{UpdateMode, WinitSettings},
11
};
12
13
const FONT_SIZE: f32 = 7.0;
14
15
#[derive(FromArgs, Resource)]
16
/// `many_buttons` general UI benchmark that stress tests layouting, text, interaction and rendering
17
struct 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.
64
fn 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 {
88
focused_mode: UpdateMode::Continuous,
89
unfocused_mode: UpdateMode::Continuous,
90
})
91
.add_systems(Update, (button_system, set_text_colors_changed));
92
93
if !args.no_camera {
94
app.add_systems(Startup, |mut commands: Commands| {
95
commands.spawn(Camera2d);
96
});
97
}
98
99
if args.many_cameras {
100
app.add_systems(Startup, setup_many_cameras);
101
} else if args.grid {
102
app.add_systems(Startup, setup_grid);
103
} else {
104
app.add_systems(Startup, setup_flex);
105
}
106
107
if args.relayout {
108
app.add_systems(Update, |mut nodes: Query<&mut Node>| {
109
nodes.iter_mut().for_each(|mut node| node.set_changed());
110
});
111
}
112
113
if args.recompute_text {
114
app.add_systems(Update, |mut text_query: Query<&mut Text>| {
115
text_query
116
.iter_mut()
117
.for_each(|mut text| text.set_changed());
118
});
119
}
120
121
if args.respawn {
122
if args.grid {
123
app.add_systems(Update, (despawn_ui, setup_grid).chain());
124
} else {
125
app.add_systems(Update, (despawn_ui, setup_flex).chain());
126
}
127
}
128
129
app.insert_resource(args).run();
130
}
131
132
fn set_text_colors_changed(mut colors: Query<&mut TextColor>) {
133
for mut text_color in colors.iter_mut() {
134
text_color.set_changed();
135
}
136
}
137
138
#[derive(Component)]
139
struct IdleColor(Color);
140
141
fn button_system(
142
mut interaction_query: Query<
143
(&Interaction, &mut BackgroundColor, &IdleColor),
144
Changed<Interaction>,
145
>,
146
) {
147
for (interaction, mut color, &IdleColor(idle_color)) in interaction_query.iter_mut() {
148
*color = match interaction {
149
Interaction::Hovered => ORANGE_RED.into(),
150
_ => idle_color.into(),
151
};
152
}
153
}
154
155
fn 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
);
206
}
207
});
208
}
209
});
210
}
211
212
fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
213
let images = if 0 < args.image_freq {
214
Some(vec![
215
asset_server.load("branding/icon.png"),
216
asset_server.load("textures/Game Icons/wrench.png"),
217
])
218
} else {
219
None
220
};
221
222
let buttons_f = args.buttons as f32;
223
let border = if args.no_borders {
224
UiRect::ZERO
225
} else {
226
UiRect::all(vmin(0.05 * 90. / buttons_f))
227
};
228
229
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
230
commands
231
.spawn(Node {
232
display: if args.display_none {
233
Display::None
234
} else {
235
Display::Grid
236
},
237
width: percent(100),
238
height: percent(100),
239
grid_template_columns: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
240
grid_template_rows: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
241
..default()
242
})
243
.with_children(|commands| {
244
for column in 0..args.buttons {
245
for row in 0..args.buttons {
246
let color = as_rainbow(row % column.max(1));
247
let border_color = Color::WHITE.with_alpha(0.5).into();
248
spawn_button(
249
commands,
250
color,
251
buttons_f,
252
column,
253
row,
254
args.text,
255
border,
256
border_color,
257
images.as_ref().map(|images| {
258
images[((column + row) / args.image_freq) % images.len()].clone()
259
}),
260
);
261
}
262
}
263
});
264
}
265
266
fn spawn_button(
267
commands: &mut ChildSpawnerCommands,
268
background_color: Color,
269
buttons: f32,
270
column: usize,
271
row: usize,
272
spawn_text: bool,
273
border: UiRect,
274
border_color: BorderColor,
275
image: Option<Handle<Image>>,
276
) {
277
let width = vw(90.0 / buttons);
278
let height = vh(90.0 / buttons);
279
let margin = UiRect::axes(width * 0.05, height * 0.05);
280
let mut builder = commands.spawn((
281
Button,
282
Node {
283
width,
284
height,
285
margin,
286
align_items: AlignItems::Center,
287
justify_content: JustifyContent::Center,
288
border,
289
..default()
290
},
291
BackgroundColor(background_color),
292
border_color,
293
IdleColor(background_color),
294
));
295
296
if let Some(image) = image {
297
builder.insert(ImageNode::new(image));
298
}
299
300
if spawn_text {
301
builder.with_children(|parent| {
302
// These labels are split to stress test multi-span text
303
parent
304
.spawn((
305
Text(format!("{column}, ")),
306
TextFont {
307
font_size: FONT_SIZE,
308
..default()
309
},
310
TextColor(Color::srgb(0.5, 0.2, 0.2)),
311
))
312
.with_child((
313
TextSpan(format!("{row}")),
314
TextFont {
315
font_size: FONT_SIZE,
316
..default()
317
},
318
TextColor(Color::srgb(0.2, 0.2, 0.5)),
319
));
320
});
321
}
322
}
323
324
fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<ChildOf>)>) {
325
commands.entity(*root_node).despawn();
326
}
327
328
fn setup_many_cameras(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
329
let images = if 0 < args.image_freq {
330
Some(vec![
331
asset_server.load("branding/icon.png"),
332
asset_server.load("textures/Game Icons/wrench.png"),
333
])
334
} else {
335
None
336
};
337
338
let buttons_f = args.buttons as f32;
339
let border = if args.no_borders {
340
UiRect::ZERO
341
} else {
342
UiRect::all(vmin(0.05 * 90. / buttons_f))
343
};
344
345
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
346
for column in 0..args.buttons {
347
for row in 0..args.buttons {
348
let color = as_rainbow(row % column.max(1));
349
let border_color = Color::WHITE.with_alpha(0.5).into();
350
let camera = commands
351
.spawn((
352
Camera2d,
353
Camera {
354
order: (column * args.buttons + row) as isize + 1,
355
..Default::default()
356
},
357
))
358
.id();
359
commands
360
.spawn((
361
Node {
362
display: if args.display_none {
363
Display::None
364
} else {
365
Display::Flex
366
},
367
flex_direction: FlexDirection::Column,
368
justify_content: JustifyContent::Center,
369
align_items: AlignItems::Center,
370
width: percent(100),
371
height: percent(100),
372
..default()
373
},
374
UiTargetCamera(camera),
375
))
376
.with_children(|commands| {
377
commands
378
.spawn(Node {
379
position_type: PositionType::Absolute,
380
top: vh(column as f32 * 100. / buttons_f),
381
left: vw(row as f32 * 100. / buttons_f),
382
..Default::default()
383
})
384
.with_children(|commands| {
385
spawn_button(
386
commands,
387
color,
388
buttons_f,
389
column,
390
row,
391
args.text,
392
border,
393
border_color,
394
images.as_ref().map(|images| {
395
images[((column + row) / args.image_freq) % images.len()]
396
.clone()
397
}),
398
);
399
});
400
});
401
}
402
}
403
}
404
405