context_menu/
context_menu.rs1use bevy::{
4 color::palettes::basic,
5 ecs::event::PropagateEntityTrigger,
6 prelude::*,
7 ui_widgets::{ListBox, ListItem, ValueChange},
8};
9
10#[derive(Event)]
12struct OpenContextMenu {
13 pos: Vec2,
14}
15
16#[derive(Event)]
18struct CloseContextMenus;
19
20#[derive(Component, Clone, Default)]
22struct ContextMenu;
23
24#[derive(Component, Clone, Default, PartialEq)]
26struct ContextMenuItem(Srgba);
27
28#[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
43fn 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 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 commands.trigger(OpenContextMenu {
77 pos: press.pointer.position,
78 });
79 } else {
80 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 })
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}