
# 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   --------- Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
638 lines
20 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|