diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 9cd125ac0d0dc..1095076e16fc6 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -1,29 +1,48 @@ -//! This example shows what happens when there is a lot of buttons on screen. -//! -//! To start the demo without text run -//! `cargo run --example many_buttons --release no-text` -//! -//! //! To start the demo without borders run -//! `cargo run --example many_buttons --release no-borders` -//! -//| To do a full layout update each frame run -//! `cargo run --example many_buttons --release recompute-layout` -//! -//! To recompute all text each frame run -//! `cargo run --example many_buttons --release recompute-text` - +/// General UI benchmark that stress tests layouting, text, interaction and rendering +use argh::FromArgs; use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, window::{PresentMode, WindowPlugin}, }; -// For a total of 110 * 110 = 12100 buttons with text -const ROW_COLUMN_COUNT: usize = 110; const FONT_SIZE: f32 = 7.0; +#[derive(FromArgs, Resource)] +/// `many_buttons` general UI benchmark that stress tests layouting, text, interaction and rendering +struct Args { + /// whether to add text to each button + #[argh(switch)] + no_text: bool, + + /// whether to add borders to each button + #[argh(switch)] + no_borders: bool, + + /// whether to perform a full relayout each frame + #[argh(switch)] + relayout: bool, + + /// whether to recompute all text each frame + #[argh(switch)] + recompute_text: bool, + + /// how many buttons per row and column of the grid. + #[argh(option, default = "110")] + buttons: usize, + + /// give every nth button an image + #[argh(option, default = "4")] + image_freq: usize, + + /// use the grid layout model + #[argh(switch)] + grid: bool, +} + /// This example shows what happens when there is a lot of buttons on screen. fn main() { + let args: Args = argh::from_env(); let mut app = App::new(); app.add_plugins(( @@ -37,22 +56,27 @@ fn main() { FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin::default(), )) - .add_systems(Startup, setup) .add_systems(Update, button_system); - if std::env::args().any(|arg| arg == "recompute-layout") { - app.add_systems(Update, |mut ui_scale: ResMut| { - ui_scale.set_changed(); + if args.grid { + app.add_systems(Startup, setup_grid); + } else { + app.add_systems(Startup, setup_flex); + } + + if args.relayout { + app.add_systems(Update, |mut style_query: Query<&mut Style>| { + style_query.for_each_mut(|mut style| style.set_changed()); }); } - if std::env::args().any(|arg| arg == "recompute-text") { + if args.recompute_text { app.add_systems(Update, |mut text_query: Query<&mut Text>| { text_query.for_each_mut(|mut text| text.set_changed()); }); } - app.run(); + app.insert_resource(args).run(); } #[derive(Component)] @@ -64,50 +88,117 @@ fn button_system( Changed, >, ) { - for (interaction, mut material, IdleColor(idle_color)) in interaction_query.iter_mut() { - if matches!(interaction, Interaction::Hovered) { - *material = Color::ORANGE_RED.into(); - } else { - *material = *idle_color; - } + for (interaction, mut button_color, IdleColor(idle_color)) in interaction_query.iter_mut() { + *button_color = match interaction { + Interaction::Hovered => Color::ORANGE_RED.into(), + _ => *idle_color, + }; } } -fn setup(mut commands: Commands) { +fn setup_flex(mut commands: Commands, asset_server: Res, args: Res) { + warn!(include_str!("warning_string.txt")); + let image = if 0 < args.image_freq { + Some(asset_server.load("branding/icon.png")) + } else { + None + }; + + let buttons_f = args.buttons as f32; + let border = if args.no_borders { + UiRect::ZERO + } else { + UiRect::all(Val::VMin(0.05 * 90. / buttons_f)) + }; + + let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8); + commands.spawn(Camera2dBundle::default()); + commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Percent(100.), + ..default() + }, + ..default() + }) + .with_children(|commands| { + for column in 0..args.buttons { + commands + .spawn(NodeBundle::default()) + .with_children(|commands| { + for row in 0..args.buttons { + let color = as_rainbow(row % column.max(1)).into(); + let border_color = Color::WHITE.with_a(0.5).into(); + spawn_button( + commands, + color, + buttons_f, + column, + row, + !args.no_text, + border, + border_color, + image + .as_ref() + .filter(|_| (column + row) % args.image_freq == 0) + .cloned(), + ); + } + }); + } + }); +} + +fn setup_grid(mut commands: Commands, asset_server: Res, args: Res) { warn!(include_str!("warning_string.txt")); + let image = if 0 < args.image_freq { + Some(asset_server.load("branding/icon.png")) + } else { + None + }; + + let buttons_f = args.buttons as f32; + let border = if args.no_borders { + UiRect::ZERO + } else { + UiRect::all(Val::VMin(0.05 * 90. / buttons_f)) + }; - let count = ROW_COLUMN_COUNT; - let count_f = count as f32; - let as_rainbow = |i: usize| Color::hsl((i as f32 / count_f) * 360.0, 0.9, 0.8); + let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8); commands.spawn(Camera2dBundle::default()); commands .spawn(NodeBundle { style: Style { + display: Display::Grid, width: Val::Percent(100.), + height: Val::Percent(100.0), + grid_template_columns: RepeatedGridTrack::flex(args.buttons as u16, 1.0), + grid_template_rows: RepeatedGridTrack::flex(args.buttons as u16, 1.0), ..default() }, ..default() }) .with_children(|commands| { - let spawn_text = std::env::args().all(|arg| arg != "no-text"); - let border = if std::env::args().all(|arg| arg != "no-borders") { - UiRect::all(Val::Percent(10. / count_f)) - } else { - UiRect::DEFAULT - }; - for i in 0..count { - for j in 0..count { - let color = as_rainbow(j % i.max(1)).into(); - let border_color = as_rainbow(i % j.max(1)).into(); + for column in 0..args.buttons { + for row in 0..args.buttons { + let color = as_rainbow(row % column.max(1)).into(); + let border_color = Color::WHITE.with_a(0.5).into(); spawn_button( commands, color, - count_f, - i, - j, - spawn_text, + buttons_f, + column, + row, + !args.no_text, border, border_color, + image + .as_ref() + .filter(|_| (column + row) % args.image_freq == 0) + .cloned(), ); } } @@ -118,23 +209,25 @@ fn setup(mut commands: Commands) { fn spawn_button( commands: &mut ChildBuilder, background_color: BackgroundColor, - total: f32, - i: usize, - j: usize, + buttons: f32, + column: usize, + row: usize, spawn_text: bool, border: UiRect, border_color: BorderColor, + image: Option>, ) { - let width = 90.0 / total; + let width = Val::Vw(90.0 / buttons); + let height = Val::Vh(90.0 / buttons); + let margin = UiRect::axes(width * 0.05, height * 0.05); let mut builder = commands.spawn(( ButtonBundle { style: Style { - width: Val::Percent(width), - height: Val::Percent(width), - bottom: Val::Percent(100.0 / total * i as f32), - left: Val::Percent(100.0 / total * j as f32), + width, + height, + margin, align_items: AlignItems::Center, - position_type: PositionType::Absolute, + justify_content: JustifyContent::Center, border, ..default() }, @@ -145,10 +238,14 @@ fn spawn_button( IdleColor(background_color), )); + if let Some(image) = image { + builder.insert(UiImage::new(image)); + } + if spawn_text { - builder.with_children(|commands| { - commands.spawn(TextBundle::from_section( - format!("{i}, {j}"), + builder.with_children(|parent| { + parent.spawn(TextBundle::from_section( + format!("{column}, {row}"), TextStyle { font_size: FONT_SIZE, color: Color::rgb(0.2, 0.2, 0.2),