640 lines
20 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|