Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/styling/box_shadow.rs
9331 views
1
//! This example shows how to create a node with a shadow and adjust its settings interactively.
2
3
use bevy::{color::palettes::css::*, prelude::*, time::Time, window::RequestRedraw};
4
5
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
6
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
7
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
8
9
const SHAPE_DEFAULT_SETTINGS: ShapeSettings = ShapeSettings { index: 0 };
10
11
const 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
20
const 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)]
49
struct ShapeSettings {
50
index: usize,
51
}
52
53
#[derive(Resource, Default)]
54
struct 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)]
64
struct ShadowNode;
65
66
#[derive(Component, PartialEq, Clone, Copy)]
67
enum 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)]
86
enum SettingType {
87
XOffset,
88
YOffset,
89
Blur,
90
Spread,
91
Count,
92
Shape,
93
Samples,
94
}
95
96
impl 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)]
111
struct HeldButton {
112
button: Option<SettingsButton>,
113
pressed_at: Option<f64>,
114
last_repeat: Option<f64>,
115
}
116
117
fn 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 ---
139
fn 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
290
fn 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
408
fn 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
431
fn 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
440
fn 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
492
fn 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
503
fn 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
519
fn 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
549
fn 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
597
fn 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
622
fn 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
}
636
637