Skip to main content

box_shadow/
box_shadow.rs

1//! This example shows how to create a node with a shadow and adjust its settings interactively.
2
3use crate::number_input_f32::number_input_f32;
4use crate::number_input_i32::number_input_i32;
5use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
6use bevy::feathers::tokens::semantic::SURFACE_PANE_BODY;
7use bevy::{
8    color::palettes::css::*,
9    feathers::{
10        containers::{pane, pane_body},
11        controls::{FeathersButton, FeathersNumberInput, NumberInputPrecision, NumberInputValue},
12        dark_theme::create_dark_theme,
13        display::caption,
14        theme::{ThemeProps, UiTheme},
15        FeathersPlugins,
16    },
17    prelude::*,
18    ui_widgets::{radio_self_update, Activate, ValueChange},
19};
20
21#[path = "../../helpers/number_input_f32.rs"]
22mod number_input_f32;
23
24#[path = "../../helpers/number_input_i32.rs"]
25mod number_input_i32;
26
27#[path = "../../helpers/radio.rs"]
28mod radio;
29
30/// Shapes that the node displayed on the screen can be.
31#[derive(Component, Clone, Copy, Default, PartialEq)]
32enum Shape {
33    #[default]
34    Square,
35    RoundedSquare,
36    Circle,
37    LongRectangle,
38    TallRectangle,
39}
40
41impl Shape {
42    /// Mutates the `node` to become the given shape.
43    fn change_node(&self, node: &mut Node) {
44        match *self {
45            Self::Square => {
46                node.width = px(164);
47                node.height = px(164);
48                node.border_radius = BorderRadius::ZERO;
49            }
50            Self::RoundedSquare => {
51                node.width = px(164);
52                node.height = px(164);
53                node.border_radius = BorderRadius::all(px(41));
54            }
55            Self::Circle => {
56                node.width = px(164);
57                node.height = px(164);
58                node.border_radius = BorderRadius::MAX;
59            }
60            Self::LongRectangle => {
61                node.width = px(240);
62                node.height = px(80);
63                node.border_radius = BorderRadius::all(px(32));
64            }
65            Self::TallRectangle => {
66                node.width = px(80);
67                node.height = px(240);
68                node.border_radius = BorderRadius::all(px(32));
69            }
70        }
71    }
72
73    /// Returns the name of the shape as a string.
74    const fn label(&self) -> &'static str {
75        match *self {
76            Shape::Square => "Square",
77            Shape::RoundedSquare => "Rounded Square",
78            Shape::Circle => "Circle",
79            Shape::LongRectangle => "Long Rectangle",
80            Shape::TallRectangle => "Tall Rectangle",
81        }
82    }
83}
84
85const SHAPE_OPTIONS: [(Shape, &str); 5] = [
86    (Shape::Square, Shape::Square.label()),
87    (Shape::RoundedSquare, Shape::RoundedSquare.label()),
88    (Shape::Circle, Shape::Circle.label()),
89    (Shape::LongRectangle, Shape::LongRectangle.label()),
90    (Shape::TallRectangle, Shape::TallRectangle.label()),
91];
92
93/// Settings that the user can modify within the example.
94#[derive(Resource)]
95struct AppSettings {
96    shape: Shape,
97    // Refer to `ShadowStyle` for information on x_offset, y_offset,
98    // blur (radius), and spread (radius)
99    x_offset: f32,
100    y_offset: f32,
101    blur: f32,
102    spread: f32,
103    /// The number of `BoxShadow`s created
104    count: usize,
105    /// The number of samples used in `BoxShadowSamples`
106    samples: u32,
107}
108
109/// The example initializes with these default settings.
110impl Default for AppSettings {
111    fn default() -> Self {
112        Self {
113            shape: Shape::default(),
114            x_offset: 20.0,
115            y_offset: 20.0,
116            blur: 10.0,
117            spread: 15.0,
118            count: 1,
119            samples: 6,
120        }
121    }
122}
123
124#[derive(Component, Clone, Default)]
125struct ShadowNode;
126
127#[derive(Component, Clone, Default)]
128struct SettingsPanel;
129
130#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Debug)]
131enum AppNumberInputF32 {
132    #[default]
133    XOffset,
134    YOffset,
135    Blur,
136    Spread,
137}
138
139#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Debug)]
140enum AppNumberInputI32 {
141    #[default]
142    Count,
143    Samples,
144}
145
146impl AppNumberInputF32 {
147    fn label(&self) -> &str {
148        match self {
149            AppNumberInputF32::XOffset => "X Offset",
150            AppNumberInputF32::YOffset => "Y Offset",
151            AppNumberInputF32::Blur => "Blur",
152            AppNumberInputF32::Spread => "Spread",
153        }
154    }
155}
156
157impl AppNumberInputI32 {
158    fn label(&self) -> &str {
159        match self {
160            AppNumberInputI32::Count => "Count (1 - 3)",
161            AppNumberInputI32::Samples => "Samples (0 - 15)",
162        }
163    }
164}
165
166fn main() {
167    App::new()
168        .add_plugins((DefaultPlugins, FeathersPlugins))
169        .insert_resource(UiTheme(get_example_theme()))
170        .init_resource::<AppSettings>()
171        .add_systems(Startup, setup)
172        .add_observer(on_value_change_i32_update_shadow)
173        .add_observer(on_value_change_f32_update_shadow)
174        .add_observer(on_value_change_update_shape)
175        .add_observer(radio_self_update)
176        .run();
177}
178
179// --- UI Setup ---
180fn setup(mut commands: Commands, app_settings: Res<AppSettings>) {
181    // create shape node
182    let mut node = Node {
183        width: px(164),
184        height: px(164),
185        border: UiRect::all(px(1)),
186        align_items: AlignItems::Center,
187        justify_content: JustifyContent::Center,
188        border_radius: BorderRadius::ZERO,
189        ..default()
190    };
191    app_settings.shape.change_node(&mut node);
192
193    commands.spawn_scene_list(bsn_list! {
194        // Camera
195        Camera2d
196        BoxShadowSamples({app_settings.samples})
197        --
198        // Centered shape with shadow
199        Node {
200            width: percent(100),
201            height: percent(100),
202            align_items: AlignItems::Center,
203            justify_content: JustifyContent::Center,
204        }
205        BackgroundColor(GRAY)
206        Children [
207            node
208            BorderColor::all(WHITE)
209            BackgroundColor(Color::srgb(0.21, 0.21, 0.21))
210            BoxShadow(vec![ShadowStyle {
211                color: Color::BLACK.with_alpha(0.8),
212                x_offset: px(app_settings.x_offset),
213                y_offset: px(app_settings.y_offset),
214                spread_radius: px(app_settings.spread),
215                blur_radius: px(app_settings.blur),
216            }])
217            ShadowNode
218        ]
219        --
220        @settings_panel_scene(&app_settings)
221    });
222}
223
224fn settings_panel_scene(app_settings: &AppSettings) -> impl Scene {
225    let selected_shape_index = SHAPE_OPTIONS
226        .iter()
227        .enumerate()
228        .find(|(_, (shape, _))| *shape == app_settings.shape)
229        // default is set to the length of the array, which will be interpreted as "no option is set"
230        .map_or(SHAPE_OPTIONS.len(), |(index, _)| index);
231
232    bsn! {
233        SettingsPanel
234        ZIndex(10)
235        @pane()
236        @main_ui_node_scene()
237        Children [
238            @pane_body()
239            Children [
240                @feathers_option_buttons(
241                    "Shape",
242                    &SHAPE_OPTIONS,
243                    selected_shape_index,
244                )
245                --
246                @number_input_f32(
247                    AppNumberInputF32::XOffset.label(),
248                    Some(AppNumberInputF32::XOffset),
249                    app_settings.x_offset,
250                    NumberInputPrecision(0),
251                    -200. ..=200.
252                )
253                --
254                @number_input_f32(
255                    AppNumberInputF32::YOffset.label(),
256                    Some(AppNumberInputF32::YOffset),
257                    app_settings.y_offset,
258                    NumberInputPrecision(0),
259                    -200. ..=200.
260                )
261                --
262                @number_input_f32(
263                    AppNumberInputF32::Blur.label(),
264                    Some(AppNumberInputF32::Blur),
265                    app_settings.blur,
266                    NumberInputPrecision(0),
267                    0. ..=100.
268                )
269                --
270                @number_input_f32(
271                    AppNumberInputF32::Spread.label(),
272                    Some(AppNumberInputF32::Spread),
273                    app_settings.spread,
274                    NumberInputPrecision(0),
275                    -200. ..=200.
276                )
277                --
278                @number_input_i32(
279                    AppNumberInputI32::Count.label(),
280                    Some(AppNumberInputI32::Count),
281                    app_settings.count as i32,
282                    NumberInputPrecision(0),
283                    1..=3
284                )
285                --
286                @number_input_i32(
287                    AppNumberInputI32::Samples.label(),
288                    Some(AppNumberInputI32::Samples),
289                    app_settings.samples as i32,
290                    NumberInputPrecision(0),
291                    0..=15
292                )
293                --
294                // Reset button
295                @FeathersButton {
296                    @caption: bsn! { @caption("Reset") }
297                }
298                on(on_activate_reset)
299            ]
300        ]
301    }
302}
303
304fn get_example_theme() -> ThemeProps {
305    let mut props = create_dark_theme();
306    // Pane background color is made a little transparent to see the objects behind the setting controls.
307    if let Some(color) = props.semantic_base.get_mut(&SURFACE_PANE_BODY) {
308        color.set_alpha(0.9);
309    }
310    props
311}
312
313// --- SYSTEMS ---
314
315/// Update the shadow node's `BoxShadow` or the camera's `BoxShadowSamples` on any change to the i32 number inputs.
316fn on_value_change_i32_update_shadow(
317    value_change: On<ValueChange<i32>>,
318    number_input_q: Query<&AppNumberInputI32, With<FeathersNumberInput>>,
319    mut commands: Commands,
320    mut app_settings: ResMut<AppSettings>,
321    mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
322    mut samples_q: Query<&mut BoxShadowSamples, With<Camera2d>>,
323) {
324    if let Ok(app_number_input_i32) = number_input_q.get(value_change.source) {
325        match app_number_input_i32 {
326            AppNumberInputI32::Count => {
327                app_settings.count = value_change.value as usize;
328                for mut box_shadow in &mut box_shadow_q {
329                    *box_shadow = BoxShadow(generate_shadows(&app_settings));
330                }
331            }
332            AppNumberInputI32::Samples => {
333                app_settings.samples = value_change.value as u32;
334                for mut samples in &mut samples_q {
335                    samples.0 = app_settings.samples;
336                }
337            }
338        }
339    }
340
341    commands
342        .entity(value_change.source)
343        .insert(NumberInputValue::I32(value_change.value));
344}
345
346/// Update the shadow node's `BoxShadow` on any change to the f32 number inputs.
347fn on_value_change_f32_update_shadow(
348    value_change: On<ValueChange<f32>>,
349    number_input_q: Query<&AppNumberInputF32, With<FeathersNumberInput>>,
350    mut commands: Commands,
351    mut app_settings: ResMut<AppSettings>,
352    mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
353) {
354    if let Ok(app_number_input_i32) = number_input_q.get(value_change.source) {
355        match app_number_input_i32 {
356            AppNumberInputF32::XOffset => {
357                app_settings.x_offset = value_change.value;
358            }
359            AppNumberInputF32::YOffset => {
360                app_settings.y_offset = value_change.value;
361            }
362            AppNumberInputF32::Blur => {
363                app_settings.blur = value_change.value;
364            }
365            AppNumberInputF32::Spread => {
366                app_settings.spread = value_change.value;
367            }
368        }
369        for mut box_shadow in &mut box_shadow_q {
370            *box_shadow = BoxShadow(generate_shadows(&app_settings));
371        }
372    }
373
374    commands
375        .entity(value_change.source)
376        .insert(NumberInputValue::F32(value_change.value));
377}
378
379/// Update shape of `ShadowNode` if shape selection has changed
380fn on_value_change_update_shape(
381    event: On<ValueChange<Entity>>,
382    new_value_query: Query<&RadioButtonOptionValue<Shape>>,
383    mut app_settings: ResMut<AppSettings>,
384    mut node_q: Query<&mut Node, With<ShadowNode>>,
385) {
386    let Ok(RadioButtonOptionValue(shape)) = new_value_query.get(event.value) else {
387        return;
388    };
389    app_settings.shape = *shape;
390
391    for mut node in &mut node_q {
392        app_settings.shape.change_node(&mut node);
393    }
394}
395
396/// If the reset button was activated, reset app settings to default values.
397/// This observer is placed directly on the reset button.
398fn on_activate_reset(
399    _event: On<Activate>,
400    mut commands: Commands,
401    mut app_settings: ResMut<AppSettings>,
402    settings_panel_q: Single<Entity, With<SettingsPanel>>,
403    mut node_q: Query<&mut Node, With<ShadowNode>>,
404    mut box_shadow_q: Query<&mut BoxShadow, With<ShadowNode>>,
405    mut samples_q: Query<&mut BoxShadowSamples, With<Camera2d>>,
406) {
407    *app_settings = AppSettings::default();
408
409    // Reset the settings panel. It is quicker to do this
410    // than to go through every field and setting the fields correctly.
411    commands.entity(settings_panel_q.entity()).despawn();
412    commands.spawn_scene(settings_panel_scene(&app_settings));
413
414    // Do a full refresh of the box and its shadows.
415    for mut node in &mut node_q {
416        app_settings.shape.change_node(&mut node);
417    }
418
419    for mut box_shadow in &mut box_shadow_q {
420        *box_shadow = BoxShadow(generate_shadows(&app_settings));
421    }
422
423    for mut samples in &mut samples_q {
424        samples.0 = app_settings.samples;
425    }
426}
427
428fn generate_shadows(app_settings: &AppSettings) -> Vec<ShadowStyle> {
429    match app_settings.count {
430        1 => vec![make_shadow(
431            BLACK.into(),
432            app_settings.x_offset,
433            app_settings.y_offset,
434            app_settings.spread,
435            app_settings.blur,
436        )],
437        2 => vec![
438            make_shadow(
439                BLUE.into(),
440                app_settings.x_offset,
441                app_settings.y_offset,
442                app_settings.spread,
443                app_settings.blur,
444            ),
445            make_shadow(
446                YELLOW.into(),
447                -app_settings.x_offset,
448                -app_settings.y_offset,
449                app_settings.spread,
450                app_settings.blur,
451            ),
452        ],
453        3 => vec![
454            make_shadow(
455                BLUE.into(),
456                app_settings.x_offset,
457                app_settings.y_offset,
458                app_settings.spread,
459                app_settings.blur,
460            ),
461            make_shadow(
462                YELLOW.into(),
463                -app_settings.x_offset,
464                -app_settings.y_offset,
465                app_settings.spread,
466                app_settings.blur,
467            ),
468            make_shadow(
469                RED.into(),
470                app_settings.y_offset,
471                -app_settings.x_offset,
472                app_settings.spread,
473                app_settings.blur,
474            ),
475        ],
476        _ => vec![],
477    }
478}
479
480fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
481    ShadowStyle {
482        color: color.with_alpha(0.8),
483        x_offset: px(x_offset),
484        y_offset: px(y_offset),
485        spread_radius: px(spread),
486        blur_radius: px(blur),
487    }
488}