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::{relationship::RelatedSpawner, spawn::SpawnWith},
6    prelude::*,
7};
8use std::fmt::Debug;
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)]
22struct ContextMenu;
23
24/// context menu item data storing what background color `Srgba` it activates
25#[derive(Component)]
26struct ContextMenuItem(Srgba);
27
28fn main() {
29    App::new()
30        .add_plugins(DefaultPlugins)
31        .add_systems(Startup, setup)
32        .add_observer(on_trigger_menu)
33        .add_observer(on_trigger_close_menus)
34        .add_observer(text_color_on_hover::<Out>(basic::WHITE.into()))
35        .add_observer(text_color_on_hover::<Over>(basic::RED.into()))
36        .run();
37}
38
39/// helper function to reduce code duplication when generating almost identical observers for the hover text color change effect
40fn text_color_on_hover<T: Debug + Clone + Reflect>(
41    color: Color,
42) -> impl FnMut(On<Pointer<T>>, Query<&mut TextColor>, Query<&Children>) {
43    move |mut event: On<Pointer<T>>,
44          mut text_color: Query<&mut TextColor>,
45          children: Query<&Children>| {
46        let Ok(children) = children.get(event.original_event_target()) else {
47            return;
48        };
49        event.propagate(false);
50
51        // find the text among children and change its color
52        for child in children.iter() {
53            if let Ok(mut col) = text_color.get_mut(child) {
54                col.0 = color;
55            }
56        }
57    }
58}
59
60fn setup(mut commands: Commands) {
61    commands.spawn(Camera2d);
62
63    commands.spawn(background_and_button()).observe(
64        // any click bubbling up here should lead to closing any open menu
65        |_: On<Pointer<Press>>, mut commands: Commands| {
66            commands.trigger(CloseContextMenus);
67        },
68    );
69}
70
71fn on_trigger_close_menus(
72    _event: On<CloseContextMenus>,
73    mut commands: Commands,
74    menus: Query<Entity, With<ContextMenu>>,
75) {
76    for e in menus.iter() {
77        commands.entity(e).despawn();
78    }
79}
80
81fn on_trigger_menu(event: On<OpenContextMenu>, mut commands: Commands) {
82    commands.trigger(CloseContextMenus);
83
84    let pos = event.pos;
85
86    debug!("open context menu at: {pos}");
87
88    commands
89        .spawn((
90            Name::new("context menu"),
91            ContextMenu,
92            Node {
93                position_type: PositionType::Absolute,
94                left: px(pos.x),
95                top: px(pos.y),
96                flex_direction: FlexDirection::Column,
97                ..default()
98            },
99            BorderColor::all(Color::BLACK),
100            BorderRadius::all(px(4)),
101            BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.1)),
102            children![
103                context_item("fuchsia", basic::FUCHSIA),
104                context_item("gray", basic::GRAY),
105                context_item("maroon", basic::MAROON),
106                context_item("purple", basic::PURPLE),
107                context_item("teal", basic::TEAL),
108            ],
109        ))
110        .observe(
111            |event: On<Pointer<Press>>,
112             menu_items: Query<&ContextMenuItem>,
113             mut clear_col: ResMut<ClearColor>,
114             mut commands: Commands| {
115                let target = event.original_event_target();
116
117                if let Ok(item) = menu_items.get(target) {
118                    clear_col.0 = item.0.into();
119                    commands.trigger(CloseContextMenus);
120                }
121            },
122        );
123}
124
125fn context_item(text: &str, col: Srgba) -> impl Bundle {
126    (
127        Name::new(format!("item-{text}")),
128        ContextMenuItem(col),
129        Button,
130        Node {
131            padding: UiRect::all(px(5)),
132            ..default()
133        },
134        children![(
135            Pickable::IGNORE,
136            Text::new(text),
137            TextFont {
138                font_size: 24.0,
139                ..default()
140            },
141            TextColor(Color::WHITE),
142        )],
143    )
144}
145
146fn background_and_button() -> impl Bundle {
147    (
148        Name::new("background"),
149        Node {
150            width: percent(100),
151            height: percent(100),
152            align_items: AlignItems::Center,
153            justify_content: JustifyContent::Center,
154            ..default()
155        },
156        ZIndex(-10),
157        Children::spawn(SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
158            parent
159                .spawn((
160                    Name::new("button"),
161                    Button,
162                    Node {
163                        width: px(250),
164                        height: px(65),
165                        border: UiRect::all(px(5)),
166                        justify_content: JustifyContent::Center,
167                        align_items: AlignItems::Center,
168                        ..default()
169                    },
170                    BorderColor::all(Color::BLACK),
171                    BorderRadius::MAX,
172                    BackgroundColor(Color::BLACK),
173                    children![(
174                        Pickable::IGNORE,
175                        Text::new("Context Menu"),
176                        TextFont {
177                            font_size: 28.0,
178                            ..default()
179                        },
180                        TextColor(Color::WHITE),
181                        TextShadow::default(),
182                    )],
183                ))
184                .observe(|mut event: On<Pointer<Press>>, mut commands: Commands| {
185                    // by default this event would bubble up further leading to the `CloseContextMenus`
186                    // event being triggered and undoing the opening of one here right away.
187                    event.propagate(false);
188
189                    debug!("click: {}", event.pointer_location.position);
190
191                    commands.trigger(OpenContextMenu {
192                        pos: event.pointer_location.position,
193                    });
194                });
195        })),
196    )
197}