From 73447b6d7236121893e67328b393962d4a818076 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 8 Sep 2023 16:02:05 +0100 Subject: [PATCH] `many_buttons` enhancements (#9712) # Objective `many_buttons` enhancements: * use `argh` to manage the commandline arguments like the other stress tests * add an option to set the number of buttons * add a grid layout option * centre the grid properly * use viewport coords for the layout's style constraints * replace use of absolute positioning includes the changes from #9636 Displaying an image isn't actually about stress testing image rendering. Without a second texture (the first is used by the text) the entire grid will be drawn in a single batch. The extra texture used by the image forces the renderer to break up the batches at every button displaying an image, where it has to switch between the font atlas texture and the image texture. ## Solution many_buttons_new --- ## Changelog `many_buttons` stress test example enhancements: * uses `argh` to the manage the commandline arguments. * New commandline args: - `--help` display info & list all commandline options - `--buttons` set the number of buttons. - `--image-freq` set the frequency of buttons displaying images - `--grid` use a grid layout * style constraints are specified in viewport coords insead of percentage values * margins and nested bundles are used to construct the layout, instead of absolute positioning * the button grid centered in the window, the empty gap along the bottom and right is removed * an image is drawn as the background to every Nth button where N is set using the `--image-freq` commandline option. --------- Co-authored-by: Rob Parrett --- examples/stress_tests/many_buttons.rs | 213 +++++++++++++++++++------- 1 file changed, 155 insertions(+), 58 deletions(-) diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 9cd125ac0d..1095076e16 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 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 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| { - 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 { + 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 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| { + 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),