use bevy::prelude::*;
use bevy_egui::{EguiContext, EguiPlugin};
use bevy_inspector_egui::DefaultInspectorConfigPlugin;
use bevy_gearbox::{StateMachine, InitialState};
use bevy_gearbox::transitions::{Target, Source, EdgeKind, AlwaysEdge};
use bevy_ecs::schedule::ScheduleLabel;
use bevy_gearbox::transitions::edge_event_listener;
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.add_plugins((
EguiPlugin::default(),
DefaultInspectorConfigPlugin,
));
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, 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_endpoints_system)
.add_systems(Update, node_kind::sync_node_kind_machines)
.add_observer(edge_event_listener::<node_kind::AddChildClicked>)
.add_observer(edge_event_listener::<node_kind::ChildAdded>)
.add_observer(edge_event_listener::<node_kind::AllChildrenRemoved>)
.add_observer(edge_event_listener::<node_kind::MakeParallelClicked>)
.add_observer(edge_event_listener::<node_kind::MakeParentClicked>)
.add_observer(edge_event_listener::<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_transition_pulse)
.add_observer(handle_node_enter_pulse)
.add_observer(handle_delete_transition)
.add_observer(handle_delete_node)
.add_observer(on_add_edge)
.add_observer(on_remove_edge);
}
}
fn editor_ui_system(
mut editor_context: Query<&mut EguiContext, (With<EditorWindow>, Without<bevy_egui::PrimaryEguiContext>)>,
mut editor_state: ResMut<EditorState>,
mut state_machines: Query<(Entity, Option<&Name>, Option<&mut StateMachinePersistentData>, Option<&mut StateMachineTransientData>), With<StateMachine>>,
machine_list_query: Query<(Entity, Option<&Name>), With<StateMachine>>,
all_entities: Query<(Entity, Option<&Name>, Option<&InitialState>)>,
child_of_query: Query<&bevy_gearbox::StateChildOf>,
children_query: Query<&bevy_gearbox::StateChildren>,
active_query: Query<&bevy_gearbox::active::Active>,
parallel_query: Query<&bevy_gearbox::Parallel>,
mut commands: Commands,
) {
if let Ok(mut egui_context) = editor_context.single_mut() {
let ctx = egui_context.get_mut();
if let Some(selected_machine) = editor_state.selected_machine {
if let Ok((_, _, persistent_data_opt, transient_data_opt)) = state_machines.get_mut(selected_machine) {
let mut persistent_data = if let Some(data) = persistent_data_opt {
data
} else {
commands.entity(selected_machine).insert(StateMachinePersistentData::default());
let mut temp_persistent = StateMachinePersistentData::default();
let mut temp_transient = StateMachineTransientData::default();
node_editor::show_machine_editor(
ctx,
&mut editor_state,
&mut temp_persistent,
&mut temp_transient,
&all_entities,
&child_of_query,
&children_query,
&active_query,
¶llel_query,
&mut commands,
);
return;
};
let mut transient_data = if let Some(data) = transient_data_opt {
data
} else {
commands.entity(selected_machine).insert(StateMachineTransientData::default());
let mut temp_persistent = StateMachinePersistentData::default();
let mut temp_transient = StateMachineTransientData::default();
node_editor::show_machine_editor(
ctx,
&mut editor_state,
&mut temp_persistent,
&mut temp_transient,
&all_entities,
&child_of_query,
&children_query,
&active_query,
¶llel_query,
&mut commands,
);
return;
};
node_editor::show_machine_editor(
ctx,
&mut editor_state,
&mut persistent_data,
&mut transient_data,
&all_entities,
&child_of_query,
&children_query,
&active_query,
¶llel_query,
&mut commands,
);
}
} else {
machine_list::show_machine_list(
ctx,
&mut editor_state,
&machine_list_query,
&mut commands,
);
}
context_menu::render_context_menu(
ctx,
&mut editor_state,
&mut commands,
&all_entities,
&child_of_query,
¶llel_query,
);
}
}
fn handle_transition_creation_request(
trigger: Trigger<TransitionCreationRequested>,
editor_state: Res<EditorState>,
mut state_machines: Query<&mut StateMachineTransientData, With<StateMachine>>,
type_registry: Res<AppTypeRegistry>,
) {
let event = trigger.event();
let Some(selected_machine) = editor_state.selected_machine else {
return;
};
let Ok(mut transient_data) = state_machines.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: Trigger<CreateTransition>,
editor_state: Res<EditorState>,
mut state_machines: Query<(&mut StateMachineTransientData, &mut StateMachinePersistentData), With<StateMachine>>,
mut commands: Commands,
) {
let event = trigger.event();
let Some(selected_machine) = editor_state.selected_machine else {
return;
};
let Ok((mut transient_data, mut persistent_data)) = state_machines.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));
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);
}
Ok(())
}
fn handle_save_state_machine(
trigger: Trigger<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 state_machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
child_of_query: Query<&bevy_gearbox::StateChildOf>,
mut commands: Commands,
) {
let event = trigger.event();
let root = child_of_query.root_ancestor(event.source_entity);
if let Ok(mut persistent_data) = state_machines.get_mut(root) {
let initial_count = persistent_data.visual_transitions.len();
persistent_data.visual_transitions.retain(|transition| {
!(transition.source_entity == event.source_entity &&
transition.target_entity == event.target_entity &&
transition.event_type == event.event_type)
});
let final_count = persistent_data.visual_transitions.len();
if initial_count > final_count {
info!("✅ Removed visual transition from {:?} to {:?} ({}) - {} transitions remaining",
event.source_entity, event.target_entity, event.event_type, final_count);
} else {
warn!("⚠️ No matching visual transition found to remove: {:?} -> {:?} ({})",
event.source_entity, event.target_entity, 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_pulse(
trigger: Trigger<bevy_gearbox::Transition>,
mut state_machines: Query<&mut StateMachineTransientData, With<StateMachine>>,
edge_target_query: Query<&Target>,
) {
let event = trigger.event();
let target_entity = trigger.target();
if let Ok(mut transient_data) = state_machines.get_mut(target_entity) {
if let Ok(edge_target) = edge_target_query.get(event.edge) {
transient_data.transition_pulses.push(TransitionPulse::new(event.source, edge_target.0));
}
}
}
fn update_transition_pulses(
mut state_machines: Query<&mut StateMachineTransientData, With<StateMachine>>,
time: Res<Time>,
) {
for mut transient_data in state_machines.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>,
child_of_query: Query<&bevy_gearbox::StateChildOf>,
mut state_machines: Query<&mut StateMachineTransientData, With<StateMachine>>,
) {
let state = trigger.target();
let root = child_of_query.root_ancestor(state);
if let Ok(mut transient) = state_machines.get_mut(root) {
transient.node_pulses.push(NodePulse::new(state));
}
}
fn update_node_pulses(
mut state_machines: Query<&mut StateMachineTransientData, With<StateMachine>>,
time: Res<Time>,
) {
for mut transient in state_machines.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 state_machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
state_child_of_query: Query<&bevy_gearbox::StateChildOf>,
mut commands: Commands,
) {
let event = trigger.event();
let entity_to_delete = event.entity;
let root = state_child_of_query.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) = state_machines.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 on_add_edge(
trigger: Trigger<OnAdd, Source>,
sources: Query<&Source>,
targets: Query<&Target>,
always_q: Query<(), With<AlwaysEdge>>,
child_of_q: Query<&bevy_gearbox::StateChildOf>,
mut machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
) {
let edge = trigger.target();
let Ok(source) = sources.get(edge) else { return; };
let Ok(target) = targets.get(edge) else { return; };
let root = child_of_q.root_ancestor(source.0);
if let Ok(mut persistent) = machines.get_mut(root) {
if persistent.visual_transitions.iter().any(|t| t.edge_entity == edge) {
return;
}
let source_rect = persistent.nodes.get(&source.0).map(|n| n.current_rect());
let target_rect = persistent.nodes.get(&target.0).map(|n| n.current_rect());
let (source_rect, target_rect) = match (source_rect, target_rect) {
(Some(s), Some(t)) => (s, t),
_ => return,
};
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,
);
let event_type = if always_q.get(edge).is_ok() { "Always".to_string() } else { "Event".to_string() };
persistent.visual_transitions.push(TransitionConnection {
source_entity: source.0,
edge_entity: edge,
target_entity: target.0,
event_type,
source_rect,
target_rect,
event_node_position: midpoint,
is_dragging_event_node: false,
event_node_offset: egui::Vec2::ZERO,
});
}
}
fn on_remove_edge(
trigger: Trigger<OnRemove, Source>,
mut machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
) {
let edge = trigger.target();
for mut persistent in machines.iter_mut() {
persistent.visual_transitions.retain(|t| t.edge_entity != edge);
}
}
fn sync_edge_endpoints_system(
mut machines: Query<&mut StateMachinePersistentData, With<StateMachine>>,
child_of_q: Query<&bevy_gearbox::StateChildOf>,
changed_edges: Query<(Entity, &Source, &Target), Or<(Changed<Source>, Changed<Target>)>>,
) {
for (edge, source, target) in &changed_edges {
let root = child_of_q.root_ancestor(source.0);
if let Ok(mut persistent) = machines.get_mut(root) {
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;
}
}
}
}
fn handle_set_initial_state_request(
trigger: Trigger<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);
}
});
}