Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/widgets/standard_widgets.rs
9331 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, InputFocus,
14
},
15
picking::hover::Hovered,
16
prelude::*,
17
ui::{Checked, InteractionDisabled, Pressed},
18
ui_widgets::{
19
checkbox_self_update, observe,
20
popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide},
21
Activate, Button, Checkbox, CoreSliderDragState, MenuAction, MenuButton, MenuEvent,
22
MenuItem, MenuPopup, RadioButton, RadioGroup, Slider, SliderRange, SliderThumb,
23
SliderValue, TrackClick, UiWidgetsPlugins, ValueChange,
24
},
25
};
26
27
fn main() {
28
App::new()
29
.add_plugins((
30
DefaultPlugins,
31
UiWidgetsPlugins,
32
InputDispatchPlugin,
33
TabNavigationPlugin,
34
))
35
.insert_resource(DemoWidgetStates {
36
slider_value: 50.0,
37
slider_click: TrackClick::Snap,
38
})
39
.add_systems(Startup, setup)
40
.add_systems(
41
Update,
42
(
43
update_widget_values,
44
update_button_style,
45
update_button_style2,
46
update_slider_style.after(update_widget_values),
47
update_slider_style2.after(update_widget_values),
48
update_checkbox_or_radio_style.after(update_widget_values),
49
update_checkbox_or_radio_style2.after(update_widget_values),
50
update_menu_item_style,
51
update_menu_item_style2,
52
toggle_disabled,
53
),
54
)
55
.run();
56
}
57
58
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
59
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
60
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
61
const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05);
62
const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35);
63
const ELEMENT_OUTLINE: Color = Color::srgb(0.45, 0.45, 0.45);
64
const ELEMENT_FILL: Color = Color::srgb(0.35, 0.75, 0.35);
65
const ELEMENT_FILL_DISABLED: Color = Color::srgb(0.5019608, 0.5019608, 0.5019608);
66
67
/// Marker which identifies buttons with a particular style, in this case the "Demo style".
68
#[derive(Component)]
69
struct DemoButton;
70
71
/// Marker which identifies sliders with a particular style.
72
#[derive(Component, Default)]
73
struct DemoSlider;
74
75
/// Marker which identifies the slider's thumb element.
76
#[derive(Component, Default)]
77
struct DemoSliderThumb;
78
79
/// Marker which identifies checkboxes with a particular style.
80
#[derive(Component, Default)]
81
struct DemoCheckbox;
82
83
/// Marker which identifies a styled radio button. We'll use this to change the track click
84
/// behavior.
85
#[derive(Component, Default)]
86
struct DemoRadio(TrackClick);
87
88
/// Menu anchor marker
89
#[derive(Component)]
90
struct DemoMenuAnchor;
91
92
/// Menu button styling marker
93
#[derive(Component)]
94
struct DemoMenuButton;
95
96
/// Menu item styling marker
97
#[derive(Component)]
98
struct DemoMenuItem;
99
100
/// A struct to hold the state of various widgets shown in the demo.
101
///
102
/// While it is possible to use the widget's own state components as the source of truth,
103
/// in many cases widgets will be used to display dynamic data coming from deeper within the app,
104
/// using some kind of data-binding. This example shows how to maintain an external source of
105
/// truth for widget states.
106
#[derive(Resource)]
107
struct DemoWidgetStates {
108
slider_value: f32,
109
slider_click: TrackClick,
110
}
111
112
/// Update the widget states based on the changing resource.
113
fn update_widget_values(
114
res: Res<DemoWidgetStates>,
115
mut sliders: Query<(Entity, &mut Slider), With<DemoSlider>>,
116
radios: Query<(Entity, &DemoRadio, Has<Checked>)>,
117
mut commands: Commands,
118
) {
119
if res.is_changed() {
120
for (slider_ent, mut slider) in sliders.iter_mut() {
121
commands
122
.entity(slider_ent)
123
.insert(SliderValue(res.slider_value));
124
slider.track_click = res.slider_click;
125
}
126
127
for (radio_id, radio_value, checked) in radios.iter() {
128
let will_be_checked = radio_value.0 == res.slider_click;
129
if will_be_checked != checked {
130
if will_be_checked {
131
commands.entity(radio_id).insert(Checked);
132
} else {
133
commands.entity(radio_id).remove::<Checked>();
134
}
135
}
136
}
137
}
138
}
139
140
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
141
// ui camera
142
commands.spawn(Camera2d);
143
commands.spawn(demo_root(&assets));
144
}
145
146
fn demo_root(asset_server: &AssetServer) -> impl Bundle {
147
(
148
Node {
149
width: percent(100),
150
height: percent(100),
151
align_items: AlignItems::Center,
152
justify_content: JustifyContent::Center,
153
display: Display::Flex,
154
flex_direction: FlexDirection::Column,
155
row_gap: px(10),
156
..default()
157
},
158
TabGroup::default(),
159
children![
160
(
161
button(asset_server),
162
observe(|_activate: On<Activate>| {
163
info!("Button clicked!");
164
}),
165
),
166
(
167
slider(0.0, 100.0, 50.0),
168
observe(
169
|value_change: On<ValueChange<f32>>,
170
mut widget_states: ResMut<DemoWidgetStates>| {
171
widget_states.slider_value = value_change.value;
172
},
173
)
174
),
175
(
176
checkbox(asset_server, "Checkbox"),
177
observe(checkbox_self_update)
178
),
179
(
180
radio_group(asset_server),
181
observe(
182
|value_change: On<ValueChange<Entity>>,
183
mut widget_states: ResMut<DemoWidgetStates>,
184
q_radios: Query<&DemoRadio>| {
185
if let Ok(radio) = q_radios.get(value_change.value) {
186
widget_states.slider_click = radio.0;
187
}
188
},
189
)
190
),
191
menu_button(asset_server),
192
Text::new("Press 'D' to toggle widget disabled states"),
193
],
194
)
195
}
196
197
fn button(asset_server: &AssetServer) -> impl Bundle {
198
(
199
Node {
200
width: px(150),
201
height: px(65),
202
border: UiRect::all(px(5)),
203
border_radius: BorderRadius::MAX,
204
justify_content: JustifyContent::Center,
205
align_items: AlignItems::Center,
206
..default()
207
},
208
DemoButton,
209
Button,
210
Hovered::default(),
211
TabIndex(0),
212
BorderColor::all(Color::BLACK),
213
BackgroundColor(NORMAL_BUTTON),
214
children![(
215
Text::new("Button"),
216
TextFont {
217
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
218
font_size: FontSize::Px(33.0),
219
..default()
220
},
221
TextColor(Color::srgb(0.9, 0.9, 0.9)),
222
TextShadow::default(),
223
)],
224
)
225
}
226
227
fn menu_button(asset_server: &AssetServer) -> impl Bundle {
228
(
229
Node { ..default() },
230
DemoMenuAnchor,
231
observe(on_menu_event),
232
children![(
233
Node {
234
width: px(200),
235
height: px(65),
236
border: UiRect::all(px(5)),
237
box_sizing: BoxSizing::BorderBox,
238
justify_content: JustifyContent::SpaceBetween,
239
align_items: AlignItems::Center,
240
padding: UiRect::axes(px(16), px(0)),
241
border_radius: BorderRadius::all(px(5)),
242
..default()
243
},
244
DemoMenuButton,
245
MenuButton,
246
Hovered::default(),
247
TabIndex(0),
248
BorderColor::all(Color::BLACK),
249
BackgroundColor(NORMAL_BUTTON),
250
children![
251
(
252
Text::new("Menu"),
253
TextFont {
254
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
255
font_size: FontSize::Px(33.0),
256
..default()
257
},
258
TextColor(Color::srgb(0.9, 0.9, 0.9)),
259
TextShadow::default(),
260
),
261
(
262
Node {
263
width: px(12),
264
height: px(12),
265
..default()
266
},
267
BackgroundColor(GRAY.into()),
268
)
269
],
270
)],
271
)
272
}
273
274
fn update_button_style(
275
mut buttons: Query<
276
(
277
Has<Pressed>,
278
&Hovered,
279
Has<InteractionDisabled>,
280
&mut BackgroundColor,
281
&mut BorderColor,
282
&Children,
283
),
284
(
285
Or<(
286
Changed<Pressed>,
287
Changed<Hovered>,
288
Added<InteractionDisabled>,
289
)>,
290
With<DemoButton>,
291
),
292
>,
293
mut text_query: Query<&mut Text>,
294
) {
295
for (pressed, hovered, disabled, mut color, mut border_color, children) in &mut buttons {
296
let mut text = text_query.get_mut(children[0]).unwrap();
297
set_button_style(
298
disabled,
299
hovered.get(),
300
pressed,
301
&mut color,
302
&mut border_color,
303
&mut text,
304
);
305
}
306
}
307
308
/// Supplementary system to detect removed marker components
309
fn update_button_style2(
310
mut buttons: Query<
311
(
312
Has<Pressed>,
313
&Hovered,
314
Has<InteractionDisabled>,
315
&mut BackgroundColor,
316
&mut BorderColor,
317
&Children,
318
),
319
With<DemoButton>,
320
>,
321
mut removed_depressed: RemovedComponents<Pressed>,
322
mut removed_disabled: RemovedComponents<InteractionDisabled>,
323
mut text_query: Query<&mut Text>,
324
) {
325
removed_depressed
326
.read()
327
.chain(removed_disabled.read())
328
.for_each(|entity| {
329
if let Ok((pressed, hovered, disabled, mut color, mut border_color, children)) =
330
buttons.get_mut(entity)
331
{
332
let mut text = text_query.get_mut(children[0]).unwrap();
333
set_button_style(
334
disabled,
335
hovered.get(),
336
pressed,
337
&mut color,
338
&mut border_color,
339
&mut text,
340
);
341
}
342
});
343
}
344
345
fn set_button_style(
346
disabled: bool,
347
hovered: bool,
348
pressed: bool,
349
color: &mut BackgroundColor,
350
border_color: &mut BorderColor,
351
text: &mut Text,
352
) {
353
match (disabled, hovered, pressed) {
354
// Disabled button
355
(true, _, _) => {
356
**text = "Disabled".to_string();
357
*color = NORMAL_BUTTON.into();
358
border_color.set_all(GRAY);
359
}
360
361
// Pressed and hovered button
362
(false, true, true) => {
363
**text = "Press".to_string();
364
*color = PRESSED_BUTTON.into();
365
border_color.set_all(RED);
366
}
367
368
// Hovered, unpressed button
369
(false, true, false) => {
370
**text = "Hover".to_string();
371
*color = HOVERED_BUTTON.into();
372
border_color.set_all(WHITE);
373
}
374
375
// Unhovered button (either pressed or not).
376
(false, false, _) => {
377
**text = "Button".to_string();
378
*color = NORMAL_BUTTON.into();
379
border_color.set_all(BLACK);
380
}
381
}
382
}
383
384
/// Create a demo slider
385
fn slider(min: f32, max: f32, value: f32) -> impl Bundle {
386
(
387
Node {
388
display: Display::Flex,
389
flex_direction: FlexDirection::Column,
390
justify_content: JustifyContent::Center,
391
align_items: AlignItems::Stretch,
392
justify_items: JustifyItems::Center,
393
column_gap: px(4),
394
height: px(12),
395
width: percent(30),
396
..default()
397
},
398
Name::new("Slider"),
399
Hovered::default(),
400
DemoSlider,
401
Slider {
402
track_click: TrackClick::Snap,
403
},
404
SliderValue(value),
405
SliderRange::new(min, max),
406
TabIndex(0),
407
Children::spawn((
408
// Slider background rail
409
Spawn((
410
Node {
411
height: px(6),
412
border_radius: BorderRadius::all(px(3)),
413
..default()
414
},
415
BackgroundColor(SLIDER_TRACK), // Border color for the slider
416
)),
417
// Invisible track to allow absolute placement of thumb entity. This is narrower than
418
// the actual slider, which allows us to position the thumb entity using simple
419
// percentages, without having to measure the actual width of the slider thumb.
420
Spawn((
421
Node {
422
display: Display::Flex,
423
position_type: PositionType::Absolute,
424
left: px(0),
425
// Track is short by 12px to accommodate the thumb.
426
right: px(12),
427
top: px(0),
428
bottom: px(0),
429
..default()
430
},
431
children![(
432
// Thumb
433
DemoSliderThumb,
434
SliderThumb,
435
Node {
436
display: Display::Flex,
437
width: px(12),
438
height: px(12),
439
position_type: PositionType::Absolute,
440
left: percent(0), // This will be updated by the slider's value
441
border_radius: BorderRadius::MAX,
442
..default()
443
},
444
BackgroundColor(SLIDER_THUMB),
445
)],
446
)),
447
)),
448
)
449
}
450
451
/// Update the visuals of the slider based on the slider state.
452
fn update_slider_style(
453
sliders: Query<
454
(
455
Entity,
456
&SliderValue,
457
&SliderRange,
458
&Hovered,
459
&CoreSliderDragState,
460
Has<InteractionDisabled>,
461
),
462
(
463
Or<(
464
Changed<SliderValue>,
465
Changed<SliderRange>,
466
Changed<Hovered>,
467
Changed<CoreSliderDragState>,
468
Added<InteractionDisabled>,
469
)>,
470
With<DemoSlider>,
471
),
472
>,
473
children: Query<&Children>,
474
mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
475
) {
476
for (slider_ent, value, range, hovered, drag_state, disabled) in sliders.iter() {
477
for child in children.iter_descendants(slider_ent) {
478
if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
479
&& is_thumb
480
{
481
thumb_node.left = percent(range.thumb_position(value.0) * 100.0);
482
thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);
483
}
484
}
485
}
486
}
487
488
fn update_slider_style2(
489
sliders: Query<
490
(
491
Entity,
492
&Hovered,
493
&CoreSliderDragState,
494
Has<InteractionDisabled>,
495
),
496
With<DemoSlider>,
497
>,
498
children: Query<&Children>,
499
mut thumbs: Query<(&mut BackgroundColor, Has<DemoSliderThumb>), Without<DemoSlider>>,
500
mut removed_disabled: RemovedComponents<InteractionDisabled>,
501
) {
502
removed_disabled.read().for_each(|entity| {
503
if let Ok((slider_ent, hovered, drag_state, disabled)) = sliders.get(entity) {
504
for child in children.iter_descendants(slider_ent) {
505
if let Ok((mut thumb_bg, is_thumb)) = thumbs.get_mut(child)
506
&& is_thumb
507
{
508
thumb_bg.0 = thumb_color(disabled, hovered.0 | drag_state.dragging);
509
}
510
}
511
}
512
});
513
}
514
515
fn thumb_color(disabled: bool, hovered: bool) -> Color {
516
match (disabled, hovered) {
517
(true, _) => ELEMENT_FILL_DISABLED,
518
519
(false, true) => SLIDER_THUMB.lighter(0.3),
520
521
_ => SLIDER_THUMB,
522
}
523
}
524
525
/// Create a demo checkbox
526
fn checkbox(asset_server: &AssetServer, caption: &str) -> impl Bundle {
527
(
528
Node {
529
display: Display::Flex,
530
flex_direction: FlexDirection::Row,
531
justify_content: JustifyContent::FlexStart,
532
align_items: AlignItems::Center,
533
align_content: AlignContent::Center,
534
column_gap: px(4),
535
..default()
536
},
537
Name::new("Checkbox"),
538
Hovered::default(),
539
DemoCheckbox,
540
Checkbox,
541
TabIndex(0),
542
Children::spawn((
543
Spawn((
544
// Checkbox outer
545
Node {
546
display: Display::Flex,
547
width: px(16),
548
height: px(16),
549
border: UiRect::all(px(2)),
550
border_radius: BorderRadius::all(px(3)),
551
..default()
552
},
553
BorderColor::all(ELEMENT_OUTLINE), // Border color for the checkbox
554
children![
555
// Checkbox inner
556
(
557
Node {
558
display: Display::Flex,
559
width: px(8),
560
height: px(8),
561
position_type: PositionType::Absolute,
562
left: px(2),
563
top: px(2),
564
..default()
565
},
566
BackgroundColor(ELEMENT_FILL),
567
),
568
],
569
)),
570
Spawn((
571
Text::new(caption),
572
TextFont {
573
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
574
font_size: FontSize::Px(20.0),
575
..default()
576
},
577
)),
578
)),
579
)
580
}
581
582
// Update the element's styles.
583
fn update_checkbox_or_radio_style(
584
mut q_checkbox: Query<
585
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
586
(
587
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
588
Or<(
589
Added<DemoCheckbox>,
590
Changed<Hovered>,
591
Added<Checked>,
592
Added<InteractionDisabled>,
593
)>,
594
),
595
>,
596
mut q_border_color: Query<
597
(&mut BorderColor, &mut Children),
598
(Without<DemoCheckbox>, Without<DemoRadio>),
599
>,
600
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
601
) {
602
for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {
603
let Some(border_id) = children.first() else {
604
continue;
605
};
606
607
let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id) else {
608
continue;
609
};
610
611
let Some(mark_id) = border_children.first() else {
612
warn!("Checkbox does not have a mark entity.");
613
continue;
614
};
615
616
let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {
617
warn!("Checkbox mark entity lacking a background color.");
618
continue;
619
};
620
621
set_checkbox_or_radio_style(
622
is_disabled,
623
*is_hovering,
624
checked,
625
&mut border_color,
626
&mut mark_bg,
627
);
628
}
629
}
630
631
fn update_checkbox_or_radio_style2(
632
mut q_checkbox: Query<
633
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
634
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
635
>,
636
mut q_border_color: Query<
637
(&mut BorderColor, &mut Children),
638
(Without<DemoCheckbox>, Without<DemoRadio>),
639
>,
640
mut q_bg_color: Query<
641
&mut BackgroundColor,
642
(Without<DemoCheckbox>, Without<DemoRadio>, Without<Children>),
643
>,
644
mut removed_checked: RemovedComponents<Checked>,
645
mut removed_disabled: RemovedComponents<InteractionDisabled>,
646
) {
647
removed_checked
648
.read()
649
.chain(removed_disabled.read())
650
.for_each(|entity| {
651
if let Ok((checked, Hovered(is_hovering), is_disabled, children)) =
652
q_checkbox.get_mut(entity)
653
{
654
let Some(border_id) = children.first() else {
655
return;
656
};
657
658
let Ok((mut border_color, border_children)) = q_border_color.get_mut(*border_id)
659
else {
660
return;
661
};
662
663
let Some(mark_id) = border_children.first() else {
664
warn!("Checkbox does not have a mark entity.");
665
return;
666
};
667
668
let Ok(mut mark_bg) = q_bg_color.get_mut(*mark_id) else {
669
warn!("Checkbox mark entity lacking a background color.");
670
return;
671
};
672
673
set_checkbox_or_radio_style(
674
is_disabled,
675
*is_hovering,
676
checked,
677
&mut border_color,
678
&mut mark_bg,
679
);
680
}
681
});
682
}
683
684
fn set_checkbox_or_radio_style(
685
disabled: bool,
686
hovering: bool,
687
checked: bool,
688
border_color: &mut BorderColor,
689
mark_bg: &mut BackgroundColor,
690
) {
691
let color: Color = if disabled {
692
// If the element is disabled, use a lighter color
693
ELEMENT_OUTLINE.with_alpha(0.2)
694
} else if hovering {
695
// If hovering, use a lighter color
696
ELEMENT_OUTLINE.lighter(0.2)
697
} else {
698
// Default color for the element
699
ELEMENT_OUTLINE
700
};
701
702
// Update the background color of the element
703
border_color.set_all(color);
704
705
let mark_color: Color = match (disabled, checked) {
706
(true, true) => ELEMENT_FILL_DISABLED,
707
(false, true) => ELEMENT_FILL,
708
(_, false) => Srgba::NONE.into(),
709
};
710
711
if mark_bg.0 != mark_color {
712
// Update the color of the element
713
mark_bg.0 = mark_color;
714
}
715
}
716
717
/// Create a demo radio group
718
fn radio_group(asset_server: &AssetServer) -> impl Bundle {
719
(
720
Node {
721
display: Display::Flex,
722
flex_direction: FlexDirection::Column,
723
align_items: AlignItems::Start,
724
column_gap: px(4),
725
..default()
726
},
727
Name::new("RadioGroup"),
728
RadioGroup,
729
TabIndex::default(),
730
children![
731
(radio(asset_server, TrackClick::Drag, "Slider Drag"),),
732
(radio(asset_server, TrackClick::Step, "Slider Step"),),
733
(radio(asset_server, TrackClick::Snap, "Slider Snap"),)
734
],
735
)
736
}
737
738
/// Create a demo radio button
739
fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle {
740
(
741
Node {
742
display: Display::Flex,
743
flex_direction: FlexDirection::Row,
744
justify_content: JustifyContent::FlexStart,
745
align_items: AlignItems::Center,
746
align_content: AlignContent::Center,
747
column_gap: px(4),
748
..default()
749
},
750
Name::new("RadioButton"),
751
Hovered::default(),
752
DemoRadio(value),
753
RadioButton,
754
Children::spawn((
755
Spawn((
756
// Radio outer
757
Node {
758
display: Display::Flex,
759
width: px(16),
760
height: px(16),
761
border: UiRect::all(px(2)),
762
border_radius: BorderRadius::MAX,
763
..default()
764
},
765
BorderColor::all(ELEMENT_OUTLINE), // Border color for the radio button
766
children![
767
// Radio inner
768
(
769
Node {
770
display: Display::Flex,
771
width: px(8),
772
height: px(8),
773
position_type: PositionType::Absolute,
774
left: px(2),
775
top: px(2),
776
border_radius: BorderRadius::MAX,
777
..default()
778
},
779
BackgroundColor(ELEMENT_FILL),
780
),
781
],
782
)),
783
Spawn((
784
Text::new(caption),
785
TextFont {
786
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
787
font_size: FontSize::Px(20.0),
788
..default()
789
},
790
)),
791
)),
792
)
793
}
794
795
fn on_menu_event(
796
menu_event: On<MenuEvent>,
797
q_anchor: Single<(Entity, &Children), With<DemoMenuAnchor>>,
798
q_popup: Query<Entity, With<MenuPopup>>,
799
assets: Res<AssetServer>,
800
mut focus: ResMut<InputFocus>,
801
mut commands: Commands,
802
) {
803
let (anchor, children) = q_anchor.into_inner();
804
let popup = children.iter().find_map(|c| q_popup.get(c).ok());
805
info!("Menu action: {:?}", menu_event.action);
806
match menu_event.action {
807
MenuAction::Open => {
808
if popup.is_none() {
809
spawn_menu(anchor, assets, commands);
810
}
811
}
812
MenuAction::Toggle => match popup {
813
Some(popup) => commands.entity(popup).despawn(),
814
None => spawn_menu(anchor, assets, commands),
815
},
816
MenuAction::Close | MenuAction::CloseAll => {
817
if let Some(popup) = popup {
818
commands.entity(popup).despawn();
819
}
820
}
821
MenuAction::FocusRoot => {
822
focus.0 = Some(anchor);
823
}
824
}
825
}
826
827
fn spawn_menu(anchor: Entity, assets: Res<AssetServer>, mut commands: Commands) {
828
let menu = commands
829
.spawn((
830
Node {
831
display: Display::Flex,
832
flex_direction: FlexDirection::Column,
833
min_height: px(10.),
834
min_width: percent(100),
835
border: UiRect::all(px(1)),
836
position_type: PositionType::Absolute,
837
..default()
838
},
839
MenuPopup::default(),
840
Visibility::Hidden, // Will be visible after positioning
841
BorderColor::all(GREEN),
842
BackgroundColor(GRAY.into()),
843
BoxShadow::new(
844
Srgba::BLACK.with_alpha(0.9).into(),
845
px(0),
846
px(0),
847
px(1),
848
px(4),
849
),
850
GlobalZIndex(100),
851
Popover {
852
positions: vec![
853
PopoverPlacement {
854
side: PopoverSide::Bottom,
855
align: PopoverAlign::Start,
856
gap: 2.0,
857
},
858
PopoverPlacement {
859
side: PopoverSide::Top,
860
align: PopoverAlign::Start,
861
gap: 2.0,
862
},
863
],
864
window_margin: 10.0,
865
},
866
OverrideClip,
867
children![
868
menu_item(&assets),
869
menu_item(&assets),
870
menu_item(&assets),
871
menu_item(&assets)
872
],
873
))
874
.id();
875
commands.entity(anchor).add_child(menu);
876
}
877
878
fn menu_item(asset_server: &AssetServer) -> impl Bundle {
879
(
880
Node {
881
padding: UiRect::axes(px(8), px(2)),
882
justify_content: JustifyContent::Center,
883
align_items: AlignItems::Start,
884
..default()
885
},
886
DemoMenuItem,
887
MenuItem,
888
Hovered::default(),
889
TabIndex(0),
890
BackgroundColor(NORMAL_BUTTON),
891
children![(
892
Text::new("Menu Item"),
893
TextFont {
894
font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
895
font_size: FontSize::Px(33.0),
896
..default()
897
},
898
TextColor(Color::srgb(0.9, 0.9, 0.9)),
899
TextShadow::default(),
900
)],
901
)
902
}
903
904
fn update_menu_item_style(
905
mut buttons: Query<
906
(
907
Has<Pressed>,
908
&Hovered,
909
Has<InteractionDisabled>,
910
&mut BackgroundColor,
911
),
912
(
913
Or<(
914
Changed<Pressed>,
915
Changed<Hovered>,
916
Added<InteractionDisabled>,
917
)>,
918
With<DemoMenuItem>,
919
),
920
>,
921
) {
922
for (pressed, hovered, disabled, mut color) in &mut buttons {
923
set_menu_item_style(disabled, hovered.get(), pressed, &mut color);
924
}
925
}
926
927
/// Supplementary system to detect removed marker components
928
fn update_menu_item_style2(
929
mut buttons: Query<
930
(
931
Has<Pressed>,
932
&Hovered,
933
Has<InteractionDisabled>,
934
&mut BackgroundColor,
935
),
936
With<DemoMenuItem>,
937
>,
938
mut removed_depressed: RemovedComponents<Pressed>,
939
mut removed_disabled: RemovedComponents<InteractionDisabled>,
940
) {
941
removed_depressed
942
.read()
943
.chain(removed_disabled.read())
944
.for_each(|entity| {
945
if let Ok((pressed, hovered, disabled, mut color)) = buttons.get_mut(entity) {
946
set_menu_item_style(disabled, hovered.get(), pressed, &mut color);
947
}
948
});
949
}
950
951
fn set_menu_item_style(disabled: bool, hovered: bool, pressed: bool, color: &mut BackgroundColor) {
952
match (disabled, hovered, pressed) {
953
// Pressed and hovered menu item
954
(false, true, true) => {
955
*color = PRESSED_BUTTON.into();
956
}
957
958
// Hovered, unpressed menu item
959
(false, true, false) => {
960
*color = HOVERED_BUTTON.into();
961
}
962
963
// Unhovered menu item (either pressed or not).
964
_ => {
965
*color = NORMAL_BUTTON.into();
966
}
967
}
968
}
969
970
fn toggle_disabled(
971
input: Res<ButtonInput<KeyCode>>,
972
mut interaction_query: Query<
973
(Entity, Has<InteractionDisabled>),
974
Or<(
975
With<Button>,
976
With<MenuButton>,
977
With<Slider>,
978
With<Checkbox>,
979
With<RadioButton>,
980
)>,
981
>,
982
mut commands: Commands,
983
) {
984
if input.just_pressed(KeyCode::KeyD) {
985
for (entity, disabled) in &mut interaction_query {
986
if disabled {
987
info!("Widget enabled");
988
commands.entity(entity).remove::<InteractionDisabled>();
989
} else {
990
info!("Widget disabled");
991
commands.entity(entity).insert(InteractionDisabled);
992
}
993
}
994
}
995
}
996
997