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