bevy/examples/ui/box_shadow.rs
2025-06-26 19:38:19 +00:00

640 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,
}
impl SettingType {
fn label(&self) -> &str {
match self {
SettingType::XOffset => "X Offset",
SettingType::YOffset => "Y Offset",
SettingType::Blur => "Blur",
SettingType::Spread => "Spread",
SettingType::Count => "Count",
SettingType::Shape => "Shape",
SettingType::Samples => "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(
SettingType::Shape,
SettingsButton::ShapePrev,
SettingsButton::ShapeNext,
shape.index as f32,
&asset_server,
),
build_setting_row(
SettingType::XOffset,
SettingsButton::XOffsetDec,
SettingsButton::XOffsetInc,
shadow.x_offset,
&asset_server,
),
build_setting_row(
SettingType::YOffset,
SettingsButton::YOffsetDec,
SettingsButton::YOffsetInc,
shadow.y_offset,
&asset_server,
),
build_setting_row(
SettingType::Blur,
SettingsButton::BlurDec,
SettingsButton::BlurInc,
shadow.blur,
&asset_server,
),
build_setting_row(
SettingType::Spread,
SettingsButton::SpreadDec,
SettingsButton::SpreadInc,
shadow.spread,
&asset_server,
),
build_setting_row(
SettingType::Count,
SettingsButton::CountDec,
SettingsButton::CountInc,
shadow.count as f32,
&asset_server,
),
// Add BoxShadowSamples as a setting row
build_setting_row(
SettingType::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(
setting_type: SettingType,
dec: SettingsButton,
inc: SettingsButton,
value: f32,
asset_server: &Res<AssetServer>,
) -> impl Bundle {
let value_text = match setting_type {
SettingType::Shape => SHAPES[value as usize % SHAPES.len()].0.to_string(),
SettingType::Count => format!("{}", value as usize),
_ => format!("{value:.1}"),
};
(
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(setting_type.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 setting_type == SettingType::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![{
(
Text::new(value_text),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 16.0,
..default()
},
setting_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 setting_type == SettingType::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(),
}
}
}