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 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))] = &[
21    ("1", |node| {
22        node.width = px(164);
23        node.height = px(164);
24        node.border_radius = BorderRadius::ZERO;
25    }),
26    ("2", |node| {
27        node.width = px(164);
28        node.height = px(164);
29        node.border_radius = BorderRadius::all(px(41));
30    }),
31    ("3", |node| {
32        node.width = px(164);
33        node.height = px(164);
34        node.border_radius = BorderRadius::MAX;
35    }),
36    ("4", |node| {
37        node.width = px(240);
38        node.height = px(80);
39        node.border_radius = BorderRadius::all(px(32));
40    }),
41    ("5", |node| {
42        node.width = px(80);
43        node.height = px(240);
44        node.border_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                border_radius: BorderRadius::ZERO,
166                ..default()
167            };
168            SHAPES[shape.index % SHAPES.len()].1(&mut node);
169
170            (
171                node,
172                BorderColor::all(WHITE),
173                BackgroundColor(Color::srgb(0.21, 0.21, 0.21)),
174                BoxShadow(vec![ShadowStyle {
175                    color: Color::BLACK.with_alpha(0.8),
176                    x_offset: px(shadow.x_offset),
177                    y_offset: px(shadow.y_offset),
178                    spread_radius: px(shadow.spread),
179                    blur_radius: px(shadow.blur),
180                }]),
181                ShadowNode,
182            )
183        }]);
184
185    // Settings Panel
186    commands
187        .spawn((
188            Node {
189                flex_direction: FlexDirection::Column,
190                position_type: PositionType::Absolute,
191                left: px(24),
192                bottom: px(24),
193                width: px(270),
194                padding: UiRect::all(px(16)),
195                border_radius: BorderRadius::all(px(12)),
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            ZIndex(10),
201        ))
202        .insert(children![
203            build_setting_row(
204                SettingType::Shape,
205                SettingsButton::ShapePrev,
206                SettingsButton::ShapeNext,
207                shape.index as f32,
208                &asset_server,
209            ),
210            build_setting_row(
211                SettingType::XOffset,
212                SettingsButton::XOffsetDec,
213                SettingsButton::XOffsetInc,
214                shadow.x_offset,
215                &asset_server,
216            ),
217            build_setting_row(
218                SettingType::YOffset,
219                SettingsButton::YOffsetDec,
220                SettingsButton::YOffsetInc,
221                shadow.y_offset,
222                &asset_server,
223            ),
224            build_setting_row(
225                SettingType::Blur,
226                SettingsButton::BlurDec,
227                SettingsButton::BlurInc,
228                shadow.blur,
229                &asset_server,
230            ),
231            build_setting_row(
232                SettingType::Spread,
233                SettingsButton::SpreadDec,
234                SettingsButton::SpreadInc,
235                shadow.spread,
236                &asset_server,
237            ),
238            build_setting_row(
239                SettingType::Count,
240                SettingsButton::CountDec,
241                SettingsButton::CountInc,
242                shadow.count as f32,
243                &asset_server,
244            ),
245            // Add BoxShadowSamples as a setting row
246            build_setting_row(
247                SettingType::Samples,
248                SettingsButton::SamplesDec,
249                SettingsButton::SamplesInc,
250                shadow.samples as f32,
251                &asset_server,
252            ),
253            // Reset button
254            (
255                Node {
256                    flex_direction: FlexDirection::Row,
257                    align_items: AlignItems::Center,
258                    height: px(36),
259                    margin: UiRect::top(px(12)),
260                    ..default()
261                },
262                children![(
263                    Button,
264                    Node {
265                        width: px(90),
266                        height: px(32),
267                        justify_content: JustifyContent::Center,
268                        align_items: AlignItems::Center,
269                        border_radius: BorderRadius::all(px(8)),
270                        ..default()
271                    },
272                    BackgroundColor(NORMAL_BUTTON),
273                    SettingsButton::Reset,
274                    children![(
275                        Text::new("Reset"),
276                        TextFont {
277                            font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
278                            font_size: FontSize::Px(16.0),
279                            ..default()
280                        },
281                    )],
282                )],
283            ),
284        ]);
285}
286
287// --- UI Helper Functions ---
288
289// Helper to return an input to the children! macro for a setting row
290fn build_setting_row(
291    setting_type: SettingType,
292    dec: SettingsButton,
293    inc: SettingsButton,
294    value: f32,
295    asset_server: &Res<AssetServer>,
296) -> impl Bundle {
297    let value_text = match setting_type {
298        SettingType::Shape => SHAPES[value as usize % SHAPES.len()].0.to_string(),
299        SettingType::Count => format!("{}", value as usize),
300        _ => format!("{value:.1}"),
301    };
302
303    (
304        Node {
305            flex_direction: FlexDirection::Row,
306            align_items: AlignItems::Center,
307            height: px(32),
308            ..default()
309        },
310        children![
311            (
312                Node {
313                    width: px(80),
314                    justify_content: JustifyContent::FlexEnd,
315                    align_items: AlignItems::Center,
316                    ..default()
317                },
318                // Attach SettingType to the value label node, not the parent row
319                children![(
320                    Text::new(setting_type.label()),
321                    TextFont {
322                        font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
323                        font_size: FontSize::Px(16.0),
324                        ..default()
325                    },
326                )],
327            ),
328            (
329                Button,
330                Node {
331                    width: px(28),
332                    height: px(28),
333                    margin: UiRect::left(px(8)),
334                    justify_content: JustifyContent::Center,
335                    align_items: AlignItems::Center,
336                    border_radius: BorderRadius::all(px(6)),
337                    ..default()
338                },
339                BackgroundColor(Color::WHITE),
340                dec,
341                children![(
342                    Text::new(if setting_type == SettingType::Shape {
343                        "<"
344                    } else {
345                        "-"
346                    }),
347                    TextFont {
348                        font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
349                        font_size: FontSize::Px(18.0),
350                        ..default()
351                    },
352                )],
353            ),
354            (
355                Node {
356                    width: px(48),
357                    height: px(28),
358                    margin: UiRect::horizontal(px(8)),
359                    justify_content: JustifyContent::Center,
360                    align_items: AlignItems::Center,
361                    border_radius: BorderRadius::all(px(6)),
362                    ..default()
363                },
364                children![{
365                    (
366                        Text::new(value_text),
367                        TextFont {
368                            font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
369                            font_size: FontSize::Px(16.0),
370                            ..default()
371                        },
372                        setting_type,
373                    )
374                }],
375            ),
376            (
377                Button,
378                Node {
379                    width: px(28),
380                    height: px(28),
381                    justify_content: JustifyContent::Center,
382                    align_items: AlignItems::Center,
383                    border_radius: BorderRadius::all(px(6)),
384                    ..default()
385                },
386                BackgroundColor(Color::WHITE),
387                inc,
388                children![(
389                    Text::new(if setting_type == SettingType::Shape {
390                        ">"
391                    } else {
392                        "+"
393                    }),
394                    TextFont {
395                        font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
396                        font_size: FontSize::Px(18.0),
397                        ..default()
398                    },
399                )],
400            ),
401        ],
402    )
403}
404
405// --- SYSTEMS ---
406
407// Update the shadow node's BoxShadow on resource changes
408fn update_shadow(
409    shadow: Res<ShadowSettings>,
410    mut query: Query<&mut BoxShadow, With<ShadowNode>>,
411    mut label_query: Query<(&mut Text, &SettingType)>,
412) {
413    for mut box_shadow in &mut query {
414        *box_shadow = BoxShadow(generate_shadows(&shadow));
415    }
416    // Update value labels for shadow settings
417    for (mut text, setting) in &mut label_query {
418        let value = match setting {
419            SettingType::XOffset => format!("{:.1}", shadow.x_offset),
420            SettingType::YOffset => format!("{:.1}", shadow.y_offset),
421            SettingType::Blur => format!("{:.1}", shadow.blur),
422            SettingType::Spread => format!("{:.1}", shadow.spread),
423            SettingType::Count => format!("{}", shadow.count),
424            SettingType::Shape => continue,
425            SettingType::Samples => format!("{}", shadow.samples),
426        };
427        *text = Text::new(value);
428    }
429}
430
431fn update_shadow_samples(
432    shadow: Res<ShadowSettings>,
433    mut query: Query<&mut BoxShadowSamples, With<Camera2d>>,
434) {
435    for mut samples in &mut query {
436        samples.0 = shadow.samples;
437    }
438}
439
440fn generate_shadows(shadow: &ShadowSettings) -> Vec<ShadowStyle> {
441    match shadow.count {
442        1 => vec![make_shadow(
443            BLACK.into(),
444            shadow.x_offset,
445            shadow.y_offset,
446            shadow.spread,
447            shadow.blur,
448        )],
449        2 => vec![
450            make_shadow(
451                BLUE.into(),
452                shadow.x_offset,
453                shadow.y_offset,
454                shadow.spread,
455                shadow.blur,
456            ),
457            make_shadow(
458                YELLOW.into(),
459                -shadow.x_offset,
460                -shadow.y_offset,
461                shadow.spread,
462                shadow.blur,
463            ),
464        ],
465        3 => vec![
466            make_shadow(
467                BLUE.into(),
468                shadow.x_offset,
469                shadow.y_offset,
470                shadow.spread,
471                shadow.blur,
472            ),
473            make_shadow(
474                YELLOW.into(),
475                -shadow.x_offset,
476                -shadow.y_offset,
477                shadow.spread,
478                shadow.blur,
479            ),
480            make_shadow(
481                RED.into(),
482                shadow.y_offset,
483                -shadow.x_offset,
484                shadow.spread,
485                shadow.blur,
486            ),
487        ],
488        _ => vec![],
489    }
490}
491
492fn make_shadow(color: Color, x_offset: f32, y_offset: f32, spread: f32, blur: f32) -> ShadowStyle {
493    ShadowStyle {
494        color: color.with_alpha(0.8),
495        x_offset: px(x_offset),
496        y_offset: px(y_offset),
497        spread_radius: px(spread),
498        blur_radius: px(blur),
499    }
500}
501
502// Update shape of ShadowNode if shape selection changed
503fn update_shape(
504    shape: Res<ShapeSettings>,
505    mut query: Query<&mut Node, With<ShadowNode>>,
506    mut label_query: Query<(&mut Text, &SettingType)>,
507) {
508    for mut node in &mut query {
509        SHAPES[shape.index % SHAPES.len()].1(&mut node);
510    }
511    for (mut text, kind) in &mut label_query {
512        if *kind == SettingType::Shape {
513            *text = Text::new(SHAPES[shape.index % SHAPES.len()].0);
514        }
515    }
516}
517
518// Handles button interactions for all settings
519fn button_system(
520    mut interaction_query: Query<
521        (&Interaction, &SettingsButton),
522        (Changed<Interaction>, With<Button>),
523    >,
524    mut shadow: ResMut<ShadowSettings>,
525    mut shape: ResMut<ShapeSettings>,
526    mut held: ResMut<HeldButton>,
527    time: Res<Time>,
528) {
529    let now = time.elapsed_secs_f64();
530    for (interaction, btn) in &mut interaction_query {
531        match *interaction {
532            Interaction::Pressed => {
533                trigger_button_action(btn, &mut shadow, &mut shape);
534                held.button = Some(*btn);
535                held.pressed_at = Some(now);
536                held.last_repeat = Some(now);
537            }
538            Interaction::None | Interaction::Hovered => {
539                if held.button == Some(*btn) {
540                    held.button = None;
541                    held.pressed_at = None;
542                    held.last_repeat = None;
543                }
544            }
545        }
546    }
547}
548
549fn trigger_button_action(
550    btn: &SettingsButton,
551    shadow: &mut ShadowSettings,
552    shape: &mut ShapeSettings,
553) {
554    match btn {
555        SettingsButton::XOffsetInc => shadow.x_offset += 1.0,
556        SettingsButton::XOffsetDec => shadow.x_offset -= 1.0,
557        SettingsButton::YOffsetInc => shadow.y_offset += 1.0,
558        SettingsButton::YOffsetDec => shadow.y_offset -= 1.0,
559        SettingsButton::BlurInc => shadow.blur = (shadow.blur + 1.0).max(0.0),
560        SettingsButton::BlurDec => shadow.blur = (shadow.blur - 1.0).max(0.0),
561        SettingsButton::SpreadInc => shadow.spread += 1.0,
562        SettingsButton::SpreadDec => shadow.spread -= 1.0,
563        SettingsButton::CountInc => {
564            if shadow.count < 3 {
565                shadow.count += 1;
566            }
567        }
568        SettingsButton::CountDec => {
569            if shadow.count > 1 {
570                shadow.count -= 1;
571            }
572        }
573        SettingsButton::ShapePrev => {
574            if shape.index == 0 {
575                shape.index = SHAPES.len() - 1;
576            } else {
577                shape.index -= 1;
578            }
579        }
580        SettingsButton::ShapeNext => {
581            shape.index = (shape.index + 1) % SHAPES.len();
582        }
583        SettingsButton::Reset => {
584            *shape = SHAPE_DEFAULT_SETTINGS;
585            *shadow = SHADOW_DEFAULT_SETTINGS;
586        }
587        SettingsButton::SamplesInc => shadow.samples += 1,
588        SettingsButton::SamplesDec => {
589            if shadow.samples > 1 {
590                shadow.samples -= 1;
591            }
592        }
593    }
594}
595
596// System to repeat button action while held
597fn button_repeat_system(
598    time: Res<Time>,
599    mut held: ResMut<HeldButton>,
600    mut shadow: ResMut<ShadowSettings>,
601    mut shape: ResMut<ShapeSettings>,
602    mut request_redraw_writer: MessageWriter<RequestRedraw>,
603) {
604    if held.button.is_some() {
605        request_redraw_writer.write(RequestRedraw);
606    }
607    const INITIAL_DELAY: f64 = 0.15;
608    const REPEAT_RATE: f64 = 0.08;
609    if let (Some(btn), Some(pressed_at)) = (held.button, held.pressed_at) {
610        let now = time.elapsed_secs_f64();
611        let since_pressed = now - pressed_at;
612        let last_repeat = held.last_repeat.unwrap_or(pressed_at);
613        let since_last = now - last_repeat;
614        if since_pressed > INITIAL_DELAY && since_last > REPEAT_RATE {
615            trigger_button_action(&btn, &mut shadow, &mut shape);
616            held.last_repeat = Some(now);
617        }
618    }
619}
620
621// Changes color of button on hover and on pressed
622fn button_color_system(
623    mut query: Query<
624        (&Interaction, &mut BackgroundColor),
625        (Changed<Interaction>, With<Button>, With<SettingsButton>),
626    >,
627) {
628    for (interaction, mut color) in &mut query {
629        match *interaction {
630            Interaction::Pressed => *color = PRESSED_BUTTON.into(),
631            Interaction::Hovered => *color = HOVERED_BUTTON.into(),
632            Interaction::None => *color = NORMAL_BUTTON.into(),
633        }
634    }
635}