diff --git a/crates/bevy_editor/src/editor.rs b/crates/bevy_editor/src/editor.rs index 2feb12193a..324cf48d10 100644 --- a/crates/bevy_editor/src/editor.rs +++ b/crates/bevy_editor/src/editor.rs @@ -36,7 +36,7 @@ use bevy::prelude::*; -use crate::remote::{client, RemoteClientPlugin, types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus, RemoteEntity}}; +use crate::remote::{client, RemoteClientPlugin, types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus}}; use crate::panels::{EntityListPlugin, ComponentInspectorPlugin}; use crate::widgets::{WidgetsPlugin, ScrollViewBuilder, ScrollContent}; @@ -71,7 +71,6 @@ impl Plugin for EditorPlugin { .add_systems(Startup, setup_editor_ui) .add_systems(Update, ( setup_scroll_content_markers, - refresh_entity_list, handle_entity_selection, update_entity_button_colors, handle_component_inspection, @@ -433,102 +432,7 @@ fn update_remote_connection( } } -/// Refresh the entity list display -fn refresh_entity_list( - editor_state: Res, - mut commands: Commands, - entity_list_area_query: Query>, - list_items_query: Query>, - mut local_entity_count: Local, -) { - // Only refresh when the actual entity count changes, not on every state change - let current_count = editor_state.entities.len(); - if *local_entity_count == current_count { - return; - } - *local_entity_count = current_count; - - // Clear existing list items - for entity in &list_items_query { - commands.entity(entity).despawn(); - } - - // Find the entity list area and add new items - for list_area_entity in entity_list_area_query.iter() { - // Clear children by despawning them - commands.entity(list_area_entity).despawn_children(); - - commands.entity(list_area_entity).with_children(|parent| { - if editor_state.entities.is_empty() { - // Show empty state - parent.spawn(( - Text::new("No entities connected.\nStart a bevy_remote server to see entities."), - TextFont { - font_size: 12.0, - ..default() - }, - TextColor(Color::srgb(0.6, 0.6, 0.6)), - Node { - padding: UiRect::all(Val::Px(16.0)), - ..default() - }, - )); - } else { - // Add entity items - for remote_entity in &editor_state.entities { - create_entity_list_item(parent, remote_entity, &editor_state); - } - } - }); - } -} - -fn create_entity_list_item(parent: &mut ChildSpawnerCommands, remote_entity: &RemoteEntity, editor_state: &EditorState) { - // Determine the correct background color based on selection state - let bg_color = if Some(remote_entity.id) == editor_state.selected_entity_id { - Color::srgb(0.3, 0.4, 0.5) // Selected state - } else { - Color::srgb(0.2, 0.2, 0.2) // Default state - }; - - parent - .spawn(( - Button, - Node { - width: Val::Percent(100.0), - height: Val::Px(32.0), - align_items: AlignItems::Center, - padding: UiRect::all(Val::Px(10.0)), - margin: UiRect::bottom(Val::Px(2.0)), - border: UiRect::all(Val::Px(1.0)), - ..default() - }, - BackgroundColor(bg_color), - BorderColor::all(Color::srgb(0.3, 0.3, 0.3)), - EntityListItem::from_remote_entity(&remote_entity), - )) - .with_children(|parent| { - // Entity icon and name - parent.spawn(( - Node { - flex_direction: FlexDirection::Row, - align_items: AlignItems::Center, - ..default() - }, - )).with_children(|parent| { - parent.spawn(( - Text::new(format!("Entity {}", remote_entity.id)), - TextFont { - font_size: 13.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - )); - }); - }); -} - -/// Create status bar with connection info +/// Update status bar with connection info fn update_status_bar( editor_state: Res, remote_conn: Res, diff --git a/crates/bevy_editor/src/remote/entity_naming.rs b/crates/bevy_editor/src/remote/entity_naming.rs new file mode 100644 index 0000000000..873b417b09 --- /dev/null +++ b/crates/bevy_editor/src/remote/entity_naming.rs @@ -0,0 +1,314 @@ +//! Smart entity naming system for the editor +//! +//! This module provides intelligent naming for entities based on their components. +//! It follows a precedence system: +//! 1. Name component (highest priority) +//! 2. Common Bevy components (Camera, Window, etc.) +//! 3. User components +//! 4. Entity ID fallback + +use super::types::RemoteEntity; +use serde_json::Value; + +/// Component precedence levels for naming +#[derive(Debug, PartialEq, PartialOrd, Clone)] +pub enum ComponentPrecedence { + /// Built-in Name component - highest priority + Name = 0, + /// User/custom components - very high priority for meaningful names + UserComponent = 1, + /// Primary Bevy components (Camera, Window, etc.) + PrimaryBevy = 2, + /// Secondary Bevy components (Transform, Visibility, etc.) + SecondaryBevy = 3, + /// Entity ID fallback - lowest priority + EntityId = 4, +} + +/// A component that can provide a name for an entity +#[derive(Debug, Clone)] +pub struct NamingComponent { + pub name: String, + pub precedence: ComponentPrecedence, + pub display_name: String, +} + +/// Generate a smart display name for an entity based on its components +pub fn generate_entity_display_name(entity: &RemoteEntity, component_data: Option<&Value>) -> String { + let mut best_naming: Option = None; + + // First check if we have component data with actual Name values + if let Some(data) = component_data { + if let Some(name_value) = extract_name_from_component_data(data) { + return format!("#{} ({})", entity.id, name_value); + } + } + + // Analyze available components to find the best naming option + for component_name in &entity.components { + if let Some(naming) = analyze_component_for_naming(component_name) { + if best_naming.is_none() || naming.precedence < best_naming.as_ref().unwrap().precedence { + best_naming = Some(naming); + } + } + } + + // Also check full component names for more accurate detection + for full_component_name in &entity.full_component_names { + if let Some(naming) = analyze_component_for_naming(full_component_name) { + if best_naming.is_none() || naming.precedence < best_naming.as_ref().unwrap().precedence { + best_naming = Some(naming); + } + } + } + + match best_naming { + Some(naming) if naming.precedence != ComponentPrecedence::EntityId => { + format!("#{} ({})", entity.id, naming.display_name) + } + _ => { + // Fallback to entity ID + format!("Entity {}", entity.id) + } + } +} + +/// Extract the actual name value from component data JSON +fn extract_name_from_component_data(component_data: &Value) -> Option { + if let Some(components) = component_data.get("components").and_then(|v| v.as_object()) { + // Look for Name component in various forms + for (component_type, component_value) in components { + if component_type.contains("Name") || component_type.ends_with("::Name") { + // Try to extract the actual name value + if let Some(name_str) = component_value.as_str() { + return Some(name_str.to_string()); + } + // Handle nested name values + if let Some(obj) = component_value.as_object() { + if let Some(name_val) = obj.get("name").or_else(|| obj.get("value")).or_else(|| obj.get("0")) { + if let Some(name_str) = name_val.as_str() { + return Some(name_str.to_string()); + } + } + } + } + } + } + None +} + +/// Analyze a component name to determine if it can provide entity naming +fn analyze_component_for_naming(component_name: &str) -> Option { + // Remove module paths for analysis + let clean_name = component_name + .split("::") + .last() + .unwrap_or(component_name); + + // Check for Name component (highest priority) + if clean_name == "Name" || component_name.ends_with("::Name") { + return Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::Name, + display_name: "Named Entity".to_string(), + }); + } + + // Primary Bevy components that are very distinctive + match clean_name { + "Camera" | "Camera2d" | "Camera3d" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Camera".to_string(), + }), + "Window" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Window".to_string(), + }), + "DirectionalLight" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Directional Light".to_string(), + }), + "PointLight" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Point Light".to_string(), + }), + "SpotLight" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Spot Light".to_string(), + }), + "AudioListener" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Audio Listener".to_string(), + }), + "AudioSource" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::PrimaryBevy, + display_name: "Audio Source".to_string(), + }), + + // Secondary Bevy components (lower priority) + "Mesh3d" | "Mesh2d" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Mesh".to_string(), + }), + "MeshMaterial3d" | "MeshMaterial2d" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Material".to_string(), + }), + "Text" | "Text2d" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Text".to_string(), + }), + "Sprite" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Sprite".to_string(), + }), + "ImageNode" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Image".to_string(), + }), + "Button" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "Button".to_string(), + }), + "Node" => Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::SecondaryBevy, + display_name: "UI Node".to_string(), + }), + + _ => { + // Check if it's likely a user component (not starting with common Bevy prefixes) + if !is_bevy_builtin_component(component_name) { + Some(NamingComponent { + name: component_name.to_string(), + precedence: ComponentPrecedence::UserComponent, + display_name: clean_name.to_string(), + }) + } else { + None + } + } + } +} + +/// Check if a component is likely a Bevy built-in component +fn is_bevy_builtin_component(component_name: &str) -> bool { + let bevy_prefixes = [ + "bevy_", + "std::", + "core::", + "alloc::", + "winit::", + ]; + + let has_bevy_prefix = bevy_prefixes.iter().any(|prefix| component_name.starts_with(prefix)); + let is_common_bevy = matches!(component_name.split("::").last().unwrap_or(""), + "Transform" | "GlobalTransform" | "Visibility" | "InheritedVisibility" | + "ViewVisibility" | "ComputedVisibility" | "Parent" | "Children" | "Aabb" | + "TransformTreeChanged" | "Frustum" | "Projection" | "VisibleEntities" | + "DebandDither" | "Tonemapping" | "ClusterConfig" | "RenderEntity" | + "SyncToRenderWorld" | "Msaa" | "CubemapFrusta" | "CubemapVisibleEntities" + ); + + has_bevy_prefix || is_common_bevy +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_entity_naming_precedence() { + let mut entity = RemoteEntity { + id: 123, + components: vec!["Camera".to_string(), "Transform".to_string()], + full_component_names: vec!["bevy_render::camera::Camera".to_string(), "bevy_transform::components::Transform".to_string()], + }; + + let display_name = generate_entity_display_name(&entity, None); + assert_eq!(display_name, "#123 (Camera)"); + + // Test with user component (should take precedence over Camera) + entity.components.push("Player".to_string()); + entity.full_component_names.push("my_game::Player".to_string()); + + let display_name = generate_entity_display_name(&entity, None); + assert_eq!(display_name, "#123 (Player)"); + + // Test with Name component (should take precedence over everything) + entity.components.push("Name".to_string()); + entity.full_component_names.push("bevy_core::name::Name".to_string()); + + let display_name = generate_entity_display_name(&entity, None); + assert_eq!(display_name, "#123 (Named Entity)"); + } + + #[test] + fn test_user_component_precedence_over_bevy() { + let entity = RemoteEntity { + id: 456, + components: vec!["Cube".to_string(), "Mesh3d".to_string(), "Transform".to_string()], + full_component_names: vec![ + "my_game::Cube".to_string(), + "bevy_mesh::mesh::Mesh3d".to_string(), + "bevy_transform::components::Transform".to_string() + ], + }; + + let display_name = generate_entity_display_name(&entity, None); + // User component "Cube" should take precedence over "Mesh3d" + assert_eq!(display_name, "#456 (Cube)"); + } + + #[test] + fn test_fallback_to_entity_id() { + let entity = RemoteEntity { + id: 789, + components: vec!["Transform".to_string(), "Visibility".to_string()], + full_component_names: vec!["bevy_transform::components::Transform".to_string(), "bevy_render::view::visibility::Visibility".to_string()], + }; + + let display_name = generate_entity_display_name(&entity, None); + assert_eq!(display_name, "Entity 789"); + } + + #[test] + fn test_cube_example_from_server() { + // Simulate the cube entity from server.rs example + let entity = RemoteEntity { + id: 42, + components: vec![ + "Aabb".to_string(), + "Cube".to_string(), + "Mesh3d".to_string(), + "MeshMaterial3d".to_string(), + "Transform".to_string(), + ], + full_component_names: vec![ + "bevy_camera::primitives::Aabb".to_string(), + "server::Cube".to_string(), + "bevy_mesh::mesh::Mesh3d".to_string(), + "bevy_pbr::material::MeshMaterial3d".to_string(), + "bevy_transform::components::Transform".to_string(), + ], + }; + + let display_name = generate_entity_display_name(&entity, None); + // User component "Cube" should take precedence over all Bevy components including Aabb + assert_eq!(display_name, "#42 (Cube)"); + println!("Cube entity correctly named: {}", display_name); + } +} diff --git a/crates/bevy_editor/src/remote/mod.rs b/crates/bevy_editor/src/remote/mod.rs index c8fd7c9889..1da7fead11 100644 --- a/crates/bevy_editor/src/remote/mod.rs +++ b/crates/bevy_editor/src/remote/mod.rs @@ -34,11 +34,13 @@ pub mod types; pub mod client; pub mod connection; +pub mod entity_naming; use bevy::prelude::*; pub use types::*; pub use client::*; pub use connection::*; +pub use entity_naming::*; /// Plugin that handles remote connection functionality #[derive(Default)] diff --git a/crates/bevy_editor/src/widgets/list_view.rs b/crates/bevy_editor/src/widgets/list_view.rs index 739ca1ff06..f3e667592b 100644 --- a/crates/bevy_editor/src/widgets/list_view.rs +++ b/crates/bevy_editor/src/widgets/list_view.rs @@ -281,9 +281,27 @@ pub struct EntityListItem { impl EntityListItem { pub fn from_remote_entity(remote_entity: &crate::remote::types::RemoteEntity) -> Self { + // Use the smart naming system to generate a meaningful display name + let display_name = crate::remote::entity_naming::generate_entity_display_name(remote_entity, None); + Self { entity_id: remote_entity.id, - name: format!("Entity {}", remote_entity.id), + name: display_name, + components: remote_entity.components.clone(), + children_count: 0, + } + } + + /// Create EntityListItem with component data for better Name extraction + pub fn from_remote_entity_with_data( + remote_entity: &crate::remote::types::RemoteEntity, + component_data: Option<&serde_json::Value> + ) -> Self { + let display_name = crate::remote::entity_naming::generate_entity_display_name(remote_entity, component_data); + + Self { + entity_id: remote_entity.id, + name: display_name, components: remote_entity.components.clone(), children_count: 0, } @@ -292,7 +310,8 @@ impl EntityListItem { impl ListDisplayable for EntityListItem { fn display_text(&self) -> String { - format!("Entity {} ({})", self.entity_id, self.name) + // The name already contains the smart formatting (e.g., "#123 (Camera)") + self.name.clone() } }