Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/box_shadow.rs
6595 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, &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)]
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
..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
291
fn 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
409
fn 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
432
fn 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
441
fn 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
493
fn 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
504
fn 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
520
fn 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
550
fn 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
598
fn button_repeat_system(
599
time: Res<Time>,
600
mut held: ResMut<HeldButton>,
601
mut shadow: ResMut<ShadowSettings>,
602
mut shape: ResMut<ShapeSettings>,
603
mut redraw_events: EventWriter<RequestRedraw>,
604
) {
605
if held.button.is_some() {
606
redraw_events.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
623
fn 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
}
637
638