1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
//! Context menu system for node interactions
//!
//! This module handles:
//! - Right-click context menu rendering and interaction
//! - Node action processing (Inspect, Add Child)
//! - Entity creation and hierarchy management
use bevy::prelude::*;
// use bevy::ecs::reflect::ReflectComponent;
// use bevy::prelude::AppTypeRegistry;
use bevy_gearbox::{StateMachineRoot, InitialState};
use bevy_egui::egui;
use crate::editor_state::{EditorState, NodeAction, NodeActionTriggered, NodeContextMenuRequested, TransitionContextMenuRequested, DeleteTransition, DeleteNode};
use crate::components::{NodeType, LeafNode};
use crate::{StateMachinePersistentData, StateMachineTransientData};
/// Observer to handle context menu requests
///
/// Renders a context menu at the requested position with available actions.
pub fn handle_context_menu_request(
trigger: Trigger<NodeContextMenuRequested>,
mut editor_state: ResMut<EditorState>,
) {
let event = trigger.event();
// Store the context menu request in editor state for rendering
editor_state.context_menu_entity = Some(event.entity);
editor_state.context_menu_position = Some(event.position);
}
/// Observer to handle transition context menu requests
pub fn handle_transition_context_menu_request(
trigger: Trigger<TransitionContextMenuRequested>,
mut editor_state: ResMut<EditorState>,
) {
let event = trigger.event();
// Store the transition context menu request in editor state for rendering
editor_state.transition_context_menu = Some((event.source_entity, event.target_entity, event.event_type.clone()));
editor_state.transition_context_menu_position = Some(event.position);
}
/// Observer to handle node actions triggered from context menus
///
/// Processes actions like Inspect and Add Child, performing the necessary
/// entity creation and component management.
pub fn handle_node_action(
trigger: Trigger<NodeActionTriggered>,
mut commands: Commands,
mut editor_state: ResMut<EditorState>,
mut state_machines: Query<(&mut StateMachinePersistentData, &mut StateMachineTransientData), With<StateMachineRoot>>,
name_query: Query<&Name>,
) {
let event = trigger.event();
// Get the currently selected state machine
let Some(selected_machine) = editor_state.selected_machine else {
return;
};
let Ok((mut persistent_data, mut transient_data)) = state_machines.get_mut(selected_machine) else {
return;
};
match event.action {
NodeAction::Inspect => {
// Set the entity to be inspected
editor_state.inspected_entity = Some(event.entity);
}
NodeAction::AddChild => {
// Create a new child entity
let child_entity = commands.spawn((
bevy_gearbox::StateChildOf(event.entity),
Name::new("New State"),
)).id();
// Add the child as a leaf node in the editor at an offset position
if let Some(parent_node) = persistent_data.nodes.get(&event.entity) {
let parent_pos = match parent_node {
NodeType::Leaf(leaf_node) => leaf_node.entity_node.position,
NodeType::Parent(parent_node) => parent_node.entity_node.position,
};
// Position the child at an offset from the parent
let child_pos = parent_pos + egui::Vec2::new(50.0, 50.0);
let leaf_node = LeafNode::new(child_pos);
persistent_data.nodes.insert(child_entity, NodeType::Leaf(leaf_node));
}
}
NodeAction::Rename => {
let entity_name = name_query.get(event.entity).unwrap().to_string();
transient_data.text_editing.start_editing(event.entity, &entity_name);
}
NodeAction::SetAsInitialState => {
// Set this entity as the initial state for its parent
// This requires finding the parent and updating its InitialState component
let target_entity = event.entity; // Capture the entity to avoid lifetime issues
commands.queue(move |world: &mut World| {
// Find the parent of this entity by getting its ChildOf component
if let Some(child_of) = world.entity(target_entity).get::<ChildOf>() {
let parent_entity = child_of.0;
// Set or update the InitialState component on the parent
world.entity_mut(parent_entity).insert(InitialState(target_entity));
info!("✅ Set entity {:?} as initial state for parent {:?}", target_entity, parent_entity);
} else {
warn!("⚠️ Could not find parent for entity {:?} to set as initial state", target_entity);
}
});
}
NodeAction::Delete => {
// Trigger the delete node event
commands.trigger(DeleteNode {
entity: event.entity,
});
}
}
}
/// Render context menu UI if one is requested
///
/// This function should be called during UI rendering to display context menus.
pub fn render_context_menu(
ctx: &egui::Context,
editor_state: &mut EditorState,
commands: &mut Commands,
) {
if let (Some(entity), Some(position)) = (editor_state.context_menu_entity, editor_state.context_menu_position) {
let menu_id = egui::Id::new("context_menu").with(entity);
egui::Area::new(menu_id)
.fixed_pos(position)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::popup(ui.style())
.show(ui, |ui| {
ui.set_min_width(120.0);
if ui.button("Inspect").clicked() {
commands.trigger(NodeActionTriggered {
entity,
action: NodeAction::Inspect,
});
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
ui.close_menu();
}
if ui.button("Add child").clicked() {
commands.trigger(NodeActionTriggered {
entity,
action: NodeAction::AddChild,
});
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
ui.close_menu();
}
if ui.button("Rename").clicked() {
commands.trigger(NodeActionTriggered {
entity,
action: NodeAction::Rename,
});
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
ui.close_menu();
}
if ui.button("Set as Initial State").clicked() {
commands.trigger(NodeActionTriggered {
entity,
action: NodeAction::SetAsInitialState,
});
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
ui.close_menu();
}
if ui.button("🗑 Delete Node").clicked() {
commands.trigger(NodeActionTriggered {
entity,
action: NodeAction::Delete,
});
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
ui.close_menu();
}
});
});
// Close context menu if clicked elsewhere
if ctx.input(|i| i.pointer.any_click()) {
let pointer_pos = ctx.input(|i| i.pointer.hover_pos().unwrap_or_default());
let menu_rect = egui::Rect::from_min_size(position, egui::Vec2::new(120.0, 60.0));
if !menu_rect.contains(pointer_pos) {
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
}
}
}
// Render transition context menu if requested
if let (Some((source, target, event_type)), Some(position)) = (
editor_state.transition_context_menu.clone(),
editor_state.transition_context_menu_position
) {
let menu_id = egui::Id::new("transition_context_menu").with((source, target));
egui::Area::new(menu_id)
.fixed_pos(position)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::popup(ui.style())
.show(ui, |ui| {
ui.set_min_width(120.0);
if ui.button("Inspect").clicked() {
// Resolve using the stored edge_entity in the visual model
let event_type_clone = event_type.clone();
commands.queue(move |world: &mut World| {
info!(
"Inspect requested (direct): source={:?} target={:?} event_type={}",
source, target, event_type_clone
);
let Some(editor_state) = world.get_resource::<EditorState>() else {
warn!("Inspect: EditorState missing");
return;
};
let Some(root) = editor_state.selected_machine else {
warn!("Inspect: no selected machine");
return;
};
let Some(persistent) = world.get::<StateMachinePersistentData>(root) else {
warn!("Inspect: missing StateMachinePersistentData on root {:?}", root);
return;
};
if let Some(conn) = persistent.visual_transitions.iter().find(|t| t.source_entity == source && t.target_entity == target && t.event_type == event_type_clone) {
let e = conn.edge_entity;
if world.entities().contains(e) {
if let Some(mut es) = world.get_resource_mut::<EditorState>() {
es.inspected_entity = Some(e);
}
info!("Inspect: set inspected_entity to edge {:?}", e);
} else {
warn!("Inspect: stored edge_entity {:?} no longer exists", e);
}
} else {
warn!("Inspect: no matching TransitionConnection found in visual_transitions");
}
});
editor_state.transition_context_menu = None;
editor_state.transition_context_menu_position = None;
ui.close_menu();
}
if ui.button("🗑 Delete Transition").clicked() {
commands.trigger(DeleteTransition {
source_entity: source,
target_entity: target,
event_type: event_type.clone(),
});
editor_state.transition_context_menu = None;
editor_state.transition_context_menu_position = None;
ui.close_menu();
}
});
});
// Close transition context menu if clicked elsewhere
if ctx.input(|i| i.pointer.any_click()) {
let pointer_pos = ctx.input(|i| i.pointer.hover_pos().unwrap_or_default());
let menu_rect = egui::Rect::from_min_size(position, egui::Vec2::new(120.0, 40.0));
if !menu_rect.contains(pointer_pos) {
editor_state.transition_context_menu = None;
editor_state.transition_context_menu_position = None;
}
}
}
}