use bevy::prelude::*;
use bevy_gearbox::InitialState;
use bevy_gearbox::active::Active;
use egui::Pos2;
use std::collections::HashMap;
use crate::components::NodeType;
#[derive(Default)]
pub struct TextEditingState {
pub editing_entity: Option<Entity>,
pub current_text: String,
pub should_focus: bool,
pub first_focus: bool,
}
impl TextEditingState {
pub fn start_editing(&mut self, entity: Entity, current_name: &str) {
self.editing_entity = Some(entity);
self.current_text = current_name.to_string();
self.should_focus = true;
self.first_focus = true;
}
pub fn stop_editing(&mut self) -> Option<(Entity, String)> {
if let Some(entity) = self.editing_entity {
let text = self.current_text.clone();
self.editing_entity = None;
self.current_text.clear();
self.should_focus = false;
self.first_focus = false;
Some((entity, text))
} else {
None
}
}
pub fn cancel_editing(&mut self) {
self.editing_entity = None;
self.current_text.clear();
self.should_focus = false;
self.first_focus = false;
}
pub fn is_editing(&self, entity: Entity) -> bool {
self.editing_entity == Some(entity)
}
}
#[derive(Default)]
pub struct TransitionCreationState {
pub source_entity: Option<Entity>,
pub awaiting_target_selection: bool,
pub dropdown_position: Option<Pos2>,
pub target_entity: Option<Entity>,
pub show_event_dropdown: bool,
pub available_event_types: Vec<String>,
}
impl TransitionConnection {
pub fn calculate_two_segment_points(&self) -> (egui::Pos2, egui::Pos2, egui::Pos2, egui::Pos2) {
let source_to_event_start = closest_point_on_rect_edge(self.source_rect, self.event_node_position);
let source_to_event_end = self.event_node_position;
let event_to_target_start = self.event_node_position;
let event_to_target_end = closest_point_on_rect_edge(self.target_rect, self.event_node_position);
(source_to_event_start, source_to_event_end, event_to_target_start, event_to_target_end)
}
pub fn get_event_node_position(&self) -> egui::Pos2 {
self.event_node_position
}
pub fn get_event_node_rect(&self, text_size: egui::Vec2) -> egui::Rect {
let padding = egui::Vec2::new(12.0, 6.0);
let pill_size = text_size + padding * 2.0;
egui::Rect::from_center_size(self.event_node_position, pill_size)
}
pub fn update_event_node_position(&mut self) {
let midpoint = egui::Pos2::new(
(self.source_rect.center().x + self.target_rect.center().x) / 2.0,
(self.source_rect.center().y + self.target_rect.center().y) / 2.0,
);
self.event_node_position = midpoint + self.event_node_offset;
}
pub fn update_event_node_offset(&mut self) {
let midpoint = egui::Pos2::new(
(self.source_rect.center().x + self.target_rect.center().x) / 2.0,
(self.source_rect.center().y + self.target_rect.center().y) / 2.0,
);
self.event_node_offset = self.event_node_position - midpoint;
}
}
impl TransitionCreationState {
pub fn start_transition(&mut self, source: Entity) {
self.source_entity = Some(source);
self.awaiting_target_selection = true;
self.target_entity = None;
self.show_event_dropdown = false;
self.dropdown_position = None;
}
pub fn set_target(&mut self, target: Entity, dropdown_pos: Pos2) {
self.target_entity = Some(target);
self.awaiting_target_selection = false;
self.show_event_dropdown = true;
self.dropdown_position = Some(dropdown_pos);
}
pub fn cancel(&mut self) {
*self = Default::default();
}
pub fn complete(&mut self) {
*self = Default::default();
}
pub fn is_active(&self) -> bool {
self.source_entity.is_some()
}
}
#[derive(Component, Default)]
pub struct StateMachinePersistentData {
pub nodes: HashMap<Entity, NodeType>,
pub visual_transitions: Vec<TransitionConnection>,
}
#[derive(Component, Default)]
pub struct StateMachineTransientData {
pub selected_node: Option<Entity>,
pub transition_creation: TransitionCreationState,
pub text_editing: TextEditingState,
pub transition_pulses: Vec<TransitionPulse>,
}
#[derive(Resource, Default)]
pub struct EditorState {
pub selected_machine: Option<Entity>,
pub context_menu_entity: Option<Entity>,
pub context_menu_position: Option<Pos2>,
pub transition_context_menu: Option<(Entity, Entity, String)>, pub transition_context_menu_position: Option<Pos2>,
pub inspected_entity: Option<Entity>,
pub inspector_tab: InspectorTab,
pub component_addition: ComponentAdditionState,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InspectorTab {
Inspect,
Remove,
Add,
}
impl Default for InspectorTab {
fn default() -> Self {
Self::Inspect
}
}
#[derive(Debug, Default)]
pub struct ComponentAdditionState {
pub search_text: String,
pub dropdown_open: bool,
pub component_hierarchy: Option<crate::entity_inspector::ComponentHierarchy>,
pub expanded_namespaces: std::collections::HashSet<String>,
}
impl ComponentAdditionState {
pub fn update_hierarchy(&mut self, hierarchy: crate::entity_inspector::ComponentHierarchy) {
self.component_hierarchy = Some(hierarchy);
}
pub fn toggle_namespace(&mut self, namespace_path: &str) {
if self.expanded_namespaces.contains(namespace_path) {
self.expanded_namespaces.remove(namespace_path);
} else {
self.expanded_namespaces.insert(namespace_path.to_string());
}
}
pub fn is_namespace_expanded(&self, namespace_path: &str) -> bool {
self.expanded_namespaces.contains(namespace_path)
}
}
impl EditorState {
pub fn current_machine(&self) -> Option<Entity> {
self.selected_machine
}
pub fn set_current_machine(&mut self, entity: Option<Entity>) {
self.selected_machine = entity;
}
}
#[derive(Component)]
pub struct EditorWindow;
#[derive(Event)]
pub struct NodeContextMenuRequested {
pub entity: Entity,
pub position: Pos2,
}
#[derive(Event)]
pub struct TransitionContextMenuRequested {
pub source_entity: Entity,
pub target_entity: Entity,
pub event_type: String,
pub position: Pos2,
}
#[derive(Debug, Clone)]
pub enum NodeAction {
Inspect,
AddChild,
Rename,
SetAsInitialState,
Delete,
}
#[derive(Event)]
pub struct NodeActionTriggered {
pub entity: Entity,
pub action: NodeAction,
}
#[derive(Event, Debug)]
pub struct NodeDragged {
pub entity: Entity,
pub drag_delta: egui::Vec2,
}
#[derive(Event)]
pub struct TransitionCreationRequested {
pub source_entity: Entity,
}
#[derive(Event)]
pub struct CreateTransition {
pub source_entity: Entity,
pub target_entity: Entity,
pub event_type: String,
}
#[derive(Event)]
pub struct SaveStateMachine {
pub entity: Entity,
}
#[derive(Event)]
pub struct DeleteTransition {
pub source_entity: Entity,
pub target_entity: Entity,
pub event_type: String,
}
#[derive(Event)]
pub struct DeleteNode {
pub entity: Entity,
}
#[derive(Clone)]
pub struct TransitionPulse {
pub source_entity: Entity,
pub target_entity: Entity,
pub timer: Timer,
}
impl TransitionPulse {
pub fn new(source_entity: Entity, target_entity: Entity) -> Self {
Self {
source_entity,
target_entity,
timer: Timer::from_seconds(0.4, TimerMode::Once),
}
}
pub fn intensity(&self) -> f32 {
1.0 - self.timer.fraction()
}
}
pub const ACTIVE_STATE_COLOR: egui::Color32 = egui::Color32::from_rgb(255, 215, 0); pub const NORMAL_NODE_COLOR: egui::Color32 = egui::Color32::from_rgb(60, 60, 60); pub const TRANSITION_COLOR: egui::Color32 = egui::Color32::WHITE;
pub fn get_node_color(entity: Entity, active_query: &Query<&Active>) -> egui::Color32 {
if active_query.contains(entity) {
ACTIVE_STATE_COLOR
} else {
NORMAL_NODE_COLOR
}
}
pub fn get_transition_color(source: Entity, target: Entity, pulses: &[TransitionPulse]) -> egui::Color32 {
let base_transition_color = NORMAL_NODE_COLOR;
if let Some(pulse) = pulses.iter().find(|p| p.source_entity == source && p.target_entity == target) {
let intensity = pulse.intensity();
lerp_color(base_transition_color, ACTIVE_STATE_COLOR, intensity)
} else {
base_transition_color
}
}
fn lerp_color(from: egui::Color32, to: egui::Color32, t: f32) -> egui::Color32 {
let t = t.clamp(0.0, 1.0);
egui::Color32::from_rgb(
((from.r() as f32) * (1.0 - t) + (to.r() as f32) * t) as u8,
((from.g() as f32) * (1.0 - t) + (to.g() as f32) * t) as u8,
((from.b() as f32) * (1.0 - t) + (to.b() as f32) * t) as u8,
)
}
pub fn draw_interactive_pill_label(
ui: &mut egui::Ui,
position: egui::Pos2,
text: &str,
font_id: egui::FontId,
is_dragging: bool,
color: egui::Color32,
) -> egui::Response {
let galley = ui.fonts(|f| f.layout_no_wrap(text.to_string(), font_id, egui::Color32::WHITE));
let text_size = galley.size();
let padding = egui::Vec2::new(8.0, 4.0);
let pill_size = text_size + padding * 2.0;
let pill_rect = egui::Rect::from_center_size(position, pill_size);
let response = ui.allocate_rect(pill_rect, egui::Sense::click_and_drag());
let painter = ui.painter();
let bg_color = if is_dragging {
egui::Color32::from_rgb(
(color.r() as f32 * 1.2).min(255.0) as u8,
(color.g() as f32 * 1.2).min(255.0) as u8,
(color.b() as f32 * 1.2).min(255.0) as u8,
)
} else {
color
};
painter.rect_filled(
pill_rect,
egui::CornerRadius::same((pill_size.y / 2.0) as u8),
bg_color,
);
painter.rect_stroke(
pill_rect,
egui::CornerRadius::same((pill_size.y / 2.0) as u8),
egui::Stroke::new(1.0, egui::Color32::WHITE),
egui::StrokeKind::Outside,
);
let text_pos = pill_rect.center() - text_size * 0.5;
painter.galley(text_pos, galley, egui::Color32::WHITE);
response
}
pub struct RenderItem {
pub entity: Entity,
pub z_order: i32,
}
#[derive(Debug, Clone)]
pub struct TransitionConnection {
pub source_entity: Entity,
pub edge_entity: Entity,
pub target_entity: Entity,
pub event_type: String,
pub source_rect: egui::Rect,
pub target_rect: egui::Rect,
pub event_node_position: egui::Pos2,
pub is_dragging_event_node: bool,
pub event_node_offset: egui::Vec2,
}
pub fn get_entity_name(entity: Entity, all_entities: &Query<(Entity, Option<&Name>, Option<&InitialState>)>) -> String {
if let Ok((_, name_opt, _)) = all_entities.get(entity) {
if let Some(name) = name_opt {
name.as_str().to_string()
} else {
format!("Entity {:?}", entity)
}
} else {
format!("Unknown Entity {:?}", entity)
}
}
pub fn get_entity_name_from_world(entity: Entity, world: &mut World) -> String {
let mut query = world.query::<(Entity, Option<&Name>)>();
if let Ok((_, name_opt)) = query.get(world, entity) {
if let Some(name) = name_opt {
name.as_str().to_string()
} else {
format!("Entity {:?}", entity)
}
} else {
format!("Unknown Entity {:?}", entity)
}
}
pub fn should_get_selection_boost(
entity: Entity,
selected_node: Option<Entity>,
child_of_query: &Query<&bevy_gearbox::StateChildOf>,
) -> bool {
if let Some(selected) = selected_node {
if entity == selected {
return true;
}
let mut current = selected;
while let Ok(child_of) = child_of_query.get(current) {
if child_of.0 == entity {
return true;
}
current = child_of.0;
}
}
false
}
pub fn closest_point_on_rect_edge(rect: egui::Rect, point: egui::Pos2) -> egui::Pos2 {
let center = rect.center();
let direction = point - center;
let t_left = if direction.x != 0.0 { (rect.min.x - center.x) / direction.x } else { f32::INFINITY };
let t_right = if direction.x != 0.0 { (rect.max.x - center.x) / direction.x } else { f32::INFINITY };
let t_top = if direction.y != 0.0 { (rect.min.y - center.y) / direction.y } else { f32::INFINITY };
let t_bottom = if direction.y != 0.0 { (rect.max.y - center.y) / direction.y } else { f32::INFINITY };
let mut min_t = f32::INFINITY;
if t_left > 0.0 { min_t = min_t.min(t_left); }
if t_right > 0.0 { min_t = min_t.min(t_right); }
if t_top > 0.0 { min_t = min_t.min(t_top); }
if t_bottom > 0.0 { min_t = min_t.min(t_bottom); }
if min_t == f32::INFINITY {
center
} else {
center + direction * min_t
}
}
pub fn draw_arrow(painter: &egui::Painter, start: egui::Pos2, end: egui::Pos2, color: egui::Color32) {
let stroke = egui::Stroke::new(2.0, color);
painter.line_segment([start, end], stroke);
let direction = (end - start).normalized();
let arrow_length = 8.0;
let arrow_angle = std::f32::consts::PI / 6.0;
let arrow_point1 = end - direction * arrow_length;
let perpendicular = egui::Vec2::new(-direction.y, direction.x);
let arrow_head1 = arrow_point1 + perpendicular * arrow_length * arrow_angle.sin();
let arrow_head2 = arrow_point1 - perpendicular * arrow_length * arrow_angle.sin();
painter.line_segment([end, arrow_head1], stroke);
painter.line_segment([end, arrow_head2], stroke);
}