use bevy::prelude::*;
use bevy::ecs::reflect::ReflectComponent;
use bevy_egui::egui;
use bevy_inspector_egui::{
bevy_inspector::ui_for_entity,
bevy_egui::EguiContext,
};
use crate::editor_state::{EditorState, EditorWindow, InspectorTab, get_entity_name_from_world};
fn try_add_component_via_reflection(world: &mut World, entity: Entity, component_type_name: &str) -> bool {
let component_name = component_type_name.split("::").last().unwrap_or(component_type_name);
let (reflect_component, default_component) = get_component_reflection_data(world, component_type_name);
if let (Some(reflect_component), Some(component)) = (reflect_component, default_component) {
insert_default_component(world, entity, reflect_component, component, component_name)
} else {
info!("❌ Component {} not found or missing ReflectDefault/ReflectComponent", component_name);
false
}
}
fn get_component_reflection_data(
world: &mut World,
component_type_name: &str
) -> (Option<ReflectComponent>, Option<Box<dyn PartialReflect>>) {
let type_registry = world.resource::<AppTypeRegistry>();
let registry = type_registry.read();
if let Some(registration) = registry.get_with_type_path(component_type_name) {
if let Some(reflect_default) = registration.data::<ReflectDefault>() {
if let Some(reflect_component) = registration.data::<ReflectComponent>() {
let default_component = reflect_default.default();
let reflect_component_clone = reflect_component.clone();
return (Some(reflect_component_clone), Some(default_component));
}
}
}
(None, None)
}
fn insert_default_component(
world: &mut World,
entity: Entity,
reflect_component: ReflectComponent,
component: Box<dyn PartialReflect>,
component_name: &str,
) -> bool {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if world.get_entity(entity).is_err() {
info!("❌ Entity {:?} does not exist", entity);
return;
}
let type_registry = world.resource::<AppTypeRegistry>().clone();
let registry = type_registry.read();
let mut entity_mut = world.entity_mut(entity);
reflect_component.insert(&mut entity_mut, component.as_partial_reflect(), ®istry);
}));
match result {
Ok(_) => {
info!("✅ Successfully added {} to entity {:?}", component_name, entity);
true
},
Err(_) => {
info!("❌ Failed to add {} - insertion failed", component_name);
false
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ComponentHierarchy {
pub components: std::collections::BTreeMap<String, ComponentNode>,
}
#[derive(Debug, Clone)]
pub enum ComponentNode {
Component(String), Namespace(std::collections::BTreeMap<String, ComponentNode>),
}
fn get_available_components_hierarchical(world: &World) -> ComponentHierarchy {
let type_registry = world.resource::<AppTypeRegistry>();
let registry = type_registry.read();
let mut hierarchy = ComponentHierarchy::default();
for registration in registry.iter() {
if registration.data::<ReflectComponent>().is_some()
&& registration.data::<ReflectDefault>().is_some() {
let type_path = registration.type_info().type_path();
insert_component_into_hierarchy(&mut hierarchy.components, type_path);
}
}
hierarchy
}
fn insert_component_into_hierarchy(
map: &mut std::collections::BTreeMap<String, ComponentNode>,
type_path: &str
) {
let parts: Vec<&str> = type_path.split("::").collect();
if parts.len() == 1 {
map.insert(parts[0].to_string(), ComponentNode::Component(type_path.to_string()));
return;
}
let mut current_map = map;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
current_map.insert(part.to_string(), ComponentNode::Component(type_path.to_string()));
} else {
let entry = current_map.entry(part.to_string()).or_insert_with(|| {
ComponentNode::Namespace(std::collections::BTreeMap::new())
});
match entry {
ComponentNode::Namespace(ref mut nested_map) => {
current_map = nested_map;
}
ComponentNode::Component(_) => {
return;
}
}
}
}
}
pub fn entity_inspector_system(world: &mut World) {
let inspected_entity = if let Some(editor_state) = world.get_resource::<EditorState>() {
editor_state.inspected_entity
} else {
return;
};
if let Some(inspected_entity) = inspected_entity {
let entity_name = get_entity_name_from_world(inspected_entity, world);
let Ok(egui_context) = world
.query_filtered::<&mut EguiContext, (With<EditorWindow>, Without<bevy_egui::PrimaryEguiContext>)>()
.single(world)
else {
return;
};
let mut ctx = egui_context.clone();
let mut keep_open = true;
egui::Window::new(format!("Inspector: {}", entity_name))
.default_width(300.0)
.resizable(true)
.vscroll(true)
.open(&mut keep_open)
.show(ctx.get_mut(), |ui| {
if world.entities().contains(inspected_entity) {
render_inspector_tabs(world, inspected_entity, ui);
} else {
ui.label("Entity no longer exists");
}
});
if !keep_open {
if let Some(mut editor_state) = world.get_resource_mut::<EditorState>() {
editor_state.inspected_entity = None;
}
}
}
}
fn render_inspector_tabs(world: &mut World, entity: Entity, ui: &mut egui::Ui) {
let mut editor_state = world.remove_resource::<EditorState>().unwrap_or_default();
ui.horizontal(|ui| {
if ui.selectable_label(editor_state.inspector_tab == InspectorTab::Inspect, "🔍 Inspect").clicked() {
editor_state.inspector_tab = InspectorTab::Inspect;
}
if ui.selectable_label(editor_state.inspector_tab == InspectorTab::Remove, "🗑 Remove").clicked() {
editor_state.inspector_tab = InspectorTab::Remove;
}
if ui.selectable_label(editor_state.inspector_tab == InspectorTab::Add, "➕ Add").clicked() {
editor_state.inspector_tab = InspectorTab::Add;
}
});
ui.separator();
world.insert_resource(editor_state);
let current_tab = world.resource::<EditorState>().inspector_tab.clone();
match current_tab {
InspectorTab::Inspect => {
ui_for_entity(world, entity, ui);
}
InspectorTab::Remove => {
render_component_removal_ui(world, entity, ui);
}
InspectorTab::Add => {
render_component_addition_ui(world, entity, ui);
}
}
}
fn render_component_addition_ui(world: &mut World, entity: Entity, ui: &mut egui::Ui) {
let mut editor_state = world.remove_resource::<EditorState>().unwrap_or_default();
if editor_state.component_addition.component_hierarchy.is_none() {
let hierarchy = get_available_components_hierarchical(world);
editor_state.component_addition.update_hierarchy(hierarchy);
}
ui.text_edit_singleline(&mut editor_state.component_addition.search_text);
let dropdown_response = ui.button("Select Component ▼");
if dropdown_response.clicked() {
editor_state.component_addition.dropdown_open = !editor_state.component_addition.dropdown_open;
}
if editor_state.component_addition.dropdown_open {
ui.separator();
egui::ScrollArea::vertical()
.max_height(300.0)
.show(ui, |ui| {
let hierarchy_clone = editor_state.component_addition.component_hierarchy.clone();
let search_text = editor_state.component_addition.search_text.clone();
if let Some(hierarchy) = hierarchy_clone {
if search_text.is_empty() {
render_component_hierarchy(
ui,
&hierarchy.components,
String::new(),
&mut editor_state.component_addition,
world,
entity
);
} else {
render_filtered_components(
ui,
&hierarchy.components,
&search_text,
world,
entity,
&mut editor_state.component_addition
);
}
}
});
}
world.insert_resource(editor_state);
}
fn render_component_hierarchy(
ui: &mut egui::Ui,
components: &std::collections::BTreeMap<String, ComponentNode>,
namespace_path: String,
state: &mut crate::editor_state::ComponentAdditionState,
world: &mut World,
entity: Entity,
) {
for (name, node) in components {
let current_path = if namespace_path.is_empty() {
name.clone()
} else {
format!("{}::{}", namespace_path, name)
};
match node {
ComponentNode::Component(full_type_path) => {
if ui.button(name).clicked() {
try_add_component_via_reflection(world, entity, full_type_path);
state.dropdown_open = false;
}
}
ComponentNode::Namespace(nested_components) => {
let is_expanded = state.is_namespace_expanded(¤t_path);
let expand_symbol = if is_expanded { "▼" } else { "▶" };
if ui.button(format!("{} {}", expand_symbol, name)).clicked() {
state.toggle_namespace(¤t_path);
}
if is_expanded {
ui.indent(format!("indent_{}", current_path), |ui| {
render_component_hierarchy(ui, nested_components, current_path, state, world, entity);
});
}
}
}
}
}
fn render_filtered_components(
ui: &mut egui::Ui,
components: &std::collections::BTreeMap<String, ComponentNode>,
search_text: &str,
world: &mut World,
entity: Entity,
state: &mut crate::editor_state::ComponentAdditionState,
) {
let search_lower = search_text.to_lowercase();
let mut found_any = false;
collect_matching_components(components, &search_lower, ui, world, entity, state, &mut found_any);
if !found_any {
ui.label("No matching components found");
}
}
fn collect_matching_components(
components: &std::collections::BTreeMap<String, ComponentNode>,
search_lower: &str,
ui: &mut egui::Ui,
world: &mut World,
entity: Entity,
state: &mut crate::editor_state::ComponentAdditionState,
found_any: &mut bool,
) {
for (name, node) in components {
match node {
ComponentNode::Component(full_type_path) => {
if name.to_lowercase().contains(search_lower) {
*found_any = true;
if ui.button(format!("{} ({})", name, full_type_path)).clicked() {
try_add_component_via_reflection(world, entity, full_type_path);
state.dropdown_open = false;
state.search_text.clear();
}
}
}
ComponentNode::Namespace(nested_components) => {
collect_matching_components(nested_components, search_lower, ui, world, entity, state, found_any);
}
}
}
}
fn render_component_removal_ui(world: &mut World, entity: Entity, ui: &mut egui::Ui) {
let components = get_entity_components(world, entity);
if components.is_empty() {
ui.label("No removable components found.");
return;
}
let mut components_to_remove = Vec::new();
for (component_name, type_id) in &components {
if is_essential_component(component_name) {
continue;
}
ui.horizontal(|ui| {
ui.label(component_name);
if ui.button("🗑 Remove").clicked() {
components_to_remove.push(*type_id);
info!("🗑️ Queued component {} for removal from entity {:?}", component_name, entity);
}
});
}
for type_id in components_to_remove {
remove_component_by_type_id(world, entity, type_id);
}
}
fn get_entity_components(world: &World, entity: Entity) -> Vec<(String, std::any::TypeId)> {
let mut components = Vec::new();
let type_registry = world.resource::<AppTypeRegistry>();
let registry = type_registry.read();
for registration in registry.iter() {
if let Some(reflect_component) = registration.data::<ReflectComponent>() {
if reflect_component.reflect(world.entity(entity)).is_some() {
let type_name = registration.type_info().type_path_table().short_path()
.to_string();
components.push((type_name, registration.type_id()));
}
}
}
components.sort_by(|a, b| a.0.cmp(&b.0));
components
}
fn is_essential_component(component_name: &str) -> bool {
match component_name {
"Entity" | "Transform" | "GlobalTransform" => true,
"Parent" | "Children" | "ChildOf" => true,
"Name" => true,
"StateMachine" | "StateMachinePersistentData" | "StateMachineTransientData" => true,
_ => false,
}
}
fn remove_component_by_type_id(world: &mut World, entity: Entity, type_id: std::any::TypeId) {
let (reflect_component, component_name) = {
let type_registry = world.resource::<AppTypeRegistry>();
let registry = type_registry.read();
if let Some(registration) = registry.get(type_id) {
if let Some(reflect_component) = registration.data::<ReflectComponent>() {
let component_name = registration.type_info().type_path_table().short_path().to_string();
(Some(reflect_component.clone()), component_name)
} else {
return;
}
} else {
return;
}
};
if let Some(reflect_component) = reflect_component {
if reflect_component.reflect(world.entity(entity)).is_some() {
reflect_component.remove(&mut world.entity_mut(entity));
info!("✅ Removed component {} from entity {:?}", component_name, entity);
} else {
warn!("⚠️ Component {} not found on entity {:?}", component_name, entity);
}
}
}