box_shadow/
box_shadow.rs

1//! This example shows how to create a node with a shadow and adjust its settings interactively.
2
3use bevy::{color::palettes::css::*, prelude::*, time::Time, window::RequestRedraw};
4
5const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
6const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
7const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
8
9const SHAPE_DEFAULT_SETTINGS: ShapeSettings = ShapeSettings { index: 0 };
10
11const SHADOW_DEFAULT_SETTINGS: ShadowSettings = ShadowSettings {
12    x_offset: 20.0,
13    y_offset: 20.0,
14    blur: 10.0,
15    spread: 15.0,
16    count: 1,
17    samples: 6,
18};
19
20const SHAPES: &[(&str, fn(&mut Node, &mut BorderRadius))] = &[
21    ("1", |node, radius| {
22        node.width = px(164);
23        node.height = px(164);
24        *radius = BorderRadius::ZERO;
25    }),
26    ("2", |node, radius| {
27        node.width = px(164);
28        node.height = px(164);
29        *radius = BorderRadius::all(px(41));
30    }),
31    ("3", |node, radius| {
32        node.width = px(164);
33        node.height = px(164);
34        *radius = BorderRadius::MAX;
35    }),
36    ("4", |node, radius| {
37        node.width = px(240);
38        node.height = px(80);
39        *radius = BorderRadius::all(px(32));
40    }),
41    ("5", |node, radius| {
42        node.width = px(80);
43        node.height = px(240);
44        *radius = BorderRadius::all(px(32));
45    }),
46];
47
48#[derive(Resource, Default)]
49struct ShapeSettings {
50    index: usize,
51}
52
53#[derive(Resource, Default)]
54struct ShadowSettings {
55    x_offset: f32,
56    y_offset: f32,
57    blur: f32,
58    spread: f32,
59    count: usize,
60    samples: u32,
61}
62
63#[derive(Component)]
64struct ShadowNode;
65
66#[derive(Component, PartialEq, Clone, Copy)]
67enum SettingsButton {
68    XOffsetInc,
69    XOffsetDec,
70    YOffsetInc,
71    YOffsetDec,
72    BlurInc,
73    BlurDec,
74    SpreadInc,
75    SpreadDec,
76    CountInc,
77    CountDec,
78    ShapePrev,
79    ShapeNext,
80    Reset,
81    SamplesInc,
82    SamplesDec,
83}
84
85#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
86enum SettingType {
87    XOffset,
88    YOffset,
89    Blur,
90    Spread,
91    Count,
92    Shape,
93    Samples,
94}
95
96impl SettingType {
97    fn label(&self) -> &str {
98        match self {
99            SettingType::XOffset => "X Offset",
100            SettingType::YOffset => "Y Offset",
101            SettingType::Blur => "Blur",
102            SettingType::Spread => "Spread",
103            SettingType::Count => "Count",
104            SettingType::Shape => "Shape",
105            SettingType::Samples => "Samples",
106        }
107    }
108}
109
110#[derive(Resource, Default)]
111struct HeldButton {
112    button: Option<SettingsButton>,
113    pressed_at: Option<f64>,
114    last_repeat: Option<f64>,
115}
116
117fn main() {
118    App::new()
119        .add_plugins(DefaultPlugins)
120        .insert_resource(SHADOW_DEFAULT_SETTINGS)
121        .insert_resource(SHAPE_DEFAULT_SETTINGS)
122        .insert_resource(HeldButton::default())
123        .add_systems(Startup, setup)
124        .add_systems(
125            Update,
126            (
127                button_system,
128                button_color_system,
129                update_shape.run_if(resource_changed::<ShapeSettings>),
130                update_shadow.run_if(resource_changed::<ShadowSettings>),
131                update_shadow_samples.run_if(resource_changed::<ShadowSettings>),
132                button_repeat_system,
133            ),
134        )
135        .run();
136}
137
138// --- UI Setup ---
139fn setup(
140    mut commands: Commands,
141    asset_server: Res<AssetServer>,
142    shadow: Res<ShadowSettings>,
143    shape: Res<ShapeSettings>,
144) {
145    commands.spawn((Camera2d, BoxShadowSamples(shadow.samples)));
146    // Spawn shape node
147    commands
148        .spawn((
149            Node {
150                width: percent(100),
151                height: percent(100),
152                align_items: AlignItems::Center,
153                justify_content: JustifyContent::Center,
154                ..default()
155            },
156            BackgroundColor(GRAY.into()),
157        ))
158        .insert(children![{
159            let mut node = Node {
160                width: px(164),
161                height: px(164),
162                border: UiRect::all(px(1)),
163                align_items: AlignItems::Center,
164                justify_content: JustifyContent::Center,
165                ..default()
166            };
167            let mut radius = BorderRadius::ZERO;
168            SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
169
170            (
171                node,
172                BorderColor::all(WHITE),
173                radius,
174                BackgroundColor(Color::srgb(0.21, 0.21, 0.21)),
175                BoxShadow(vec![ShadowStyle {
176                    color: Color::BLACK.with_alpha(0.8),
177                    x_offset: px(shadow.x_offset),
178                    y_offset: px(shadow.y_offset),
179                    spread_radius: px(shadow.spread),
180                    blur_radius: px(shadow.blur),
181                }]),
182                ShadowNode,
183            )
184        }]);
185
186    // Settings Panel
187    commands
188        .spawn((
189            Node {
190                flex_direction: FlexDirection::Column,
191                position_type: PositionType::Absolute,
192                left: px(24),
193                bottom: px(24),
194                width: px(270),
195                padding: UiRect::all(px(16)),
196                ..default()
197            },
198            BackgroundColor(Color::srgb(0.12, 0.12, 0.12).with_alpha(0.85)),
199            BorderColor::all(Color::WHITE.with_alpha(0.15)),
200            BorderRadius::all(px(12)),
201            ZIndex(10),
202        ))
203        .insert(children![
204            build_setting_row(
205                SettingType::Shape,
206                SettingsButton::ShapePrev,
207                SettingsButton::ShapeNext,
208                shape.index as f32,
209                &asset_server,
210            ),
211            build_setting_row(
212                SettingType::XOffset,
213                SettingsButton::XOffsetDec,
214                SettingsButton::XOffsetInc,
215                shadow.x_offset,
216                &asset_server,
217            ),
218            build_setting_row(
219                SettingType::YOffset,
220                SettingsButton::YOffsetDec,
221                SettingsButton::YOffsetInc,
222                shadow.y_offset,
223                &asset_server,
224            ),
225            build_setting_row(
226                SettingType::Blur,
227                SettingsButton::BlurDec,
228                SettingsButton::BlurInc,
229                shadow.blur,
230                &asset_server,
231            ),
232            build_setting_row(
233                SettingType::Spread,
234                SettingsButton::SpreadDec,
235                SettingsButton::SpreadInc,
236                shadow.spread,
237                &asset_server,
238            ),
239            build_setting_row(
240                SettingType::Count,
241                SettingsButton::CountDec,
242                SettingsButton::CountInc,
243                shadow.count as f32,
244                &asset_server,
245            ),
246            // Add BoxShadowSamples as a setting row
247            build_setting_row(
248                SettingType::Samples,
249                SettingsButton::SamplesDec,
250                SettingsButton::SamplesInc,
251                shadow.samples as f32,
252                &asset_server,
253            ),
254            // Reset button
255            (
256                Node {
257                    flex_direction: FlexDirection::Row,
258                    align_items: AlignItems::Center,
259                    height: px(36),
260                    margin: UiRect::top(px(12)),
261                    ..default()
262                },
263                children![(
264                    Button,
265                    Node {
266                        width: px(90),
267                        height: px(32),
268                        justify_content: JustifyContent::Center,
269                        align_items: AlignItems::Center,
270                        ..default()
271                    },
272                    BackgroundColor(NORMAL_BUTTON),
273                    BorderRadius::all(px(8)),
274                    SettingsButton::Reset,
275                    children![(
276                        Text::new("Reset"),
277                        TextFont {
278                            font: asset_server.load("fonts/FiraSans-Bold.ttf"),
279                            font_size: 16.0,
280                            ..default()
281                        },
282                    )],
283                )],
284            ),
285        ]);
286}
287
288// --- UI Helper Functions ---
289
290// Helper to return an input to the children! macro for a setting row
291fn build_setting_row(
292    setting_type: SettingType,
293    dec: SettingsButton,
294    inc: SettingsButton,
295    value: f32,
296    asset_server: &Res<AssetServer>,
297) -> impl Bundle {
298    let value_text = match setting_type {
299        SettingType::Shape => SHAPES[value as usize % SHAPES.len()].0.to_string(),
300        SettingType::Count => format!("{}", value as usize),
301        _ => format!("{value:.1}"),
302    };
303
304    (
305        Node {
306            flex_direction: FlexDirection::Row,
307            align_items: AlignItems::Center,
308            height: px(32),
309            ..default()
310        },
311        children![
312            (
313                Node {
314                    width: px(80),
315                    justify_content: JustifyContent::FlexEnd,
316                    align_items: AlignItems::Center,
317                    ..default()
318                },
319                // Attach SettingType to the value label node, not the parent row
320                children![(
321                    Text::new(setting_type.label()),
322                    TextFont {
323                        font: asset_server.load("fonts/FiraSans-Bold.ttf"),
324                        font_size: 16.0,
325                        ..default()
326                    },
327                )],
328            ),
329            (
330                Button,
331                Node {
332                    width: px(28),
333                    height: px(28),
334                    margin: UiRect::left(px(8)),
335                    justify_content: JustifyContent::Center,
336                    align_items: AlignItems::Center,
337                    ..default()
338                },
339                BackgroundColor(Color::WHITE),
340                BorderRadius::all(px(6)),
341                dec,
342                children![(
343                    Text::new(if setting_type == SettingType::Shape {
344                        "<"
345                    } else {
346                        "-"
347                    }),
348                    TextFont {
349                        font: asset_server.load("fonts/FiraSans-Bold.ttf"),
350                        font_size: 18.0,
351                        ..default()
352                    },
353                )],
354            ),
355            (
356                Node {
357                    width: px(48),
358                    height: px(28),
359                    margin: UiRect::horizontal(px(8)),
360                    justify_content: JustifyContent::Center,
361                    align_items: AlignItems::Center,
362                    ..default()
363                },
364                BorderRadius::all(px(6)),
365                children![{
366                    (
367                        Text::new(value_text),
368                        TextFont {
369                            font: asset_server.load("fonts/FiraSans-Bold.ttf"),
370                            font_size: 16.0,
371                            ..default()
372                        },
373                        setting_type,
374                    )
375                }],
376            ),
377            (
378                Button,
379                Node {
380                    width: px(28),
381                    height: px(28),
382                    justify_content: JustifyContent::Center,
383                    align_items: AlignItems::Center,
384                    ..default()
385                },
386                BackgroundColor(Color::WHITE),
387                BorderRadius::all(px(6)),
388                inc,
389                children![(
390                    Text::new(if setting_type == SettingType::Shape {
391                        ">"
392                    } else {
393                        "+"
394                    }),
395                    TextFont {
396                        font: asset_server.load("fonts/FiraSans-Bold.ttf"),
397                        font_size: 18.0,
398                        ..default()
399                    },
400                )],
401            ),
402        ],
403    )
404}
405
406// --- SYSTEMS ---
407
408// Update the shadow node's BoxShadow on resource changes
409fn update_shadow(
410    shadow: Res<ShadowSettings>,
411    mut query: Query<&mut BoxShadow, With<ShadowNode>>,
412    mut label_query: Query<(&mut Text, &SettingType)>,
413) {
414    for mut box_shadow in &mut query {
415        *box_shadow = BoxShadow(generate_shadows(&shadow));
416    }
417    // Update value labels for shadow settings
418    for (mut text, setting) in &mut label_query {
419        let value = match setting {
420            SettingType::XOffset => format!("{:.1}", shadow.x_offset),
421            SettingType::YOffset => format!("{:.1}", shadow.y_offset),
422            SettingType::Blur => format!("{:.1}", shadow.blur),
423            SettingType::Spread => format!("{:.1}", shadow.spread),
424            SettingType::Count => format!("{}", shadow.count),
425            SettingType::Shape => continue,
426            SettingType::Samples => format!("{}", shadow.samples),
427        };
428        *text = Text::new(value);
429    }
430}
431
432fn update_shadow_samples(
433    shadow: Res<ShadowSettings>,
434    mut query: Query<&mut BoxShadowSamples, With<Camera2d>>,
435) {
436    for mut samples in &mut query {
437        samples.0 = shadow.samples;
438    }
439}
440
441fn generate_shadows(shadow: &ShadowSettings) -> Vec<ShadowStyle> {
442    match shadow.count {
443        1 => vec![make_shadow(
444            BLACK.into(),
445            shadow.x_offset,
446            shadow.y_offset,
447            shadow.spread,
448            shadow.blur,
449        )],
450        2 => vec![
451            make_shadow(
452                BLUE.into(),
453                shadow.x_offset,
454                shadow.y_offset,
455                shadow.spread,
456                shadow.blur,
457            ),
458            make_shadow(
459                YELLOW.into(),
460                -shadow.x_offset,
461                -shadow.y_offset,
462                shadow.spread,
463                shadow.blur,
464            ),
465        ],
466        3 => vec![
467            make_shadow(
468                BLUE.into(),
469                shadow.x_offset,
470                shadow.y_offset,
471                shadow.spread,
472                shadow.blur,
473            ),
474            make_shadow(
475                YELLOW.into(),
476                -shadow.x_offset,
477                -shadow.y_offset,
478                shadow.spread,
479                shadow.blur,
480            ),
481            make_shadow(
482                RED.into(),
483                shadow.y_offset,
484                -shadow.x_offset,
485                shadow.spread,
486                shadow.blur,
487            ),
488        ],
489        _ => vec![],
490    }
491}
492
493fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
494    ShadowStyle {
495        color: color.with_alpha(0.8),
496        x_offset: px(x_offset),
497        y_offset: px(y_offset),
498        spread_radius: px(spread),
499        blur_radius: px(blur),
500    }
501}
502
503// Update shape of ShadowNode if shape selection changed
504fn update_shape(
505    shape: Res<ShapeSettings>,
506    mut query: Query<(&mut Node, &mut BorderRadius), With<ShadowNode>>,
507    mut label_query: Query<(&mut Text, &SettingType)>,
508) {
509    for (mut node, mut radius) in &mut query {
510        SHAPES[shape.index % SHAPES.len()].1(&mut node, &mut radius);
511    }
512    for (mut text, kind) in &mut label_query {
513        if *kind == SettingType::Shape {
514            *text = Text::new(SHAPES[shape.index % SHAPES.len()].0);
515        }
516    }
517}
518
519// Handles button interactions for all settings
520fn button_system(
521    mut interaction_query: Query<
522        (&Interaction, &SettingsButton),
523        (Changed<Interaction>, With<Button>),
524    >,
525    mut shadow: ResMut<ShadowSettings>,
526    mut shape: ResMut<ShapeSettings>,
527    mut held: ResMut<HeldButton>,
528    time: Res<Time>,
529) {
530    let now = time.elapsed_secs_f64();
531    for (interaction, btn) in &mut interaction_query {
532        match *interaction {
533            Interaction::Pressed => {
534                trigger_button_action(btn, &mut shadow, &mut shape);
535                held.button = Some(*btn);
536                held.pressed_at = Some(now);
537                held.last_repeat = Some(now);
538            }
539            Interaction::None | Interaction::Hovered => {
540                if held.button == Some(*btn) {
541                    held.button = None;
542                    held.pressed_at = None;
543                    held.last_repeat = None;
544                }
545            }
546        }
547    }
548}
549
550fn trigger_button_action(
551    btn: &SettingsButton,
552    shadow: &mut ShadowSettings,
553    shape: &mut ShapeSettings,
554) {
555    match btn {
556        SettingsButton::XOffsetInc => shadow.x_offset += 1.0,
557        SettingsButton::XOffsetDec => shadow.x_offset -= 1.0,
558        SettingsButton::YOffsetInc => shadow.y_offset += 1.0,
559        SettingsButton::YOffsetDec => shadow.y_offset -= 1.0,
560        SettingsButton::BlurInc => shadow.blur = (shadow.blur + 1.0).max(0.0),
561        SettingsButton::BlurDec => shadow.blur = (shadow.blur - 1.0).max(0.0),
562        SettingsButton::SpreadInc => shadow.spread += 1.0,
563        SettingsButton::SpreadDec => shadow.spread -= 1.0,
564        SettingsButton::CountInc => {
565            if shadow.count < 3 {
566                shadow.count += 1;
567            }
568        }
569        SettingsButton::CountDec => {
570            if shadow.count > 1 {
571                shadow.count -= 1;
572            }
573        }
574        SettingsButton::ShapePrev => {
575            if shape.index == 0 {
576                shape.index = SHAPES.len() - 1;
577            } else {
578                shape.index -= 1;
579            }
580        }
581        SettingsButton::ShapeNext => {
582            shape.index = (shape.index + 1) % SHAPES.len();
583        }
584        SettingsButton::Reset => {
585            *shape = SHAPE_DEFAULT_SETTINGS;
586            *shadow = SHADOW_DEFAULT_SETTINGS;
587        }
588        SettingsButton::SamplesInc => shadow.samples += 1,
589        SettingsButton::SamplesDec => {
590            if shadow.samples > 1 {
591                shadow.samples -= 1;
592            }
593        }
594    }
595}
596
597// System to repeat button action while held
598fn button_repeat_system(
599    time: Res<Time>,
600    mut held: ResMut<HeldButton>,
601    mut shadow: ResMut<ShadowSettings>,
602    mut shape: ResMut<ShapeSettings>,
603    mut request_redraw_writer: MessageWriter<RequestRedraw>,
604) {
605    if held.button.is_some() {
606        request_redraw_writer.write(RequestRedraw);
607    }
608    const INITIAL_DELAY: f64 = 0.15;
609    const REPEAT_RATE: f64 = 0.08;
610    if let (Some(btn), Some(pressed_at)) = (held.button, held.pressed_at) {
611        let now = time.elapsed_secs_f64();
612        let since_pressed = now - pressed_at;
613        let last_repeat = held.last_repeat.unwrap_or(pressed_at);
614        let since_last = now - last_repeat;
615        if since_pressed > INITIAL_DELAY && since_last > REPEAT_RATE {
616            trigger_button_action(&btn, &mut shadow, &mut shape);
617            held.last_repeat = Some(now);
618        }
619    }
620}
621
622// Changes color of button on hover and on pressed
623fn button_color_system(
624    mut query: Query<
625        (&Interaction, &mut BackgroundColor),
626        (Changed<Interaction>, With<Button>, With<SettingsButton>),
627    >,
628) {
629    for (interaction, mut color) in &mut query {
630        match *interaction {
631            Interaction::Pressed => *color = PRESSED_BUTTON.into(),
632            Interaction::Hovered => *color = HOVERED_BUTTON.into(),
633            Interaction::None => *color = NORMAL_BUTTON.into(),
634        }
635    }
636}