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