use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::platform::collections::HashSet;
use bevy_egui::EguiContext;
use bevy_egui::PrimaryEguiContext;
use bevy_inspector_egui::bevy_inspector::ui_for_world;
use bevy_gearbox::{StateMachine, InitialState};
use bevy_gearbox::transitions::{Target, Source, EdgeKind, AlwaysEdge};
use bevy_ecs::schedule::ScheduleLabel;
use bevy_gearbox::transitions::TransitionEventAppExt;
mod editor_state;
mod hierarchy;
mod node_editor;
mod context_menu;
mod window_management;
mod entity_inspector;
mod machine_list;
pub mod components;
pub mod reflectable;
pub mod node_kind;
pub use editor_state::*;
use bevy::ecs::reflect::ReflectComponent;
use bevy::prelude::AppTypeRegistry;
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct EditorWindowContextPass;
pub struct GearboxEditorPlugin;
impl Plugin for GearboxEditorPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<EditorState>();
app.register_type::<reflectable::ReflectableStateMachinePersistentData>()
.register_type::<reflectable::ReflectableNode>()
.register_type::<reflectable::ReflectableNodeType>()
.register_type::<reflectable::ReflectableTransitionConnection>();
app.add_systems(Update, window_management::handle_editor_hotkeys)
.add_observer(window_management::cleanup_editor_window)
.add_systems(EditorWindowContextPass, editor_ui_system)
.add_systems(EditorWindowContextPass, embedded_world_inspector_exclusive)
.add_systems(EditorWindowContextPass, entity_inspector::entity_inspector_system)
.add_systems(Update, (
node_editor::update_node_types,
hierarchy::constrain_children_to_parents,
hierarchy::recalculate_parent_sizes,
update_transition_pulses,
update_node_pulses,
reflectable::sync_reflectable_on_persistent_change,
).chain())
.add_systems(Update, sync_edge_visuals_from_ecs)
.add_systems(Update, node_kind::sync_node_kind_machines)
.add_transition_event::<node_kind::AddChildClicked>()
.add_transition_event::<node_kind::ChildAdded>()
.add_transition_event::<node_kind::AllChildrenRemoved>()
.add_transition_event::<node_kind::MakeParallelClicked>()
.add_transition_event::<node_kind::MakeParentClicked>()
.add_transition_event::<node_kind::MakeLeafClicked>()
.add_observer(node_kind::on_enter_nodekind_state_parallel)
.add_observer(node_kind::on_enter_nodekind_state_parent)
.add_observer(node_kind::on_enter_nodekind_state_parent_via_make_parent)
.add_observer(node_kind::on_enter_nodekind_state_leaf)
.add_observer(node_kind::on_remove_state_children);
app.add_observer(handle_set_initial_state_request);
app.add_observer(context_menu::handle_context_menu_request)
.add_observer(context_menu::handle_node_action)
.add_observer(context_menu::handle_transition_context_menu_request)
.add_observer(hierarchy::handle_parent_child_movement)
.add_observer(handle_transition_creation_request)
.add_observer(handle_create_transition)
.add_observer(handle_save_state_machine)
.add_observer(reflectable::on_add_reflectable_state_machine)
.add_observer(handle_node_enter_pulse)
.add_observer(handle_transition_actions_pulse)
.add_observer(handle_delete_transition)
.add_observer(handle_delete_transition_by_edge)
.add_observer(handle_delete_node)
.add_observer(handle_background_context_menu_request)
.add_observer(handle_open_machine_request)
.add_observer(handle_close_machine_request)
.add_observer(handle_view_related);
}
}
fn editor_ui_system(
mut q_editor_context: Query<&mut EguiContext, (With<EditorWindow>, Without<bevy_egui::PrimaryEguiContext>)>,
mut editor_state: ResMut<EditorState>,
mut q_sm_data: Query<(Entity, Option<&Name>, Option<&mut StateMachinePersistentData>, Option<&mut StateMachineTransientData>), With<StateMachine>>,
q_sm: Query<(Entity, Option<&Name>), With<StateMachine>>,
q_entities: Query<(Entity, Option<&Name>, Option<&InitialState>)>,
q_child_of: Query<&bevy_gearbox::StateChildOf>,
q_children: Query<&bevy_gearbox::StateChildren>,
q_active: Query<&bevy_gearbox::active::Active>,
q_parallel: Query<&bevy_gearbox::Parallel>,
mut commands: Commands,
) {
if let Ok(mut egui_context) = q_editor_context.single_mut() {
let ctx = egui_context.get_mut();
egui::CentralPanel::default().show(ctx, |ui| {
handle_background_interactions(ui, &mut editor_state, &mut commands);
for open_machine in &editor_state.open_machines.clone() {
if let Ok((sm_entity, _, persistent_data_opt, transient_data_opt)) = q_sm_data.get_mut(open_machine.entity) {
if persistent_data_opt.is_none() {
commands.entity(open_machine.entity).insert(StateMachinePersistentData::default());
continue;
}
if transient_data_opt.is_none() {
commands.entity(open_machine.entity).insert(StateMachineTransientData::default());
continue;
}
let (_, _, Some(mut persistent_data), Some(mut transient_data)) = q_sm_data.get_mut(open_machine.entity).unwrap() else {
continue;
};
apply_canvas_offset_to_nodes(&mut persistent_data, open_machine.canvas_offset);
node_editor::show_single_machine_on_canvas(
ui,
&mut editor_state,
&mut persistent_data,
&mut transient_data,
sm_entity,
&q_entities,
&q_child_of,
&q_children,
&q_active,
&q_parallel,
&mut commands,
);
remove_canvas_offset_from_nodes(&mut persistent_data, open_machine.canvas_offset);
}
}
context_menu::render_context_menu(
ctx,
&mut editor_state,
&mut commands,
&q_entities,
&q_child_of,
&q_parallel,
);
render_background_context_menu(
ctx,
&mut editor_state,
&q_sm,
&mut commands,
);
});
}
}
fn embedded_world_inspector_exclusive(world: &mut World) {
let ctx_opt = {
let mut query = world.query_filtered::<&mut EguiContext, (With<EditorWindow>, Without<PrimaryEguiContext>)>();
query.iter_mut(world).next().map(|mut egui_context| egui_context.get_mut().clone())
};
if let Some(ctx) = ctx_opt {
egui::Window::new("World Inspector").default_open(true).show(&ctx, |ui| {
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
ui_for_world(world, ui);
});
});
}
}
fn handle_transition_creation_request(
trigger: On<TransitionCreationRequested>,
editor_state: Res<EditorState>,
mut q_sm: Query<&mut StateMachineTransientData, With<StateMachine>>,
type_registry: Res<AppTypeRegistry>,
) {
let event = trigger.event();
let mut selected_machine = None;
for open_machine in &editor_state.open_machines {
selected_machine = Some(open_machine.entity);
break; }
let Some(selected_machine) = selected_machine else {
return;
};
let Ok(mut transient_data) = q_sm.get_mut(selected_machine) else {
return;
};
transient_data.transition_creation.start_transition(event.source_entity);
discover_transition_edge_listener_event_types(&mut transient_data.transition_creation, &type_registry);
}
fn handle_create_transition(
trigger: On<CreateTransition>,
editor_state: Res<EditorState>,
mut q_sm: Query<(&mut StateMachineTransientData, &mut StateMachinePersistentData), With<StateMachine>>,
mut commands: Commands,
) {
let event = trigger.event();
let mut selected_machine = None;
for open_machine in &editor_state.open_machines {
selected_machine = Some(open_machine.entity);
break; }
let Some(selected_machine) = selected_machine else {
return;
};
let Ok((mut transient_data, mut persistent_data)) = q_sm.get_mut(selected_machine) else {
return;
};
let source = event.source_entity;
let target = event.target_entity;
let event_type = event.event_type.clone();
let edge_entity = commands.spawn_empty().id();
commands.queue(move |world: &mut World| {
match create_transition_edge_entity(world, edge_entity, source, target, &event_type) {
Ok(edge) => {
info!("✅ Created transition edge {:?} for {:?} -> {:?} ({})", edge, source, target, event_type);
}
Err(e) => {
warn!("Failed to create transition: {}", e);
}
}
});
transient_data.transition_creation.complete();
if let (Some(source_rect), Some(target_rect)) = (
persistent_data.nodes.get(&event.source_entity).map(|n| n.current_rect()),
persistent_data.nodes.get(&event.target_entity).map(|n| n.current_rect())
) {
let initial_event_position = egui::Pos2::new(
(source_rect.center().x + target_rect.center().x) / 2.0,
(source_rect.center().y + target_rect.center().y) / 2.0,
);
persistent_data.visual_transitions.push(TransitionConnection {
source_entity: event.source_entity,
edge_entity: edge_entity,
target_entity: event.target_entity,
event_type: event.event_type.clone(),
source_rect,
target_rect,
event_node_position: initial_event_position,
is_dragging_event_node: false,
event_node_offset: egui::Vec2::ZERO, });
}
}
fn discover_transition_edge_listener_event_types(
transition_state: &mut TransitionCreationState,
type_registry: &AppTypeRegistry,
) {
let registry = type_registry.read();
let mut event_types = Vec::new();
for registration in registry.iter() {
let type_path = registration.type_info().type_path();
if let Some(start) = type_path.find("EventEdge<") {
if let Some(end) = type_path[start..].find('>') {
let event_type = &type_path[start + 10..start + end];
if let Some(last_part) = event_type.split("::").last() {
if !event_types.contains(&last_part.to_string()) {
event_types.push(last_part.to_string());
}
}
}
}
}
event_types.sort();
if !event_types.iter().any(|e| e == "Always") {
event_types.insert(0, "Always".to_string());
}
transition_state.available_event_types = event_types;
}
fn create_transition_edge_entity(
world: &mut World,
edge_entity: Entity,
source_entity: Entity,
target_entity: Entity,
event_type: &str,
) -> Result<(), String> {
if event_type == "Always" {
world.entity_mut(edge_entity).insert((Source(source_entity), Target(target_entity), EdgeKind::External, AlwaysEdge, Name::new("Always")));
return Ok(());
}
let (type_path, reflect_component) = {
let type_registry = world.resource::<AppTypeRegistry>();
let registry = type_registry.read();
let mut transition_listener_type_path = None;
for registration in registry.iter() {
let type_path = registration.type_info().type_path();
if type_path.contains("EventEdge<") && type_path.contains(event_type) {
transition_listener_type_path = Some(type_path.to_string());
break;
}
}
let Some(type_path) = transition_listener_type_path else {
return Err(format!("EventEdge<{}> not found in type registry", event_type));
};
let Some(registration) = registry.get_with_type_path(&type_path) else { return Err(format!("Type registration not found for {}", type_path)); };
let Some(reflect_component) = registration.data::<ReflectComponent>() else { return Err(format!("ReflectComponent not found for {}", type_path)); };
(type_path, reflect_component.clone())
};
let edge = edge_entity;
world
.entity_mut(edge)
.insert((Source(source_entity), Target(target_entity), EdgeKind::External));
{
let type_registry = world.resource::<AppTypeRegistry>().clone();
let registry = type_registry.read();
let Some(registration) = registry.get_with_type_path(&type_path) else { return Err(format!("Type registration not found for {}", type_path)); };
let type_info = registration.type_info();
let mut dynamic_struct = bevy::reflect::DynamicStruct::default();
if let bevy::reflect::TypeInfo::Struct(_) = type_info { dynamic_struct.set_represented_type(Some(type_info)); } else { return Err(format!("EventEdge is not a struct type: {}", type_path)); }
let mut entity_mut = world.entity_mut(edge);
reflect_component.insert(&mut entity_mut, dynamic_struct.as_partial_reflect(), ®istry);
}
world.entity_mut(edge).insert(Name::new(event_type.to_string()));
Ok(())
}
fn handle_save_state_machine(
trigger: On<SaveStateMachine>,
mut commands: Commands,
) {
let event = trigger.event();
let entity = event.entity;
commands.queue(move |world: &mut World| {
let entity_name = if let Some(name) = world.get::<Name>(entity) {
name.as_str().to_string()
} else {
format!("state_machine_{:?}", entity)
};
let filename = format!("assets/{}.scn.ron", entity_name.replace(" ", "_").to_lowercase());
match crate::reflectable::ReflectableStateMachinePersistentData::save_state_machine_to_file(
world,
entity,
&filename
) {
Ok(_) => {
info!("✅ State machine '{}' saved to {}", entity_name, filename);
}
Err(e) => {
error!("❌ Failed to save state machine '{}': {}", entity_name, e);
}
}
});
}
fn handle_delete_transition(
trigger: Trigger<DeleteTransition>,
mut q_sm: Query<&mut StateMachinePersistentData, With<StateMachine>>,
q_child_of: Query<&bevy_gearbox::StateChildOf>,
mut commands: Commands,
) {
let event = trigger.event();
let root = q_child_of.root_ancestor(event.source_entity);
if let Ok(mut persistent_data) = q_sm.get_mut(root) {
persistent_data.visual_transitions.retain(|transition| {
!(transition.source_entity == event.source_entity &&
transition.target_entity == event.target_entity &&
transition.event_type == event.event_type)
});
} else {
warn!("⚠️ Could not find state machine persistent data for root {:?}", root);
}
let source_entity = event.source_entity;
let target_entity = event.target_entity;
let event_type = event.event_type.clone();
commands.queue(move |world: &mut World| {
let registry = world.resource::<AppTypeRegistry>().clone();
let registry = registry.read();
let mut reflect_listener: Option<bevy::ecs::reflect::ReflectComponent> = None;
for registration in registry.iter() {
let type_info = registration.type_info();
let type_name = type_info.type_path_table().short_path();
if type_name.starts_with("EventEdge<") && type_name.contains(&event_type) {
reflect_listener = registration.data::<ReflectComponent>().cloned();
break;
}
}
if reflect_listener.is_none() {
warn!("Could not resolve EventEdge<{}> for deletion", event_type);
return;
}
let reflect_listener = reflect_listener.unwrap();
let mut to_remove: Option<Entity> = None;
let mut q = world.query::<(Entity, &Source, &Target)>();
for (edge, src, tgt) in q.iter(world) {
if src.0 == source_entity && tgt.0 == target_entity {
if reflect_listener.reflect(world.entity(edge)).is_some() {
to_remove = Some(edge);
break;
}
}
}
if let Some(edge) = to_remove {
world.entity_mut(edge).despawn();
info!("✅ Removed edge {:?} for {:?} -> {:?} ({})", edge, source_entity, target_entity, event_type);
} else {
warn!("⚠️ No matching edge found to remove: {:?} -> {:?} ({})", source_entity, target_entity, event_type);
}
});
}
fn handle_transition_actions_pulse(
transition_actions: On<bevy_gearbox::TransitionActions>,
q_edge: Query<(&Source, &Target)>,
q_child_of: Query<&bevy_gearbox::StateChildOf>,
mut q_sm: Query<&mut StateMachineTransientData, With<StateMachine>>,
) {
let edge = transition_actions.target;
let Ok((Source(source), Target(target))) = q_edge.get(edge) else { return; };
let root = q_child_of.root_ancestor(*source);
if let Ok(mut transient) = q_sm.get_mut(root) {
transient.transition_pulses.push(TransitionPulse::new(*source, *target, edge));
}
}
fn update_transition_pulses(
mut q_sm: Query<&mut StateMachineTransientData, With<StateMachine>>,
time: Res<Time>,
) {
for mut transient_data in q_sm.iter_mut() {
for pulse in transient_data.transition_pulses.iter_mut() {
pulse.timer.tick(time.delta());
}
transient_data.transition_pulses.retain(|pulse| !pulse.timer.finished());
}
}
fn handle_node_enter_pulse(
trigger: Trigger<bevy_gearbox::EnterState>,
q_child_of: Query<&bevy_gearbox::StateChildOf>,
mut q_sm: Query<&mut StateMachineTransientData, With<StateMachine>>,
) {
let state = trigger.target();
let root = q_child_of.root_ancestor(state);
if let Ok(mut transient) = q_sm.get_mut(root) {
transient.node_pulses.push(NodePulse::new(state));
}
}
fn update_node_pulses(
mut q_sm: Query<&mut StateMachineTransientData, With<StateMachine>>,
time: Res<Time>,
) {
for mut transient in q_sm.iter_mut() {
for pulse in transient.node_pulses.iter_mut() {
pulse.timer.tick(time.delta());
}
transient.node_pulses.retain(|p| !p.timer.finished());
}
}
fn handle_delete_node(
trigger: Trigger<DeleteNode>,
mut q_sm: Query<&mut StateMachinePersistentData, With<StateMachine>>,
q_state_child_of: Query<&bevy_gearbox::StateChildOf>,
mut commands: Commands,
) {
let event = trigger.event();
let entity_to_delete = event.entity;
let root = q_state_child_of.root_ancestor(entity_to_delete);
if entity_to_delete == root {
warn!("⚠️ Cannot delete the root state machine entity {:?}", entity_to_delete);
return;
}
let Ok(mut persistent_data) = q_sm.get_mut(root) else {
warn!("⚠️ Could not find persistent data for state machine root {:?}", root);
return;
};
let incoming_to_deleted: Vec<_> = persistent_data
.visual_transitions
.iter()
.filter(|t| t.target_entity == entity_to_delete)
.cloned()
.collect();
for t in incoming_to_deleted {
commands.trigger(DeleteTransition {
source_entity: t.source_entity,
target_entity: t.target_entity,
event_type: t.event_type.clone(),
});
}
persistent_data.nodes.remove(&entity_to_delete);
commands.entity(entity_to_delete).despawn();
}
fn sync_edge_visuals_from_ecs(
editor_state: Res<EditorState>,
mut machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
q_edges: Query<(Entity, &Source, &Target)>,
q_names: Query<&Name>,
q_child_of: Query<&bevy_gearbox::StateChildOf>,
) {
for open_machine in &editor_state.open_machines {
let selected_root = open_machine.entity;
let Ok(mut persistent) = machines.get_mut(selected_root) else { continue; };
let mut seen_edges = HashSet::new();
let mut node_rects = HashMap::new();
for (entity, node) in &persistent.nodes {
node_rects.insert(*entity, node.current_rect());
}
for (edge, source, target) in &q_edges {
if q_child_of.root_ancestor(source.0) != selected_root { continue; }
seen_edges.insert(edge);
let (Some(source_rect), Some(target_rect)) = (
node_rects.get(&source.0).copied(),
node_rects.get(&target.0).copied(),
) else { continue; };
let label = if let Ok(n) = q_names.get(edge) { n.as_str().to_string() } else { format!("{:?}", edge) };
if let Some(vt) = persistent.visual_transitions.iter_mut().find(|t| t.edge_entity == edge) {
vt.source_entity = source.0;
vt.target_entity = target.0;
vt.source_rect = source_rect;
vt.target_rect = target_rect;
vt.event_type = label;
if !vt.is_dragging_event_node {
vt.update_event_node_position();
}
} else {
let midpoint = egui::Pos2::new(
(source_rect.center().x + target_rect.center().x) / 2.0,
(source_rect.center().y + target_rect.center().y) / 2.0,
);
persistent.visual_transitions.push(TransitionConnection {
source_entity: source.0,
edge_entity: edge,
target_entity: target.0,
event_type: label,
source_rect,
target_rect,
event_node_position: midpoint,
is_dragging_event_node: false,
event_node_offset: egui::Vec2::ZERO,
});
}
}
persistent.visual_transitions.retain(|t| seen_edges.contains(&t.edge_entity));
}
}
fn handle_delete_transition_by_edge(
trigger: On<DeleteTransitionByEdge>,
mut commands: Commands,
) {
let event = trigger.event();
let edge = event.edge_entity;
commands.queue(move |world: &mut World| {
if world.entities().contains(edge) {
world.entity_mut(edge).despawn();
info!("✅ Removed edge {:?}", edge);
} else {
warn!("⚠️ DeleteTransitionByEdge: edge {:?} does not exist", edge);
}
});
}
fn handle_set_initial_state_request(
trigger: On<SetInitialStateRequested>,
mut commands: Commands,
) {
let req = trigger.event();
let child = req.child_entity;
commands.queue(move |world: &mut World| {
if let Some(child_of) = world.entity(child).get::<bevy_gearbox::StateChildOf>() {
let parent = child_of.0;
world.entity_mut(parent).insert(InitialState(child));
info!("✅ Set InitialState({:?}) on parent {:?}", child, parent);
} else {
warn!("⚠️ SetInitialStateRequested: entity {:?} has no StateChildOf parent", child);
}
});
}
fn handle_background_interactions(
ui: &mut egui::Ui,
editor_state: &mut EditorState,
commands: &mut Commands,
) {
if editor_state.suppress_background_context_menu_once {
editor_state.suppress_background_context_menu_once = false;
return;
}
if ui.input(|i| i.pointer.secondary_clicked()) {
let pointer_pos = ui.input(|i| i.pointer.hover_pos().unwrap_or_default());
let clicked_on_machine = editor_state.open_machines.iter().any(|machine| {
let machine_rect = calculate_machine_rect(machine);
machine_rect.contains(pointer_pos)
});
if !clicked_on_machine {
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
editor_state.transition_context_menu = None;
editor_state.transition_context_menu_position = None;
editor_state.show_machine_selection_menu = false;
commands.trigger(BackgroundContextMenuRequested {
position: pointer_pos,
});
}
}
}
fn calculate_machine_rect(open_machine: &OpenMachine) -> egui::Rect {
let size = egui::Vec2::new(500.0, 350.0); egui::Rect::from_min_size(
egui::Pos2::new(open_machine.canvas_offset.x, open_machine.canvas_offset.y),
size,
)
}
fn apply_canvas_offset_to_nodes(persistent_data: &mut StateMachinePersistentData, offset: egui::Vec2) {
for node in persistent_data.nodes.values_mut() {
match node {
crate::components::NodeType::Leaf(leaf_node) => {
leaf_node.entity_node.position += offset;
}
crate::components::NodeType::Parent(parent_node) => {
parent_node.entity_node.position += offset;
}
}
}
for transition in persistent_data.visual_transitions.iter_mut() {
transition.event_node_position += offset;
}
}
fn remove_canvas_offset_from_nodes(persistent_data: &mut StateMachinePersistentData, offset: egui::Vec2) {
for node in persistent_data.nodes.values_mut() {
match node {
crate::components::NodeType::Leaf(leaf_node) => {
leaf_node.entity_node.position -= offset;
}
crate::components::NodeType::Parent(parent_node) => {
parent_node.entity_node.position -= offset;
}
}
}
for transition in persistent_data.visual_transitions.iter_mut() {
transition.event_node_position -= offset;
}
}
fn render_background_context_menu(
ctx: &egui::Context,
editor_state: &mut EditorState,
q_sm: &Query<(Entity, Option<&Name>), With<StateMachine>>,
commands: &mut Commands,
) {
if let Some(position) = editor_state.background_context_menu_position {
let menu_id = egui::Id::new("background_context_menu");
let mut last_main_menu_rect: Option<egui::Rect> = None;
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(200.0);
ui.heading("Canvas");
ui.separator();
if ui.button("Open State Machine").clicked() {
editor_state.show_machine_selection_menu = true;
}
if ui.button("Create New Machine").clicked() {
let new_entity = commands.spawn((
StateMachine::new(),
Name::new("New Machine"),
)).id();
commands.trigger(OpenMachineRequested { entity: new_entity });
editor_state.background_context_menu_position = None;
}
last_main_menu_rect = Some(ui.min_rect());
});
});
if !editor_state.show_machine_selection_menu {
if let Some(menu_rect) = last_main_menu_rect {
if ctx.input(|i| i.pointer.any_click()) {
let pointer_pos = ctx.input(|i| i.pointer.hover_pos().unwrap_or_default());
if !menu_rect.contains(pointer_pos) {
editor_state.background_context_menu_position = None;
}
}
}
}
}
if editor_state.show_machine_selection_menu {
if let Some(base_position) = editor_state.background_context_menu_position {
let submenu_position = egui::Pos2::new(base_position.x + 210.0, base_position.y);
let submenu_id = egui::Id::new("machine_selection_submenu");
let mut last_submenu_rect: Option<egui::Rect> = None;
egui::Area::new(submenu_id)
.fixed_pos(submenu_position)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
ui.set_min_width(200.0);
ui.heading("Select Machine");
ui.separator();
let mut found_machines = false;
for (entity, name_opt) in q_sm.iter() {
if editor_state.is_machine_open(entity) {
continue;
}
if let Some(name) = name_opt {
if name.as_str() == "NodeKind" {
continue;
}
}
found_machines = true;
let display_name = if let Some(name) = name_opt {
name.as_str().to_string()
} else {
format!("Unnamed Machine")
};
if ui.button(&display_name).clicked() {
commands.trigger(OpenMachineRequested { entity });
editor_state.background_context_menu_position = None;
editor_state.show_machine_selection_menu = false;
}
}
if !found_machines {
ui.label("No available machines");
}
ui.separator();
if ui.button("Cancel").clicked() {
editor_state.show_machine_selection_menu = false;
}
last_submenu_rect = Some(ui.min_rect());
});
});
if let (Some(main_menu_rect), Some(submenu_rect)) = ( Some(egui::Rect::from_min_size(base_position, egui::Vec2::new(200.0, 150.0))), last_submenu_rect) {
if ctx.input(|i| i.pointer.any_click()) {
let pointer_pos = ctx.input(|i| i.pointer.hover_pos().unwrap_or_default());
if !main_menu_rect.contains(pointer_pos) && !submenu_rect.contains(pointer_pos) {
editor_state.background_context_menu_position = None;
editor_state.show_machine_selection_menu = false;
}
}
}
}
}
}
fn handle_background_context_menu_request(
trigger: On<BackgroundContextMenuRequested>,
mut editor_state: ResMut<EditorState>,
) {
let event = trigger.event();
if editor_state.suppress_background_context_menu_once {
editor_state.suppress_background_context_menu_once = false;
return;
}
editor_state.context_menu_entity = None;
editor_state.context_menu_position = None;
editor_state.transition_context_menu = None;
editor_state.transition_context_menu_position = None;
editor_state.show_machine_selection_menu = false;
editor_state.background_context_menu_position = Some(event.position);
}
fn handle_open_machine_request(
trigger: On<OpenMachineRequested>,
mut editor_state: ResMut<EditorState>,
q_name: Query<&Name>,
) {
let event = trigger.event();
if editor_state.is_machine_open(event.entity) {
return;
}
let display_name = if let Ok(name) = q_name.get(event.entity) {
name.as_str().to_string()
} else {
format!("Machine {:?}", event.entity)
};
editor_state.add_machine(event.entity, display_name);
info!("✅ Opened machine {:?} on canvas", event.entity);
}
fn handle_close_machine_request(
trigger: On<CloseMachineRequested>,
mut editor_state: ResMut<EditorState>,
) {
let event = trigger.event();
editor_state.remove_machine(event.entity);
info!("✅ Closed machine {:?} from canvas", event.entity);
}
fn handle_view_related(
trigger: On<ViewRelated>,
mut editor_state: ResMut<EditorState>,
q_name: Query<&Name>,
q_sm: Query<Entity, With<StateMachine>>,
) {
let event = trigger.event();
if !editor_state.is_machine_open(event.origin) {
return;
}
if q_sm.get(event.target).is_err() {
warn!("ViewRelated target entity {:?} does not have a StateMachine component", event.target);
return;
}
if editor_state.is_machine_open(event.target) {
return;
}
let display_name = if let Ok(name) = q_name.get(event.target) {
name.as_str().to_string()
} else {
format!("Related {:?}", event.target)
};
let origin_offset = editor_state.open_machines.iter()
.find(|m| m.entity == event.origin)
.map(|m| m.canvas_offset)
.unwrap_or(egui::Vec2::ZERO);
let related_offset = origin_offset + egui::Vec2::new(300.0, 100.0);
editor_state.add_machine_with_offset(event.target, display_name, related_offset);
editor_state.related_entities
.entry(event.origin)
.or_insert_with(Vec::new)
.push(event.target);
info!("🔗 Auto-loaded related machine {:?} because origin {:?} is being viewed",
event.target, event.origin);
}