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
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 155 additions & 58 deletions examples/stress_tests/many_buttons.rs
Original file line number Diff line number Diff line change
@@ -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((
Expand All @@ -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<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 +88,117 @@ 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>) {
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>) {
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(),
);
}
}
Expand All @@ -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<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 +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),
Expand Down