Skip to main content

standard_widgets/
standard_widgets.rs

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