Skip to content

feat: Add comprehensive read-only Bevy Inspector with remote inspection capabilities #20189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ assets/scenes/load_scene_example-new.scn.ron

# Generated by "examples/window/screenshot.rs"
**/screenshot-*.png
.DS_Store
assets/.DS_Store
benches/.DS_Store
crates/.DS_Store
examples/.DS_Store
release-content/.DS_Store
tests/.DS_Store
tools/.DS_Store
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,13 @@ name = "bloom_2d"
path = "examples/2d/bloom_2d.rs"
doc-scrape-examples = true

# Inspector
[[example]]
name = "inspector"
path = "examples/dev_tools/inspector.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[package.metadata.example.bloom_2d]
name = "2D Bloom"
description = "Illustrates bloom post-processing in 2d"
Expand Down
13 changes: 11 additions & 2 deletions crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,39 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
bevy_ci_testing = ["serde", "ron"]
bevy_ci_testing = ["ron"]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" }
bevy_core_widgets = { path = "../bevy_core_widgets", version = "0.17.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" }
bevy_ui_render = { path = "../bevy_ui_render", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.17.0-dev" }

# other
serde = { version = "1.0", features = ["derive"], optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
rand = { version = "0.8" }
ron = { version = "0.10", optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "2.0" }

[lints]
workspace = true
Expand Down
125 changes: 125 additions & 0 deletions crates/bevy_dev_tools/src/inspector/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Inspector Component Markers and State

use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use std::collections::{HashMap, HashSet};
use bevy_reflect::Reflect;

/// Marker component to exclude inspector's own entities from being inspected
#[derive(Component, Reflect)]
pub struct InspectorMarker;

/// Component for the main inspector window root
#[derive(Component)]
pub struct InspectorWindowRoot {
pub window_entity: Entity,
}

/// Component for the inspector tree/list container
#[derive(Component)]
pub struct InspectorTreeRoot;

/// Component for collapsible group headers with disclosure triangles
#[derive(Component, Clone)]
pub struct DisclosureTriangle {
pub group_id: String,
pub is_expanded: bool,
pub entity_count: usize,
}

/// Component for individual entity rows in the inspector
#[derive(Component, Clone)]
pub struct EntityRow {
pub entity_id: Entity,
pub display_name: String,
pub component_names: Vec<String>,
pub is_selected: bool,
}

/// Component for component detail panels
#[derive(Component)]
pub struct ComponentDetailPanel {
pub entity_id: Entity,
pub component_type_name: String,
}

/// Component for scrollable containers
#[derive(Component)]
pub struct ScrollableContainer {
pub scroll_position: f32,
pub content_height: f32,
}

/// Resource tracking inspector UI state
#[derive(bevy_ecs::resource::Resource)]
pub struct InspectorState {
/// Currently expanded groups
pub expanded_groups: HashSet<String>,
/// Currently selected entity (for detailed view)
pub selected_entity: Option<Entity>,
/// Search filter text
pub search_filter: String,
/// Last refresh timestamp
pub last_refresh: std::time::Instant,
/// Window visibility state
pub window_visible: bool,
/// Current view mode (tree, list, detailed)
pub view_mode: ViewMode,
/// Cached entity groupings
pub entity_groups: HashMap<String, Vec<Entity>>,
/// Inspector window entity (for separate window)
pub inspector_window_entity: Option<Entity>,
}

impl InspectorState {
pub fn new() -> Self {
Self {
expanded_groups: HashSet::new(),
selected_entity: None,
search_filter: String::new(),
last_refresh: std::time::Instant::now(),
window_visible: false,
view_mode: ViewMode::Tree,
entity_groups: HashMap::new(),
inspector_window_entity: None,
}
}

pub fn toggle_group(&mut self, group_id: &str) {
if self.expanded_groups.contains(group_id) {
self.expanded_groups.remove(group_id);
} else {
self.expanded_groups.insert(group_id.to_string());
}
}

pub fn is_group_expanded(&self, group_id: &str) -> bool {
self.expanded_groups.contains(group_id)
}

pub fn select_entity(&mut self, entity: Entity) {
self.selected_entity = Some(entity);
self.view_mode = ViewMode::Detailed;
}

pub fn clear_selection(&mut self) {
self.selected_entity = None;
self.view_mode = ViewMode::Tree;
}
}

/// Inspector view modes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ViewMode {
/// Hierarchical tree view grouped by components
#[default]
Tree,
/// Flat list view of all entities
List,
/// Detailed view of a single entity's components
Detailed,
}

/// Marker component for the details panel
#[derive(Component)]
pub struct InspectorDetailsPanel;
129 changes: 129 additions & 0 deletions crates/bevy_dev_tools/src/inspector/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Inspector Configuration and Settings

use bevy_ecs::resource::Resource;
use bevy_input::keyboard::KeyCode;
use std::collections::HashMap;

/// Main inspector configuration resource
#[derive(Clone, Resource, Debug)]
pub struct InspectorConfig {
/// Keyboard shortcut to toggle inspector window
pub toggle_key: KeyCode,
/// Inspector window dimensions
pub window_width: f32,
pub window_height: f32,
/// Window title
pub window_title: String,
/// Auto-refresh interval in seconds (0.0 = disabled)
pub auto_refresh_interval: f32,
/// Maximum entities to show per group before pagination
pub max_entities_per_group: usize,
/// Whether to show component count badges
pub show_component_counts: bool,
/// Whether to show entity IDs in the display
pub show_entity_ids: bool,
/// Custom entity grouping rules
pub grouping_rules: EntityGroupingRules,
/// UI styling preferences
pub styling: InspectorStyling,
}

impl Default for InspectorConfig {
fn default() -> Self {
Self {
toggle_key: KeyCode::F12,
window_width: 800.0,
window_height: 900.0,
window_title: "Bevy Entity Inspector".to_string(),
auto_refresh_interval: 0.5,
max_entities_per_group: 50,
show_component_counts: true,
show_entity_ids: true,
grouping_rules: EntityGroupingRules::default(),
styling: InspectorStyling::default(),
}
}
}

/// Entity grouping configuration
#[derive(Clone, Debug)]
pub struct EntityGroupingRules {
/// Priority order for component-based grouping
pub component_priority: Vec<String>,
/// Custom group names for specific component combinations
pub custom_group_names: HashMap<Vec<String>, String>,
/// Components to ignore when creating groups
pub ignored_components: Vec<String>,
}

impl Default for EntityGroupingRules {
fn default() -> Self {
Self {
component_priority: vec![
"Camera".to_string(),
"Mesh3d".to_string(),
"DirectionalLight".to_string(),
"PointLight".to_string(),
"SpotLight".to_string(),
"Transform".to_string(),
"GlobalTransform".to_string(),
"Name".to_string(),
],
custom_group_names: {
let mut map = HashMap::new();
map.insert(
vec!["Camera".to_string(), "Transform".to_string()],
"Cameras".to_string(),
);
map.insert(
vec!["Mesh3d".to_string(), "MeshMaterial3d<StandardMaterial>".to_string()],
"3D Objects".to_string(),
);
map.insert(
vec!["DirectionalLight".to_string()],
"Directional Lights".to_string(),
);
map.insert(
vec!["PointLight".to_string()],
"Point Lights".to_string(),
);
map
},
ignored_components: vec![
"InspectorMarker".to_string(),
"ComputedVisibility".to_string(),
"GlobalTransform".to_string(), // Often shown alongside Transform
],
}
}
}

/// UI styling configuration
#[derive(Clone, Debug)]
pub struct InspectorStyling {
pub background_color: (f32, f32, f32, f32),
pub header_color: (f32, f32, f32, f32),
pub text_color: (f32, f32, f32, f32),
pub highlight_color: (f32, f32, f32, f32),
pub font_size_header: f32,
pub font_size_normal: f32,
pub font_size_small: f32,
pub padding: f32,
pub margin: f32,
}

impl Default for InspectorStyling {
fn default() -> Self {
Self {
background_color: (0.15, 0.15, 0.15, 0.95),
header_color: (0.25, 0.25, 0.25, 1.0),
text_color: (0.9, 0.9, 0.9, 1.0),
highlight_color: (0.3, 0.7, 1.0, 1.0),
font_size_header: 18.0,
font_size_normal: 14.0,
font_size_small: 12.0,
padding: 8.0,
margin: 4.0,
}
}
}
Loading
Loading