bevy_gearbox_editor 0.4.2

State machine system for the bevy game engine
Documentation
//! 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_gearbox::{StateMachine};
use bevy_egui::egui;

use crate::editor_state::{EditorState, NodeAction, NodeActionTriggered, NodeContextMenuRequested, TransitionContextMenuRequested, DeleteNode, SetInitialStateRequested, DeleteTransitionByEdge, SaveStateMachine, CloseMachineRequested};
use crate::components::{NodeType, LeafNode};
use crate::{StateMachinePersistentData, StateMachineTransientData};
use crate::node_kind::{AddChildClicked, MakeParallelClicked, MakeParentClicked, MakeLeafClicked};

/// Observer to handle context menu requests
/// 
/// Renders a context menu at the requested position with available actions.
pub fn handle_context_menu_request(
    node_context_menu_requested: On<NodeContextMenuRequested>,
    mut editor_state: ResMut<EditorState>,
) {
    // Store the context menu request in editor state for rendering
    // Mutual exclusivity: close background and transition menus
    editor_state.background_context_menu_position = None;
    editor_state.transition_context_menu = None;
    editor_state.transition_context_menu_position = None;
    editor_state.show_machine_selection_menu = false;
    // Open node menu
    editor_state.context_menu_entity = Some(node_context_menu_requested.entity);
    editor_state.context_menu_position = Some(node_context_menu_requested.position);
    // Suppress background menu for this frame
    editor_state.suppress_background_context_menu_once = true;
}

/// Observer to handle transition context menu requests
pub fn handle_transition_context_menu_request(
    transition_context_menu_requested: On<TransitionContextMenuRequested>,
    mut editor_state: ResMut<EditorState>,
) {
    // Store the transition context menu request in editor state for rendering
    // Mutual exclusivity: close background and node menus
    editor_state.background_context_menu_position = None;
    editor_state.context_menu_entity = None;
    editor_state.context_menu_position = None;
    editor_state.show_machine_selection_menu = false;
    editor_state.transition_context_menu = Some((transition_context_menu_requested.source_entity, transition_context_menu_requested.target_entity, transition_context_menu_requested.event_type.clone(), transition_context_menu_requested.edge_entity));
    editor_state.transition_context_menu_position = Some(transition_context_menu_requested.position);
    // Suppress background menu for this frame
    editor_state.suppress_background_context_menu_once = true;
}

