Skip to main content

context_menu/
context_menu.rs

1//! This example illustrates how to create a context menu that changes the clear color
2
3use bevy::{
4    color::palettes::basic,
5    ecs::event::PropagateEntityTrigger,
6    prelude::*,
7    ui_widgets::{ListBox, ListItem, ValueChange},
8};
9
10/// event opening a new context menu at position `pos`
11#[derive(Event)]
12struct OpenContextMenu {
13    pos: Vec2,
14}
15
16/// event will be sent to close currently open context menus
17#[derive(Event)]
18struct CloseContextMenus;
19
20/// marker component identifying root of a context menu
21#[derive(Component, Clone, Default)]
22struct ContextMenu;
23
24/// context menu item data storing what background color `Srgba` it activates
25#[derive(Component, Clone, Default, PartialEq)]
26struct ContextMenuItem(Srgba);
27
28/// marker component for context item text
29#[derive(Component, Clone, Default)]
30struct ContextMenuItemText;
31
32fn main() {
33    App::new()
34        .add_plugins(DefaultPlugins)
35        .add_systems(Startup, setup)
36        .add_observer(on_trigger_menu)
37        .add_observer(on_trigger_close_menus)
38        .add_observer(text_color_on_hover::<PointerOut>(basic::WHITE.into()))
39        .add_observer(text_color_on_hover::<PointerOver>(basic::RED.into()))
40        .run();
41}
42
43/// helper function to reduce code duplication when generating almost identical observers for the hover text color change effect
44fn text_color_on_hover<
45    E: PointerEvent + for<'a> Event<Trigger<'a> = PropagateEntityTrigger<true, E, PointerTraversal>>,
46>(
47    color: Color,
48) -> impl FnMut(On<E>, Query<&mut TextColor, With<ContextMenuItemText>>, Query<&Children>) {
49    move |mut event: On<E>,
50          mut text_color: Query<&mut TextColor, With<ContextMenuItemText>>,
51          children: Query<&Children>| {
52        let Ok(children) = children.get(event.original_event_target()) else {
53            return;
54        };
55        event.propagate(false);
56
57        // find the text among children and change its color
58        for child in children.iter() {
59            if let Ok(mut col) = text_color.get_mut(child) {
60                col.0 = color;
61            }
62        }
63    }
64}
65
66fn setup(mut commands: Commands) {
67    commands.spawn(Camera2d);
68
69    commands.spawn_scene(bsn! {
70        @background()
71        on(|press: On<PointerPress>, query: Query<(), With<ContextMenu>>, mut commands: Commands| {
72            debug!("click: {}", press.pointer.position);
73
74            if query.is_empty() {
75                // Open the context menu at the pointer location if one does not exist
76                commands.trigger(OpenContextMenu {
77                    pos: press.pointer.position,
78                });
79            } else {
80                // Close the context menu if it exists
81                commands.trigger(CloseContextMenus);
82            }
83        })
84    });
85}
86
87fn on_trigger_close_menus(
88    _event: On<CloseContextMenus>,
89    mut commands: Commands,
90    menus: Query<Entity, With<ContextMenu>>,
91) {
92    for e in menus.iter() {
93        commands.entity(e).despawn();
94    }
95}
96
97fn on_trigger_menu(event: On<OpenContextMenu>, mut commands: Commands) {
98    commands.trigger(CloseContextMenus);
99
100    let pos = event.pos;
101
102    debug!("open context menu at: {pos}");
103
104    commands.spawn_scene(bsn! {
105        #ContextMenu
106        ContextMenu
107        Node {
108            position_type: PositionType::Absolute,
109            left: px(pos.x),
110            top: px(pos.y),
111            flex_direction: FlexDirection::Column,
112            border_radius: BorderRadius::all(px(4)),
113        }
114        BorderColor::all(Color::BLACK)
115        BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.1))
116        ListBox
117        Children [
118            @context_item("fuchsia", basic::FUCHSIA)
119            --
120            @context_item("gray", basic::GRAY)
121            --
122            @context_item("maroon", basic::MAROON)
123            --
124            @context_item("purple", basic::PURPLE)
125            --
126            @context_item("teal", basic::TEAL)
127        ]
128        on(|event: On<ValueChange<Entity>>,
129            menu_items: Query<&ContextMenuItem, With<ListItem>>,
130            mut clear_col: ResMut<ClearColor>,
131            mut commands: Commands| {
132                let Ok(selected) = menu_items.get(event.value) else {
133                    return;
134                };
135                clear_col.0 = selected.0.into();
136                commands.trigger(CloseContextMenus);
137
138                // We do not set the `Selected` state of any of the items because the menu
139                // will be despawned.
140        })
141    });
142}
143
144fn context_item(text: &'static str, col: Srgba) -> impl Scene {
145    bsn! {
146        Name(format!("item-{text}"))
147        ListItem
148        ContextMenuItem(col)
149        Node {
150            padding: UiRect::all(px(5)),
151        }
152        Children [
153            ContextMenuItemText
154            Pickable::IGNORE
155            Text(text)
156            TextFont {
157                font_size: FontSize::Px(24.0),
158            }
159            TextColor(Color::WHITE)
160        ]
161    }
162}
163
164fn background() -> impl Scene {
165    bsn! {
166        #Background
167        Node {
168            width: percent(100),
169            height: percent(100),
170            align_items: AlignItems::Center,
171            justify_content: JustifyContent::Center,
172        }
173        ZIndex({-10})
174        Children [
175            Text::new("Click anywhere to spawn a Context Menu.\nYour selection will change the background color.")
176            TextFont {
177                font_size: FontSize::Px(28.0),
178            }
179            TextColor(Color::WHITE)
180        ]
181    }
182}