This example shows how to create a node with a shadow and adjust its settings interactively.
use crate::number_input_f32::number_input_f32;
use crate::number_input_i32::number_input_i32;
use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
use bevy::feathers::tokens::semantic::SURFACE_PANE_BODY;
use bevy::{
color::palettes::css::*,
feathers::{
containers::{pane, pane_body},
controls::{FeathersButton, FeathersNumberInput, NumberInputPrecision, NumberInputValue},
dark_theme::create_dark_theme,
display::caption,
theme::{ThemeProps, UiTheme},
FeathersPlugins,
},
prelude::*,
ui_widgets::{radio_self_update, Activate, ValueChange},
};
#[path = "../../helpers/number_input_f32.rs"]
mod number_input_f32;
#[path = "../../helpers/number_input_i32.rs"]
mod number_input_i32;
#[path = "../../helpers/radio.rs"]
mod radio;
/// Shapes that the node displayed on the screen can be.
#[derive(Component, Clone, Copy, Default, PartialEq)]
enum Shape {
#[default]
Square,
RoundedSquare,
Circle,
LongRectangle,
TallRectangle,
}
impl Shape {
/// Mutates the `node` to become the given shape.
fn change_node(&self, node: &mut Node) {
match *self {
Self::Square => {
node.width = px(164);
node.height = px(164);
node.border_radius = BorderRadius::ZERO;
}
Self::RoundedSquare => {
node.width = px(164);
node.height = px(164);
node.border_radius = BorderRadius::all(px(41));
}
Self::Circle => {
node.width = px(164);
node.height = px(164);
node.border_radius = BorderRadius::MAX;
}
Self::LongRectangle => {
node.width = px(240);
node.height = px(80);
node.border_radius = BorderRadius::all(px(32));
}
Self::TallRectangle => {
node.width = px(80);
node.height = px(240);
node.border_radius = BorderRadius::all(px(32));
}
}
}
/// Returns the name of the shape as a string.
const fn label(&self) -> &'static str {
match *self {
Shape::Square => "Square",
Shape::RoundedSquare => "Rounded Square",
Shape::Circle => "Circle",
Shape::LongRectangle => "Long Rectangle",
Shape::TallRectangle => "Tall Rectangle",
}
}
}
const SHAPE_OPTIONS: [(Shape, &str); 5] = [
(Shape::Square, Shape::Square.label()),
(Shape::RoundedSquare, Shape::RoundedSquare.label()),
(Shape::Circle, Shape::Circle.label()),
(Shape::LongRectangle, Shape::LongRectangle.label()),
(Shape::TallRectangle, Shape::TallRectangle.label()),
];
/// Settings that the user can modify within the example.
#[derive(Resource)]
struct AppSettings {
shape: Shape,
// Refer to `ShadowStyle` for information on x_offset, y_offset,
// blur (radius), and spread (radius)
x_offset: f32,
y_offset: f32,
blur: f32,
spread: f32,
/// The number of `BoxShadow`s created
count: usize,
/// The number of samples used in `BoxShadowSamples`
samples: u32,
}
/// The example initializes with these default settings.
impl Default for AppSettings {
fn default() -> Self {
Self {
shape: Shape::default(),
x_offset: 20.0,
y_offset: 20.0,
blur: 10.0,
spread: 15.0,
count: 1,
samples: 6,
}
}
}
#[derive(Component, Clone, Default)]
struct ShadowNode;
#[derive(Component, Clone, Default)]
struct SettingsPanel;
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Debug)]
enum AppNumberInputF32 {
#[default]
XOffset,
YOffset,
Blur,
Spread,
}
#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Debug)]
enum AppNumberInputI32 {
#[default]
Count,
Samples,
}
impl AppNumberInputF32 {
fn label(&self) -> &str {
match self {
AppNumberInputF32::XOffset => "X Offset",
AppNumberInputF32::YOffset => "Y Offset",
AppNumberInputF32::Blur => "Blur",
AppNumberInputF32::Spread => "Spread",
}
}
}
impl AppNumberInputI32 {
fn label(&self) -> &str {
match self {
AppNumberInputI32::Count => "Count (1 - 3)",
AppNumberInputI32::Samples => "Samples (0 - 15)",
}
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, FeathersPlugins))
.insert_resource(UiTheme(get_example_theme()))
.init_resource::<AppSettings>()
.add_systems(Startup, setup)
.add_observer(on_value_change_i32_update_shadow)
.add_observer(on_value_change_f32_update_shadow)
.add_observer(on_value_change_update_shape)
.add_observer(radio_self_update)
.run();
}
// --- UI Setup ---
fn setup(mut commands: Commands, app_settings: Res<AppSettings>) {
// create shape node
let mut node = Node {
width: px(164),
height: px(164),
border: UiRect::all(px(1)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
border_radius: BorderRadius::ZERO,
..default()
};
app_settings.shape.change_node(&mut node);
commands.spawn_scene_list(bsn_list! {
// Camera
Camera2d
BoxShadowSamples({app_settings.samples})
--
// Centered shape with shadow
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
}
BackgroundColor(GRAY)
Children [
node
BorderColor::all(WHITE)
BackgroundColor(Color::srgb(0.21, 0.21, 0.21))
BoxShadow(vec![ShadowStyle {
color: Color::BLACK.with_alpha(0.8),
x_offset: px(app_settings.x_offset),
y_offset: px(app_settings.y_offset),
spread_radius: px(app_settings.spread),
blur_radius: px(app_settings.blur),
}])
ShadowNode
]
--
@settings_panel_scene(&app_settings)
});
}
fn settings_panel_scene(app_settings: &AppSettings) -> impl Scene {
let selected_shape_index = SHAPE_OPTIONS
.iter()
.enumerate()
.find(|(_, (shape, _))| *shape == app_settings.shape)
// default is set to the length of the array, which will be interpreted as "no option is set"
.map_or(SHAPE_OPTIONS.len(), |(index, _)| index);
bsn! {
SettingsPanel
ZIndex(10)
@pane()
@main_ui_node_scene()
Children [
@pane_body()
Children [
@feathers_option_buttons(
"Shape",
&SHAPE_OPTIONS,
selected_shape_index,
)
--
@number_input_f32(
AppNumberInputF32::XOffset.label(),
Some(AppNumberInputF32::XOffset),
app_settings.x_offset,
NumberInputPrecision(0),
-200. ..=200.
)
--
@number_input_f32(
AppNumberInputF32::YOffset.label(),
Some(AppNumberInputF32::YOffset),
app_settings.y_offset,
NumberInputPrecision(0),
-200. ..=200.
)
--
@number_input_f32(
AppNumberInputF32::Blur.label(),
Some(AppNumberInputF32::Blur),
app_settings.blur,
NumberInputPrecision(0),
0. ..=100.
)
--
@number_input_f32(
AppNumberInputF32::Spread.label(),
Some(AppNumberInputF32::Spread),
app_settings.spread,
NumberInputPrecision(0),
-200. ..=200.
)
--
@number_input_i32(
AppNumberInputI32::Count.label(),
Some(AppNumberInputI32::Count),
app_settings.count as i32,
NumberInputPrecision(0),
1..=3
)
--
@number_input_i32(
AppNumberInputI32::Samples.label(),
Some(AppNumberInputI32::Samples),
app_settings.samples as i32,
NumberInputPrecision(0),
0..=15
)
--
// Reset button
@FeathersButton {
@caption: bsn! { @caption("Reset") }
}
on(on_activate_reset)
]
]
}
}
fn get_example_theme() -> ThemeProps {
let mut props = create_dark_theme();
// Pane background color is made a little transparent to see the objects behind the setting controls.
if let Some(color) = props.semantic_base.get_mut(&SURFACE_PANE_BODY) {
color.set_alpha(0.9);
}
props
}
// --- SYSTEMS ---
/// Update the shadow node's `BoxShadow` or the camera's `BoxShadowSamples` on any change to the i32 number inputs.
fn on_value_change_i32_update_shadow(
value_change: On<ValueChange<i32>>,
number_input_q: Query<&AppNumberInputI32, With<FeathersNumberInput>>,
mut commands: Commands,
mut app_settings: ResMut<AppSettings>,
mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
mut samples_q: Query<&mut BoxShadowSamples, With<Camera2d>>,
) {
if let Ok(app_number_input_i32) = number_input_q.get(value_change.source) {
match app_number_input_i32 {
AppNumberInputI32::Count => {
app_settings.count = value_change.value as usize;
for mut box_shadow in &mut box_shadow_q {
*box_shadow = BoxShadow(generate_shadows(&app_settings));
}
}
AppNumberInputI32::Samples => {
app_settings.samples = value_change.value as u32;
for mut samples in &mut samples_q {
samples.0 = app_settings.samples;
}
}
}
}
commands
.entity(value_change.source)
.insert(NumberInputValue::I32(value_change.value));
}
/// Update the shadow node's `BoxShadow` on any change to the f32 number inputs.
fn on_value_change_f32_update_shadow(
value_change: On<ValueChange<f32>>,
number_input_q: Query<&AppNumberInputF32, With<FeathersNumberInput>>,
mut commands: Commands,
mut app_settings: ResMut<AppSettings>,
mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
) {
if let Ok(app_number_input_i32) = number_input_q.get(value_change.source) {
match app_number_input_i32 {
AppNumberInputF32::XOffset => {
app_settings.x_offset = value_change.value;
}
AppNumberInputF32::YOffset => {
app_settings.y_offset = value_change.value;
}
AppNumberInputF32::Blur => {
app_settings.blur = value_change.value;
}
AppNumberInputF32::Spread => {
app_settings.spread = value_change.value;
}
}
for mut box_shadow in &mut box_shadow_q {
*box_shadow = BoxShadow(generate_shadows(&app_settings));
}
}
commands
.entity(value_change.source)
.insert(NumberInputValue::F32(value_change.value));
}
/// Update shape of `ShadowNode` if shape selection has changed
fn on_value_change_update_shape(
event: On<ValueChange<Entity>>,
new_value_query: Query<&RadioButtonOptionValue<Shape>>,
mut app_settings: ResMut<AppSettings>,
mut node_q: Query<&mut Node, With<ShadowNode>>,
) {
let Ok(RadioButtonOptionValue(shape)) = new_value_query.get(event.value) else {
return;
};
app_settings.shape = *shape;
for mut node in &mut node_q {
app_settings.shape.change_node(&mut node);
}
}
/// If the reset button was activated, reset app settings to default values.
/// This observer is placed directly on the reset button.
fn on_activate_reset(
_event: On<Activate>,
mut commands: Commands,
mut app_settings: ResMut<AppSettings>,
settings_panel_q: Single<Entity, With<SettingsPanel>>,
mut node_q: Query<&mut Node, With<ShadowNode>>,
mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
mut samples_q: Query<&mut BoxShadowSamples, With<Camera2d>>,
) {
*app_settings = AppSettings::default();
// Reset the settings panel. It is quicker to do this
// than to go through every field and setting the fields correctly.
commands.entity(settings_panel_q.entity()).despawn();
commands.spawn_scene(settings_panel_scene(&app_settings));
// Do a full refresh of the box and its shadows.
for mut node in &mut node_q {
app_settings.shape.change_node(&mut node);
}
for mut box_shadow in &mut box_shadow_q {
*box_shadow = BoxShadow(generate_shadows(&app_settings));
}
for mut samples in &mut samples_q {
samples.0 = app_settings.samples;
}
}
fn generate_shadows(app_settings: &AppSettings) -> Vec<ShadowStyle> {
match app_settings.count {
1 => vec![make_shadow(
BLACK.into(),
app_settings.x_offset,
app_settings.y_offset,
app_settings.spread,
app_settings.blur,
)],
2 => vec![
make_shadow(
BLUE.into(),
app_settings.x_offset,
app_settings.y_offset,
app_settings.spread,
app_settings.blur,
),
make_shadow(
YELLOW.into(),
-app_settings.x_offset,
-app_settings.y_offset,
app_settings.spread,
app_settings.blur,
),
],
3 => vec![
make_shadow(
BLUE.into(),
app_settings.x_offset,
app_settings.y_offset,
app_settings.spread,
app_settings.blur,
),
make_shadow(
YELLOW.into(),
-app_settings.x_offset,
-app_settings.y_offset,
app_settings.spread,
app_settings.blur,
),
make_shadow(
RED.into(),
app_settings.y_offset,
-app_settings.x_offset,
app_settings.spread,
app_settings.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: px(x_offset),
y_offset: px(y_offset),
spread_radius: px(spread),
blur_radius: px(blur),
}
}