Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_buttons.rs
6849 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::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::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
129
fn 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)]
136
struct IdleColor(Color);
137
138
fn 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
152
fn 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
209
fn 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
263
fn 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
321
fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<ChildOf>)>) {
322
commands.entity(*root_node).despawn();
323
}
324
325
fn 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
}
401
402