Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/core_widgets_observers.rs
6595 views
1
//! This example illustrates how to create widgets using the `bevy_core_widgets` widget set.
2
3
use bevy::{
4
color::palettes::basic::*,
5
core_widgets::{
6
Activate, Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb,
7
CoreWidgetsPlugins, SliderRange, SliderValue, ValueChange,
8
},
9
ecs::system::SystemId,
10
input_focus::{
11
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
12
InputDispatchPlugin,
13
},
14
picking::hover::Hovered,
15
prelude::*,
16
ui::{Checked, InteractionDisabled, Pressed},
17
};
18
19
fn main() {
20
App::new()
21
.add_plugins((
22
DefaultPlugins,
23
CoreWidgetsPlugins,
24
InputDispatchPlugin,
25
TabNavigationPlugin,
26
))
27
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
28
.add_systems(Startup, setup)
29
.add_observer(button_on_add_pressed)
30
.add_observer(button_on_remove_pressed)
31
.add_observer(button_on_add_disabled)
32
.add_observer(button_on_remove_disabled)
33
.add_observer(button_on_change_hover)
34
.add_observer(slider_on_add_disabled)
35
.add_observer(slider_on_remove_disabled)
36
.add_observer(slider_on_change_hover)
37
.add_observer(slider_on_change_value)
38
.add_observer(slider_on_change_range)
39
.add_observer(checkbox_on_add_disabled)
40
.add_observer(checkbox_on_remove_disabled)
41
.add_observer(checkbox_on_change_hover)
42
.add_observer(checkbox_on_add_checked)
43
.add_observer(checkbox_on_remove_checked)
44
.add_systems(Update, (update_widget_values, toggle_disabled))
45
.run();
46
}
47
48
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
49
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
50
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
51
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
52
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
53
const CHECKBOX_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
54
const CHECKBOX_CHECK: Color = Color::srgb(0.35, 0.75, 0.35);
55
56
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
57
#[derive(Component)]
58
struct DemoButton;
59
60
/// Marker which identifies sliders with a particular style.
61
#[derive(Component, Default)]
62
struct DemoSlider;
63
64
/// Marker which identifies the slider's thumb element.
65
#[derive(Component, Default)]
66
struct DemoSliderThumb;
67
68
/// Marker which identifies checkboxes with a particular style.
69
#[derive(Component, Default)]
70
struct DemoCheckbox;
71
72
/// A struct to hold the state of various widgets shown in the demo.
73
///
74
/// While it is possible to use the widget's own state components as the source of truth,
75
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
76
/// using some kind of data-binding. This example shows how to maintain an external source of
77
/// truth for widget states.
78
#[derive(Resource)]
79
struct DemoWidgetStates {
80
slider_value: f32,
81
}
82
83
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
84
// System to print a value when the button is clicked.
85
let on_click = commands.register_system(|_: In<Activate>| {
86
info!("Button clicked!");
87
});
88
89
// System to update a resource when the slider value changes. Note that we could have
90
// updated the slider value directly, but we want to demonstrate externalizing the state.
91
let on_change_value = commands.register_system(
92
|value: In<ValueChange<f32>>, mut widget_states: ResMut<DemoWidgetStates>| {
93
widget_states.slider_value = value.0.value;
94
},
95
);
96
97
// ui camera
98
commands.spawn(Camera2d);
99
commands.spawn(demo_root(&assets, on_click, on_change_value));
100
}
101
102
fn demo_root(
103
asset_server: &AssetServer,
104
on_click: SystemId<In<Activate>>,
105
on_change_value: SystemId<In<ValueChange<f32>>>,
106
) -> impl Bundle {
107
(
108
Node {
109
width: percent(100),
110
height: percent(100),
111
align_items: AlignItems::Center,
112
justify_content: JustifyContent::Center,
113
display: Display::Flex,
114
flex_direction: FlexDirection::Column,
115
row_gap: px(10),
116
..default()
117
},
118
TabGroup::default(),
119
children![
120
button(asset_server, Callback::System(on_click)),
121
slider(0.0, 100.0, 50.0, Callback::System(on_change_value)),
122
checkbox(asset_server, "Checkbox", Callback::Ignore),
123
Text::new("Press 'D' to toggle widget disabled states"),
124
],
125
)
126
}
127
128
fn button(asset_server: &AssetServer, on_click: Callback<In<Activate>>) -> impl Bundle {
129
(
130
Node {
131
width: px(150),
132
height: px(65),
133
border: UiRect::all(px(5)),
134
justify_content: JustifyContent::Center,
135
align_items: AlignItems::Center,
136
..default()
137
},
138
DemoButton,
139
CoreButton {
140
on_activate: on_click,
141
},
142
Hovered::default(),
143
TabIndex(0),
144
BorderColor::all(Color::BLACK),
145
BorderRadius::MAX,
146
BackgroundColor(NORMAL_BUTTON),
147
children![(
148
Text::new("Button"),
149
TextFont {
150
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
151
font_size: 33.0,
152
..default()
153
},
154
TextColor(Color::srgb(0.9, 0.9, 0.9)),
155
TextShadow::default(),
156
)],
157
)
158
}
159
160
fn button_on_add_pressed(
161
event: On<Add, Pressed>,
162
mut buttons: Query<
163
(
164
&Hovered,
165
Has<InteractionDisabled>,
166
&mut BackgroundColor,
167
&mut BorderColor,
168
&Children,
169
),
170
With<DemoButton>,
171
>,
172
mut text_query: Query<&mut Text>,
173
) {
174
if let Ok((hovered, disabled, mut color, mut border_color, children)) =
175
buttons.get_mut(event.entity())
176
{
177
let mut text = text_query.get_mut(children[0]).unwrap();
178
set_button_style(
179
disabled,
180
hovered.get(),
181
true,
182
&mut color,
183
&mut border_color,
184
&mut text,
185
);
186
}
187
}
188
189
fn button_on_remove_pressed(
190
event: On<Remove, Pressed>,
191
mut buttons: Query<
192
(
193
&Hovered,
194
Has<InteractionDisabled>,
195
&mut BackgroundColor,
196
&mut BorderColor,
197
&Children,
198
),
199
With<DemoButton>,
200
>,
201
mut text_query: Query<&mut Text>,
202
) {
203
if let Ok((hovered, disabled, mut color, mut border_color, children)) =
204
buttons.get_mut(event.entity())
205
{
206
let mut text = text_query.get_mut(children[0]).unwrap();
207
set_button_style(
208
disabled,
209
hovered.get(),
210
false,
211
&mut color,
212
&mut border_color,
213
&mut text,
214
);
215
}
216
}
217
218
fn button_on_add_disabled(
219
event: On<Add, InteractionDisabled>,
220
mut buttons: Query<
221
(
222
Has<Pressed>,
223
&Hovered,
224
&mut BackgroundColor,
225
&mut BorderColor,
226
&Children,
227
),
228
With<DemoButton>,
229
>,
230
mut text_query: Query<&mut Text>,
231
) {
232
if let Ok((pressed, hovered, mut color, mut border_color, children)) =
233
buttons.get_mut(event.entity())
234
{
235
let mut text = text_query.get_mut(children[0]).unwrap();
236
set_button_style(
237
true,
238
hovered.get(),
239
pressed,
240
&mut color,
241
&mut border_color,
242
&mut text,
243
);
244
}
245
}
246
247
fn button_on_remove_disabled(
248
event: On<Remove, InteractionDisabled>,
249
mut buttons: Query<
250
(
251
Has<Pressed>,
252
&Hovered,
253
&mut BackgroundColor,
254
&mut BorderColor,
255
&Children,
256
),
257
With<DemoButton>,
258
>,
259
mut text_query: Query<&mut Text>,
260
) {
261
if let Ok((pressed, hovered, mut color, mut border_color, children)) =
262
buttons.get_mut(event.entity())
263
{
264
let mut text = text_query.get_mut(children[0]).unwrap();
265
set_button_style(
266
false,
267
hovered.get(),
268
pressed,
269
&mut color,
270
&mut border_color,
271
&mut text,
272
);
273
}
274
}
275
276
fn button_on_change_hover(
277
event: On<Insert, Hovered>,
278
mut buttons: Query<
279
(
280
Has<Pressed>,
281
&Hovered,
282
Has<InteractionDisabled>,
283
&mut BackgroundColor,
284
&mut BorderColor,
285
&Children,
286
),
287
With<DemoButton>,
288
>,
289
mut text_query: Query<&mut Text>,
290
) {
291
if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) =
292
buttons.get_mut(event.entity())
293
{
294
if children.is_empty() {
295
return;
296
}
297
let Ok(mut text) = text_query.get_mut(children[0]) else {
298
return;
299
};
300
set_button_style(
301
disabled,
302
hovered.get(),
303
pressed,
304
&mut color,
305
&mut border_color,
306
&mut text,
307
);
308
}
309
}
310
311
fn set_button_style(
312
disabled: bool,
313
hovered: bool,
314
pressed: bool,
315
color: &mut BackgroundColor,
316
border_color: &mut BorderColor,
317
text: &mut Text,
318
) {
319
match (disabled, hovered, pressed) {
320
// Disabled button
321
(true, _, _) => {
322
**text = "Disabled".to_string();
323
*color = NORMAL_BUTTON.into();
324
border_color.set_all(GRAY);
325
}
326
327
// Pressed and hovered button
328
(false, true, true) => {
329
**text = "Press".to_string();
330
*color = PRESSED_BUTTON.into();
331
border_color.set_all(RED);
332
}
333
334
// Hovered, unpressed button
335
(false, true, false) => {
336
**text = "Hover".to_string();
337
*color = HOVERED_BUTTON.into();
338
border_color.set_all(WHITE);
339
}
340
341
// Unhovered button (either pressed or not).
342
(false, false, _) => {
343
**text = "Button".to_string();
344
*color = NORMAL_BUTTON.into();
345
border_color.set_all(BLACK);
346
}
347
}
348
}
349
350
/// Create a demo slider
351
fn slider(
352
min: f32,
353
max: f32,
354
value: f32,
355
on_change: Callback<In<ValueChange<f32>>>,
356
) -> impl Bundle {
357
(
358
Node {
359
display: Display::Flex,
360
flex_direction: FlexDirection::Column,
361
justify_content: JustifyContent::Center,
362
align_items: AlignItems::Stretch,
363
justify_items: JustifyItems::Center,
364
column_gap: px(4),
365
height: px(12),
366
width: percent(30),
367
..default()
368
},
369
Name::new("Slider"),
370
Hovered::default(),
371
DemoSlider,
372
CoreSlider {
373
on_change,
374
..default()
375
},
376
SliderValue(value),
377
SliderRange::new(min, max),
378
TabIndex(0),
379
Children::spawn((
380
// Slider background rail
381
Spawn((
382
Node {
383
height: px(6),
384
..default()
385
},
386
BackgroundColor(SLIDER_TRACK), // Border color for the checkbox
387
BorderRadius::all(px(3)),
388
)),
389
// Invisible track to allow absolute placement of thumb entity. This is narrower than
390
// the actual slider, which allows us to position the thumb entity using simple
391
// percentages, without having to measure the actual width of the slider thumb.
392
Spawn((
393
Node {
394
display: Display::Flex,
395
position_type: PositionType::Absolute,
396
left: px(0),
397
// Track is short by 12px to accommodate the thumb.
398
right: px(12),
399
top: px(0),
400
bottom: px(0),
401
..default()
402
},
403
children![(
404
// Thumb
405
DemoSliderThumb,
406
CoreSliderThumb,
407
Node {
408
display: Display::Flex,
409
width: px(12),
410
height: px(12),
411
position_type: PositionType::Absolute,
412
left: percent(0), // This will be updated by the slider's value
413
..default()
414
},
415
BorderRadius::MAX,
416
BackgroundColor(SLIDER_THUMB),
417
)],
418
)),
419
)),
420
)
421
}
422
423
fn slider_on_add_disabled(
424
event: On<Add, InteractionDisabled>,
425
sliders: Query<(Entity, &Hovered), With<DemoSlider>>,
426
children: Query<&Children>,
427
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
428
) {
429
if let Ok((slider_ent, hovered)) = sliders.get(event.entity()) {
430
for child in children.iter_descendants(slider_ent) {
431
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
432
&& is_thumb
433
{
434
thumb_bg.0 = thumb_color(true, hovered.0);
435
}
436
}
437
}
438
}
439
440
fn slider_on_remove_disabled(
441
event: On<Remove, InteractionDisabled>,
442
sliders: Query<(Entity, &Hovered), With<DemoSlider>>,
443
children: Query<&Children>,
444
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
445
) {
446
if let Ok((slider_ent, hovered)) = sliders.get(event.entity()) {
447
for child in children.iter_descendants(slider_ent) {
448
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
449
&& is_thumb
450
{
451
thumb_bg.0 = thumb_color(false, hovered.0);
452
}
453
}
454
}
455
}
456
457
fn slider_on_change_hover(
458
event: On<Insert, Hovered>,
459
sliders: Query<(Entity, &Hovered, Has<InteractionDisabled>), With<DemoSlider>>,
460
children: Query<&Children>,
461
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
462
) {
463
if let Ok((slider_ent, hovered, disabled)) = sliders.get(event.entity()) {
464
for child in children.iter_descendants(slider_ent) {
465
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
466
&& is_thumb
467
{
468
thumb_bg.0 = thumb_color(disabled, hovered.0);
469
}
470
}
471
}
472
}
473
474
fn slider_on_change_value(
475
event: On<Insert, SliderValue>,
476
sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
477
children: Query<&Children>,
478
mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
479
) {
480
if let Ok((slider_ent, value, range)) = sliders.get(event.entity()) {
481
for child in children.iter_descendants(slider_ent) {
482
if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)
483
&& is_thumb
484
{
485
thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
486
}
487
}
488
}
489
}
490
491
fn slider_on_change_range(
492
event: On<Insert, SliderRange>,
493
sliders: Query<(Entity, &SliderValue, &SliderRange), With<DemoSlider>>,
494
children: Query<&Children>,
495
mut thumbs: Query<(&mut Node, Has<DemoSliderThumb>), Without<DemoSlider>>,
496
) {
497
if let Ok((slider_ent, value, range)) = sliders.get(event.entity()) {
498
for child in children.iter_descendants(slider_ent) {
499
if let Ok((mut thumb_node, is_thumb)) = thumbs.get_mut(child)
500
&& is_thumb
501
{
502
thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
503
}
504
}
505
}
506
}
507
508
fn thumb_color(disabled: bool, hovered: bool) -> Color {
509
match (disabled, hovered) {
510
(true, _) => GRAY.into(),
511
512
(false, true) => SLIDER_THUMB.lighter(0.3),
513
514
_ => SLIDER_THUMB,
515
}
516
}
517
518
/// Create a demo checkbox
519
fn checkbox(
520
asset_server: &AssetServer,
521
caption: &str,
522
on_change: Callback<In<ValueChange<bool>>>,
523
) -> impl Bundle {
524
(
525
Node {
526
display: Display::Flex,
527
flex_direction: FlexDirection::Row,
528
justify_content: JustifyContent::FlexStart,
529
align_items: AlignItems::Center,
530
align_content: AlignContent::Center,
531
column_gap: px(4),
532
..default()
533
},
534
Name::new("Checkbox"),
535
Hovered::default(),
536
DemoCheckbox,
537
CoreCheckbox { on_change },
538
TabIndex(0),
539
Children::spawn((
540
Spawn((
541
// Checkbox outer
542
Node {
543
display: Display::Flex,
544
width: px(16),
545
height: px(16),
546
border: UiRect::all(px(2)),
547
..default()
548
},
549
BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox
550
BorderRadius::all(px(3)),
551
children![
552
// Checkbox inner
553
(
554
Node {
555
display: Display::Flex,
556
width: px(8),
557
height: px(8),
558
position_type: PositionType::Absolute,
559
left: px(2),
560
top: px(2),
561
..default()
562
},
563
BackgroundColor(Srgba::NONE.into()),
564
),
565
],
566
)),
567
Spawn((
568
Text::new(caption),
569
TextFont {
570
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
571
font_size: 20.0,
572
..default()
573
},
574
)),
575
)),
576
)
577
}
578
579
fn checkbox_on_add_disabled(
580
event: On<Add, InteractionDisabled>,
581
checkboxes: Query<(&Hovered, Has<Checked>, &Children), With<DemoCheckbox>>,
582
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
583
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
584
) {
585
if let Ok((hovered, checked, children)) = checkboxes.get(event.entity()) {
586
set_checkbox_style(children, &mut borders, &mut marks, true, hovered.0, checked);
587
}
588
}
589
590
fn checkbox_on_remove_disabled(
591
event: On<Remove, InteractionDisabled>,
592
checkboxes: Query<(&Hovered, Has<Checked>, &Children), With<DemoCheckbox>>,
593
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
594
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
595
) {
596
if let Ok((hovered, checked, children)) = checkboxes.get(event.entity()) {
597
set_checkbox_style(
598
children,
599
&mut borders,
600
&mut marks,
601
false,
602
hovered.0,
603
checked,
604
);
605
}
606
}
607
608
fn checkbox_on_change_hover(
609
event: On<Insert, Hovered>,
610
checkboxes: Query<
611
(&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),
612
With<DemoCheckbox>,
613
>,
614
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
615
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
616
) {
617
if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.entity()) {
618
set_checkbox_style(
619
children,
620
&mut borders,
621
&mut marks,
622
disabled,
623
hovered.0,
624
checked,
625
);
626
}
627
}
628
629
fn checkbox_on_add_checked(
630
event: On<Add, Checked>,
631
checkboxes: Query<
632
(&Hovered, Has<InteractionDisabled>, Has<Checked>, &Children),
633
With<DemoCheckbox>,
634
>,
635
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
636
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
637
) {
638
if let Ok((hovered, disabled, checked, children)) = checkboxes.get(event.entity()) {
639
set_checkbox_style(
640
children,
641
&mut borders,
642
&mut marks,
643
disabled,
644
hovered.0,
645
checked,
646
);
647
}
648
}
649
650
fn checkbox_on_remove_checked(
651
event: On<Remove, Checked>,
652
checkboxes: Query<(&Hovered, Has<InteractionDisabled>, &Children), With<DemoCheckbox>>,
653
mut borders: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
654
mut marks: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
655
) {
656
if let Ok((hovered, disabled, children)) = checkboxes.get(event.entity()) {
657
set_checkbox_style(
658
children,
659
&mut borders,
660
&mut marks,
661
disabled,
662
hovered.0,
663
false,
664
);
665
}
666
}
667
668
fn set_checkbox_style(
669
children: &Children,
670
borders: &mut Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
671
marks: &mut Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
672
disabled: bool,
673
hovering: bool,
674
checked: bool,
675
) {
676
let Some(border_id) = children.first() else {
677
return;
678
};
679
680
let Ok((mut border_color, border_children)) = borders.get_mut(*border_id) else {
681
return;
682
};
683
684
let Some(mark_id) = border_children.first() else {
685
warn!("Checkbox does not have a mark entity.");
686
return;
687
};
688
689
let Ok(mut mark_bg) = marks.get_mut(*mark_id) else {
690
warn!("Checkbox mark entity lacking a background color.");
691
return;
692
};
693
694
let color: Color = if disabled {
695
// If the checkbox is disabled, use a lighter color
696
CHECKBOX_OUTLINE.with_alpha(0.2)
697
} else if hovering {
698
// If hovering, use a lighter color
699
CHECKBOX_OUTLINE.lighter(0.2)
700
} else {
701
// Default color for the checkbox
702
CHECKBOX_OUTLINE
703
};
704
705
// Update the background color of the check mark
706
border_color.set_all(color);
707
708
let mark_color: Color = match (disabled, checked) {
709
(true, true) => CHECKBOX_CHECK.with_alpha(0.5),
710
(false, true) => CHECKBOX_CHECK,
711
(_, false) => Srgba::NONE.into(),
712
};
713
714
if mark_bg.0 != mark_color {
715
// Update the color of the check mark
716
mark_bg.0 = mark_color;
717
}
718
}
719
720
/// Update the widget states based on the changing resource.
721
fn update_widget_values(
722
res: Res<DemoWidgetStates>,
723
mut sliders: Query<Entity, With<DemoSlider>>,
724
mut commands: Commands,
725
) {
726
if res.is_changed() {
727
for slider_ent in sliders.iter_mut() {
728
commands
729
.entity(slider_ent)
730
.insert(SliderValue(res.slider_value));
731
}
732
}
733
}
734
735
fn toggle_disabled(
736
input: Res<ButtonInput<KeyCode>>,
737
mut interaction_query: Query<
738
(Entity, Has<InteractionDisabled>),
739
Or<(With<CoreButton>, With<CoreSlider>, With<CoreCheckbox>)>,
740
>,
741
mut commands: Commands,
742
) {
743
if input.just_pressed(KeyCode::KeyD) {
744
for (entity, disabled) in &mut interaction_query {
745
if disabled {
746
info!("Widget enabled");
747
commands.entity(entity).remove::<InteractionDisabled>();
748
} else {
749
info!("Widget disabled");
750
commands.entity(entity).insert(InteractionDisabled);
751
}
752
}
753
}
754
}
755
756