bevy/examples/ui/box_shadow.rs
oracle58 8e585174ee
box_shadow example with adjustable settings (#19345)
# Objective

- Addresses the previous example's lack of visual appeal and clarity. It
was missing labels for clear distinction of the shadow settings used on
each of the shapes. The suggestion in the linked issue was to either
just visually update and add labels or to collapse example to a single
node with adjustable settings.
- Fixes #19240

## Solution

- Replace the previous static example with a single, central node with
adjustable settings as per issue suggestion.
- Implement button-based setting adjustments. Unfortunately slider
widgets don't seem available yet and I didn't want to further bloat the
example.
- Improve overall aesthetics of the example -- although color pallette
could still be improved. flat gray tones are probably not the best
choice as a contrast to the shadow, but the white border does help in
that aspect.
- Dynamically recolor shadows for visual clarity when increasing shadow
count.
- Add Adjustable Settings:
    - Shape selection
    - Shadow X/Y offset, blur, spread, and count
- Add Reset button to restore default settings

The disadvantage of this solution is that the old example code would
have probably been easier to digest as the new example is quite bloated
in comparison. Alternatively I could also just implement labels and fix
aesthetics of the old example without adding functionality for
adjustable settings, _but_ I personally feel like interactive examples
are more engaging to users.

## Testing

- Did you test these changes? If so, how? `cargo run --example
box_shadow` and functionality of all features of the example.
- Are there any parts that need more testing? Not that I am aware of. 
- How can other people (reviewers) test your changes? Is there anything
specific they need to know? Not really, it should be pretty
straightforward just running the new example and testing the feats.

---

## Showcase

![box-shadow-example-1](https://github.com/user-attachments/assets/57586b30-c290-4e3f-9355-5c3f6e9a6406)


![box-shadow-example-2](https://github.com/user-attachments/assets/51a51d2f-dd30-465b-b802-ddb8077adff5)

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
2025-05-27 19:43:57 +00:00

638 lines
20 KiB
Rust

//! This example shows how to create a node with a shadow and adjust its settings interactively.
use bevy::{
color::palettes::css::*, prelude::*, time::Time, window::RequestRedraw, winit::WinitSettings,
};
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
const SHAPE_DEFAULT_SETTINGS: ShapeSettings = ShapeSettings { index: 0 };
const SHADOW_DEFAULT_SETTINGS: ShadowSettings = ShadowSettings {
x_offset: 20.0,
y_offset: 20.0,
blur: 10.0,
spread: 15.0,
count: 1,
samples: 6,
};
const SHAPES: &[(&str, fn(&mut Node, &mut BorderRadius))] = &[
("1", |node, radius| {
node.width = Val::Px(164.);
node.height = Val::Px(164.);
*radius = BorderRadius::ZERO;
}),
("2", |node, radius| {
node.width = Val::Px(164.);
node.height = Val::Px(164.);
*radius = BorderRadius::all(Val::Px(41.));
}),
("3", |node, radius| {
node.width = Val::Px(164.);
node.height = Val::Px(164.);
*radius = BorderRadius::MAX;
}),
("4", |node, radius| {
node.width = Val::Px(240.);
node.height = Val::Px(80.);
*radius = BorderRadius::all(Val::Px(32.));
}),
("5", |node, radius| {
node.width = Val::Px(80.);
node.height = Val::Px(240.);
*radius = BorderRadius::all(Val::Px(32.));
}),
];
#[derive(Resource, Default)]
struct ShapeSettings {
index: usize,
}
#[derive(Resource, Default)]
struct ShadowSettings {
x_offset: f32,
y_offset: f32,
blur: f32,
spread: f32,
count: usize,
samples: u32,
}
#[derive(Component)]
struct ShadowNode;
#[derive(Component, PartialEq, Clone, Copy)]
enum SettingsButton {
XOffsetInc,
XOffsetDec,
YOffsetInc,
YOffsetDec,
BlurInc,
BlurDec,
SpreadInc,
SpreadDec,
CountInc,
CountDec,
ShapePrev,
ShapeNext,
Reset,
SamplesInc,
SamplesDec,
}
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
enum SettingType {
XOffset,
YOffset,
Blur,
Spread,
Count,
Shape,
Samples,
}
#[derive(Resource, Default)]
struct HeldButton {
button: Option<SettingsButton>,
pressed_at: Option<f64>,
last_repeat: Option<f64>,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(WinitSettings::desktop_app())
.insert_resource(SHADOW_DEFAULT_SETTINGS)
.insert_resource(SHAPE_DEFAULT_SETTINGS)
.insert_resource(HeldButton::default())
.add_systems(Startup, setup)
.add_systems(
Update,
(
button_system,
button_color_system,
update_shape.run_if(resource_changed::<ShapeSettings>),
update_shadow.run_if(resource_changed::<ShadowSettings>),
update_shadow_samples.run_if(resource_changed::<ShadowSettings>),
button_repeat_system,
),
)
.run();
}
// --- UI Setup ---
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
shadow: Res<ShadowSettings>,
shape: Res<ShapeSettings>,
) {
commands.spawn((Camera2d, BoxShadowSamples(shadow.samples)));
// Spawn shape node
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(GRAY.into()),
))
.insert(children![{
let mut node = Node {
width: Val::Px(164.),
height: Val::Px(164.),
border: UiRect::all(Val::Px(1.)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
};
let mut radius = BorderRadius::ZERO;
SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
(
node,
BorderColor::all(WHITE.into()),
radius,
BackgroundColor(Color::srgb(0.21, 0.21, 0.21)),
BoxShadow(vec![ShadowStyle {
color: Color::BLACK.with_alpha(0.8),
x_offset: Val::Px(shadow.x_offset),
y_offset: Val::Px(shadow.y_offset),
spread_radius: Val::Px(shadow.spread),
blur_radius: Val::Px(shadow.blur),
}]),
ShadowNode,
)
}]);
// Settings Panel
commands
.spawn((
Node {
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
left: Val::Px(24.0),
bottom: Val::Px(24.0),
width: Val::Px(270.0),
padding: UiRect::all(Val::Px(16.0)),
..default()
},
BackgroundColor(Color::srgb(0.12, 0.12, 0.12).with_alpha(0.85)),
BorderColor::all(Color::WHITE.with_alpha(0.15)),
BorderRadius::all(Val::Px(12.0)),
ZIndex(10),
))
.insert(children![
build_setting_row(
"Shape:",
SettingsButton::ShapePrev,
SettingsButton::ShapeNext,
shape.index as f32,
&asset_server,
),
build_setting_row(
"X Offset:",
SettingsButton::XOffsetDec,
SettingsButton::XOffsetInc,
shadow.x_offset,
&asset_server,
),
build_setting_row(
"Y Offset:",
SettingsButton::YOffsetDec,
SettingsButton::YOffsetInc,
shadow.y_offset,
&asset_server,
),
build_setting_row(
"Blur:",
SettingsButton::BlurDec,
SettingsButton::BlurInc,
shadow.blur,
&asset_server,
),
build_setting_row(
"Spread:",
SettingsButton::SpreadDec,
SettingsButton::SpreadInc,
shadow.spread,
&asset_server,
),
build_setting_row(
"Count:",
SettingsButton::CountDec,
SettingsButton::CountInc,
shadow.count as f32,
&asset_server,
),
// Add BoxShadowSamples as a setting row
build_setting_row(
"Samples:",
SettingsButton::SamplesDec,
SettingsButton::SamplesInc,
shadow.samples as f32,
&asset_server,
),
// Reset button
(
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
height: Val::Px(36.0),
margin: UiRect::top(Val::Px(12.0)),
..default()
},
children![(
Button,
Node {
width: Val::Px(90.),
height: Val::Px(32.),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(NORMAL_BUTTON),
BorderRadius::all(Val::Px(8.)),
SettingsButton::Reset,
children![(
Text::new("Reset"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 16.0,
..default()
},
)],
)],
),
]);
}
// --- UI Helper Functions ---
// Helper to return an input to the children! macro for a setting row
fn build_setting_row(
label: &str,
dec: SettingsButton,
inc: SettingsButton,
value: f32,
asset_server: &Res<AssetServer>,
) -> impl Bundle {
let label_type = match label {
"X Offset:" => SettingType::XOffset,
"Y Offset:" => SettingType::YOffset,
"Blur:" => SettingType::Blur,
"Spread:" => SettingType::Spread,
"Count:" => SettingType::Count,
"Shape:" => SettingType::Shape,
"Samples:" => SettingType::Samples,
_ => panic!("Unknown label: {}", label),
};
(
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
height: Val::Px(32.0),
..default()
},
children![
(
Node {
width: Val::Px(80.0),
justify_content: JustifyContent::FlexEnd,
align_items: AlignItems::Center,
..default()
},
// Attach SettingType to the value label node, not the parent row
children![(
Text::new(label),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 16.0,
..default()
},
)],
),
(
Button,
Node {
width: Val::Px(28.),
height: Val::Px(28.),
margin: UiRect::left(Val::Px(8.)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::WHITE),
BorderRadius::all(Val::Px(6.)),
dec,
children![(
Text::new(if label == "Shape:" { "<" } else { "-" }),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 18.0,
..default()
},
)],
),
(
Node {
width: Val::Px(48.),
height: Val::Px(28.),
margin: UiRect::horizontal(Val::Px(8.)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BorderRadius::all(Val::Px(6.)),
children![{
if label_type == SettingType::Shape {
(
Text::new(SHAPES[value as usize % SHAPES.len()].0),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 16.0,
..default()
},
label_type,
)
} else {
(
Text::new(if label_type == SettingType::Count {
format!("{}", value as usize)
} else {
format!("{:.1}", value)
}),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 16.0,
..default()
},
label_type,
)
}
}],
),
(
Button,
Node {
width: Val::Px(28.),
height: Val::Px(28.),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::WHITE),
BorderRadius::all(Val::Px(6.)),
inc,
children![(
Text::new(if label == "Shape:" { ">" } else { "+" }),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 18.0,
..default()
},
)],
),
],
)
}
// --- SYSTEMS ---
// Update the shadow node's BoxShadow on resource changes
fn update_shadow(
shadow: Res<ShadowSettings>,
mut query: Query<&mut BoxShadow, With<ShadowNode>>,
mut label_query: Query<(&mut Text, &SettingType)>,
) {
for mut box_shadow in &mut query {
*box_shadow = BoxShadow(generate_shadows(&shadow));
}
// Update value labels for shadow settings
for (mut text, setting) in &mut label_query {
let value = match setting {
SettingType::XOffset => format!("{:.1}", shadow.x_offset),
SettingType::YOffset => format!("{:.1}", shadow.y_offset),
SettingType::Blur => format!("{:.1}", shadow.blur),
SettingType::Spread => format!("{:.1}", shadow.spread),
SettingType::Count => format!("{}", shadow.count),
SettingType::Shape => continue,
SettingType::Samples => format!("{}", shadow.samples),
};
*text = Text::new(value);
}
}
fn update_shadow_samples(
shadow: Res<ShadowSettings>,
mut query: Query<&mut BoxShadowSamples, With<Camera2d>>,
) {
for mut samples in &mut query {
samples.0 = shadow.samples;
}
}
fn generate_shadows(shadow: &ShadowSettings) -> Vec<ShadowStyle> {
match shadow.count {
1 => vec![make_shadow(
BLACK.into(),
shadow.x_offset,
shadow.y_offset,
shadow.spread,
shadow.blur,
)],
2 => vec![
make_shadow(
BLUE.into(),
shadow.x_offset,
shadow.y_offset,
shadow.spread,
shadow.blur,
),
make_shadow(
YELLOW.into(),
-shadow.x_offset,
-shadow.y_offset,
shadow.spread,
shadow.blur,
),
],
3 => vec![
make_shadow(
BLUE.into(),
shadow.x_offset,
shadow.y_offset,
shadow.spread,
shadow.blur,
),
make_shadow(
YELLOW.into(),
-shadow.x_offset,
-shadow.y_offset,
shadow.spread,
shadow.blur,
),
make_shadow(
RED.into(),
shadow.y_offset,
-shadow.x_offset,
shadow.spread,
shadow.blur,
),
],
_ => vec![],
}
}
fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
ShadowStyle {
color: color.with_alpha(0.8),
x_offset: Val::Px(x_offset),
y_offset: Val::Px(y_offset),
spread_radius: Val::Px(spread),
blur_radius: Val::Px(blur),
}
}
// Update shape of ShadowNode if shape selection changed
fn update_shape(
shape: Res<ShapeSettings>,
mut query: Query<(&mut Node, &mut BorderRadius), With<ShadowNode>>,
mut label_query: Query<(&mut Text, &SettingType)>,
) {
for (mut node, mut radius) in &mut query {
SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
}
for (mut text, kind) in &mut label_query {
if *kind == SettingType::Shape {
*text = Text::new(SHAPES[shape.index % SHAPES.len()].0);
}
}
}
// Handles button interactions for all settings
fn button_system(
mut interaction_query: Query<
(&Interaction, &SettingsButton),
(Changed<Interaction>, With<Button>),
>,
mut shadow: ResMut<ShadowSettings>,
mut shape: ResMut<ShapeSettings>,
mut held: ResMut<HeldButton>,
time: Res<Time>,
) {
let now = time.elapsed_secs_f64();
for (interaction, btn) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
trigger_button_action(btn, &mut shadow, &mut shape);
held.button = Some(*btn);
held.pressed_at = Some(now);
held.last_repeat = Some(now);
}
Interaction::None | Interaction::Hovered => {
if held.button == Some(*btn) {
held.button = None;
held.pressed_at = None;
held.last_repeat = None;
}
}
}
}
}
fn trigger_button_action(
btn: &SettingsButton,
shadow: &mut ShadowSettings,
shape: &mut ShapeSettings,
) {
match btn {
SettingsButton::XOffsetInc => shadow.x_offset += 1.0,
SettingsButton::XOffsetDec => shadow.x_offset -= 1.0,
SettingsButton::YOffsetInc => shadow.y_offset += 1.0,
SettingsButton::YOffsetDec => shadow.y_offset -= 1.0,
SettingsButton::BlurInc => shadow.blur = (shadow.blur + 1.0).max(0.0),
SettingsButton::BlurDec => shadow.blur = (shadow.blur - 1.0).max(0.0),
SettingsButton::SpreadInc => shadow.spread += 1.0,
SettingsButton::SpreadDec => shadow.spread -= 1.0,
SettingsButton::CountInc => {
if shadow.count < 3 {
shadow.count += 1;
}
}
SettingsButton::CountDec => {
if shadow.count > 1 {
shadow.count -= 1;
}
}
SettingsButton::ShapePrev => {
if shape.index == 0 {
shape.index = SHAPES.len() - 1;
} else {
shape.index -= 1;
}
}
SettingsButton::ShapeNext => {
shape.index = (shape.index + 1) % SHAPES.len();
}
SettingsButton::Reset => {
*shape = SHAPE_DEFAULT_SETTINGS;
*shadow = SHADOW_DEFAULT_SETTINGS;
}
SettingsButton::SamplesInc => shadow.samples += 1,
SettingsButton::SamplesDec => {
if shadow.samples > 1 {
shadow.samples -= 1;
}
}
}
}
// System to repeat button action while held
fn button_repeat_system(
time: Res<Time>,
mut held: ResMut<HeldButton>,
mut shadow: ResMut<ShadowSettings>,
mut shape: ResMut<ShapeSettings>,
mut redraw_events: EventWriter<RequestRedraw>,
) {
if held.button.is_some() {
redraw_events.write(RequestRedraw);
}
const INITIAL_DELAY: f64 = 0.15;
const REPEAT_RATE: f64 = 0.08;
if let (Some(btn), Some(pressed_at)) = (held.button, held.pressed_at) {
let now = time.elapsed_secs_f64();
let since_pressed = now - pressed_at;
let last_repeat = held.last_repeat.unwrap_or(pressed_at);
let since_last = now - last_repeat;
if since_pressed > INITIAL_DELAY && since_last > REPEAT_RATE {
trigger_button_action(&btn, &mut shadow, &mut shape);
held.last_repeat = Some(now);
}
}
}
// Changes color of button on hover and on pressed
fn button_color_system(
mut query: Query<
(&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>, With<SettingsButton>),
>,
) {
for (interaction, mut color) in &mut query {
match *interaction {
Interaction::Pressed => *color = PRESSED_BUTTON.into(),
Interaction::Hovered => *color = HOVERED_BUTTON.into(),
Interaction::None => *color = NORMAL_BUTTON.into(),
}
}
}