context_menu/
context_menu.rs1use bevy::{
4 color::palettes::basic,
5 ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
6 prelude::*,
7};
8use std::fmt::Debug;
9
10#[derive(Event)]
12struct OpenContextMenu {
13 pos: Vec2,
14}
15
16#[derive(Event)]
18struct CloseContextMenus;
19
20#[derive(Component)]
22struct ContextMenu;
23
24#[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
39fn 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 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 |_: 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 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}