/// 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(
    node_action_triggered: On<NodeActionTriggered>,
    mut commands: Commands,
    mut editor_state: ResMut<EditorState>,
    mut q_sm: Query<(&mut StateMachinePersistentData, &mut StateMachineTransientData), With<StateMachine>>,
    q_child_of: Query<&bevy_gearbox::StateChildOf>,
    q_name: Query<&Name>,
) {
    // Resolve the state machine root that contains this entity
    let selected_machine = q_child_of.root_ancestor(node_action_triggered.entity);
    
    let Ok((mut persistent_data, mut transient_data)) = q_sm.get_mut(selected_machine) else {
        return;
    };
    
    match node_action_triggered.action {
        NodeAction::Inspect => {
            // Set the entity to be inspected
            editor_state.inspected_entity = Some(node_action_triggered.entity);
        }
        NodeAction::AddChild => {
            // Create a new child entity
            let child_entity = commands.spawn((
                bevy_gearbox::StateChildOf(node_action_triggered.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(&node_action_triggered.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));
            }

            // Notify NodeKind machine for this parent
            let parent_entity = node_action_triggered.entity;
            if let Some(&nk_root) = transient_data.node_kind_roots.get(&parent_entity) {
                commands.trigger(AddChildClicked::new(nk_root));
                commands.trigger(crate::node_kind::ChildAdded::new(nk_root));
            }
        }
        NodeAction::Rename => {
            let entity_name = q_name.get(node_action_triggered.entity).unwrap().to_string();
            transient_data.text_editing.start_editing(node_action_triggered.entity, &entity_name);
        }
        NodeAction::MakeParallel => {
            // Notify NodeKind machine to handle Parallel transition
            let state_entity = node_action_triggered.entity;
            if let Some(&nk_root) = transient_data.node_kind_roots.get(&state_entity) {
                commands.trigger(MakeParallelClicked::new(nk_root));
            }
        }
        NodeAction::MakeParent => {
            // Ask NK to become Parent from any current kind
            let state_entity = node_action_triggered.entity;
            if let Some(&nk_root) = transient_data.node_kind_roots.get(&state_entity) {
                commands.trigger(MakeParentClicked::new(nk_root));
            }
        }
        NodeAction::MakeLeaf => {
            // Ask NK to become Leaf from any current kind
            let state_entity = node_action_triggered.entity;
            if let Some(&nk_root) = transient_data.node_kind_roots.get(&state_entity) {
                commands.trigger(MakeLeafClicked::new(nk_root));
            }
        }
        NodeAction::SetAsInitialState => {
            // Request parent InitialState update via event; handled centrally
            let child_entity = node_action_triggered.entity;
            commands.trigger(SetInitialStateRequested { child_entity });
        }
        NodeAction::ResetRegion => {
            // Call into core: fire ResetMachine on the selected machine root
            commands.trigger(bevy_gearbox::ResetRegion::new(selected_machine));
        }
        NodeAction::Delete => {
            // Trigger the delete node event
            commands.trigger(DeleteNode {
                entity: node_action_triggered.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,
    all_entities: &Query<(Entity, Option<&Name>, Option<&bevy_gearbox::InitialState>)>,
    q_child_of: &Query<&bevy_gearbox::StateChildOf>,
    q_parallel: &Query<&bevy_gearbox::Parallel>,
) {
    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);
        
        // Track the rect we actually drew so outside-click detection matches
        let mut last_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(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();
                        }

                        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();
                        }

                        // Determine type of node (Leaf/Parent/Parallel/Root)
                        let is_parent = all_entities.get(entity).ok().and_then(|(_,_,init)| init.map(|_|())).is_some();
                        let is_parallel = q_parallel.get(entity).is_ok();
                        let is_leaf = !is_parent && !is_parallel;
                        let is_root = editor_state.open_machines.iter().any(|m| m.entity == entity);

                        // Common options already added: Inspect, Rename

                        // Leaf-specific options: Make Parallel, Make Parent
                        if is_leaf {
                            if ui.button("Make Parallel").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeParallel });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            if ui.button("Make Parent").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeParent });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                        }

                        // Root-specific actions (save, close, reset)
                        if is_root {
                            if ui.button("💾 Save Machine").clicked() {
                                commands.trigger(SaveStateMachine { entity });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            
                            if ui.button("✕ Close Machine").clicked() {
                                commands.trigger(CloseMachineRequested { entity });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            
                            ui.separator();
                            
                            if ui.button("↺ Reset Machine").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::ResetRegion });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                        }

                        // Parent-specific: Make Parallel, Make Leaf, Add child, Reset (if not already shown as root)
                        if is_parent {
                            if !is_root {
                                if ui.button("↺ Reset Region").clicked() {
                                    commands.trigger(NodeActionTriggered { entity, action: NodeAction::ResetRegion });
                                    editor_state.context_menu_entity = None;
                                    editor_state.context_menu_position = None;
                                    ui.close();
                                }
                            }
                            if ui.button("Make Parallel").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeParallel });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            if ui.button("Make Leaf").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeLeaf });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            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();
                            }
                        }

                        // Parallel-specific: Make Leaf, Make Parent, Add child
                        if is_parallel {
                            if !is_root {
                                if ui.button("↺ Reset Region").clicked() {
                                    commands.trigger(NodeActionTriggered { entity, action: NodeAction::ResetRegion });
                                    editor_state.context_menu_entity = None;
                                    editor_state.context_menu_position = None;
                                    ui.close();
                                }
                            }
                            if ui.button("Make Leaf").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeLeaf });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            if ui.button("Make Parent").clicked() {
                                commands.trigger(NodeActionTriggered { entity, action: NodeAction::MakeParent });
                                editor_state.context_menu_entity = None;
                                editor_state.context_menu_position = None;
                                ui.close();
                            }
                            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();
                            }
                        }

                        // Child of a parent: Set as Initial State
                        if let Ok(child_of) = q_child_of.get(entity) {
                            let parent_has_initial = all_entities
                                .get(child_of.0)
                                .ok()
                                .and_then(|(_,_,init)| init.map(|_| ()))
                                .is_some();
                            if parent_has_initial {
                                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();
                                }
                            }
                        }
                        
                        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();
                        }
                        // Capture the menu rect from the UI's cursor
                        last_menu_rect = Some(ui.min_rect());
                    });
            });
        
        // Close context menu if clicked elsewhere
        if let Some(menu_rect) = last_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.context_menu_entity = None;
                    editor_state.context_menu_position = None;
                }
            }
        }
    }
    
    // Render transition context menu if requested
    if let (Some((source, target, _, edge_entity)), 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));
        
        let mut last_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(120.0);
                        
                        if ui.button("Inspect").clicked() {
                            editor_state.inspected_entity = Some(edge_entity);
                            editor_state.transition_context_menu = None;
                            editor_state.transition_context_menu_position = None;
                            ui.close();
                        }
                        
                        if ui.button("🗑 Delete Transition").clicked() {
                            commands.trigger(DeleteTransitionByEdge { edge_entity });
                            editor_state.transition_context_menu = None;
                            editor_state.transition_context_menu_position = None;
                            ui.close();
                        }
                        last_menu_rect = Some(ui.min_rect());
                    });
            });
        
        // Close transition context menu if clicked elsewhere
        if let Some(menu_rect) = last_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.transition_context_menu = None;
                    editor_state.transition_context_menu_position = None;
                }
            }
        }
    }
}