Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

many_buttons enhancements #9712

Merged
merged 13 commits into from
Sep 8, 2023
Merged
216 changes: 158 additions & 58 deletions examples/stress_tests/many_buttons.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,83 @@
//! 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((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: (800., 800.).into(),
..default()
}),
..default()
}),
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<UiScale>| {
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)]
Expand All @@ -64,50 +89,119 @@ fn button_system(
Changed<Interaction>,
>,
) {
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<AssetServer>, args: Res<Args>) {
println!("flex setup");
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<AssetServer>, args: Res<Args>) {
println!("grid setup");
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: vec![RepeatedGridTrack::flex(args.buttons as u16, 1.0)],
grid_template_rows: vec![RepeatedGridTrack::flex(args.buttons as u16, 1.0)],
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
..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(),
);
}
}
Expand All @@ -118,23 +212,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<Handle<Image>>,
) {
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()
},
Expand All @@ -145,10 +241,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),
Expand Down