Refactor: Remove inspector module and related components

- Deleted the entire inspector module including events, plugin, remote, selection, tree, and UI components.
- Removed associated structures and systems for handling entity and component data from the remote server.
- Cleaned up the entity list panel by removing unused scrollable container plugin.
- Updated widget module to remove legacy scrollable container and examples.
This commit is contained in:
jbuehler23 2025-07-18 15:56:32 +01:00
parent c683f3c4a4
commit 9e889396db
13 changed files with 11 additions and 1529 deletions

View File

@ -35,13 +35,10 @@
//! ```
use bevy::prelude::*;
use serde_json::Value;
// Import our modular components
use crate::remote::types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus, RemoteEntity, ComponentField};
use crate::panels::{EntityListPlugin, ComponentInspectorPlugin, parse_component_fields};
use crate::remote::{client, RemoteClientPlugin, types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus, RemoteEntity}};
use crate::panels::{EntityListPlugin, ComponentInspectorPlugin};
use crate::widgets::{WidgetsPlugin, ScrollViewBuilder, ScrollContent};
use crate::formatting::{format_value_inline, format_simple_value, is_simple_value, all_numbers};
/// Main plugin for the Bevy Editor that provides a comprehensive inspector interface.
///
@ -62,6 +59,7 @@ impl Plugin for EditorPlugin {
app
// Add sub-plugins for modular functionality
.add_plugins((
RemoteClientPlugin,
EntityListPlugin,
ComponentInspectorPlugin,
WidgetsPlugin,
@ -91,7 +89,7 @@ use crate::panels::{
ComponentInspector, ComponentInspectorContent,
EntityTree, EntityListArea
};
use crate::widgets::{ExpansionButton, EntityListItem};
use crate::widgets::EntityListItem;
/// Component for status bar
#[derive(Component)]
@ -312,7 +310,7 @@ fn handle_component_inspection(
// Use the full component names for the API request
if !selected_entity.full_component_names.is_empty() {
info!("Using component names: {:?}", selected_entity.full_component_names);
match remote_client::try_fetch_component_data_with_names(
match client::try_fetch_component_data_with_names(
&remote_conn.base_url,
selected_entity_id,
selected_entity.full_component_names.clone()
@ -387,8 +385,8 @@ fn handle_component_data_fetched(
TextColor(Color::srgb(0.65, 0.65, 0.65)),
));
} else {
// Build interactive component widgets
build_component_widgets(parent, event.entity_id, &event.component_data, &display_state);
// Use the modular component inspector implementation
crate::panels::component_inspector::build_component_widgets(parent, event.entity_id, &event.component_data, &display_state);
}
});
}
@ -412,7 +410,7 @@ fn update_remote_connection(
}
// Try to fetch entities using the remote client framework
match remote_client::try_fetch_entities(&remote_conn.base_url) {
match client::try_fetch_entities(&remote_conn.base_url) {
Ok(entities) => {
info!("Successfully fetched {} entities from remote server", entities.len());
commands.trigger(EntitiesFetched { entities });
@ -435,145 +433,6 @@ fn update_remote_connection(
}
}
/// TODO: Real bevy_remote integration framework
/// This module will contain the actual HTTP client integration when implemented
mod remote_client {
use super::*;
use bevy::remote::{
builtin_methods::{
BrpQuery, BrpQueryFilter, BrpQueryParams, ComponentSelector, BRP_QUERY_METHOD,
},
BrpRequest,
};
/// Attempts to connect to a bevy_remote server and fetch entity data
pub fn try_fetch_entities(base_url: &str) -> Result<Vec<RemoteEntity>, String> {
// Create a query to get all entities with their components
let query_request = BrpRequest {
jsonrpc: "2.0".to_string(),
method: BRP_QUERY_METHOD.to_string(),
id: Some(serde_json::to_value(1).map_err(|e| format!("JSON error: {}", e))?),
params: Some(
serde_json::to_value(BrpQueryParams {
data: BrpQuery {
components: Vec::default(), // Get all components
option: ComponentSelector::All,
has: Vec::default(),
},
strict: false,
filter: BrpQueryFilter::default(),
})
.map_err(|e| format!("Failed to serialize query params: {}", e))?,
),
};
// Make the HTTP request
let response = ureq::post(base_url)
.timeout(std::time::Duration::from_secs(2))
.send_json(&query_request)
.map_err(|e| format!("HTTP request failed: {}", e))?;
// Parse the response as JSON first
let json_response: serde_json::Value = response
.into_json()
.map_err(|e| format!("Failed to parse response: {}", e))?;
// Check if we have an error or result
if let Some(error) = json_response.get("error") {
return Err(format!("Server error: {}", error));
}
if let Some(result) = json_response.get("result") {
parse_brp_entities(result.clone())
} else {
Err("No result or error in response".to_string())
}
}
/// Fetch component data for a specific entity with explicit component names
pub fn try_fetch_component_data_with_names(base_url: &str, entity_id: u32, component_names: Vec<String>) -> Result<String, String> {
// Create a get request for specific entity with component names
let get_request = BrpRequest {
jsonrpc: "2.0".to_string(),
method: "bevy/get".to_string(),
id: Some(serde_json::to_value(2).map_err(|e| format!("JSON error: {}", e))?),
params: Some(
serde_json::json!({
"entity": entity_id as u64,
"components": component_names
})
),
};
let response = ureq::post(base_url)
.timeout(std::time::Duration::from_secs(2))
.send_json(&get_request)
.map_err(|e| format!("HTTP request failed: {}", e))?;
let json_response: serde_json::Value = response
.into_json()
.map_err(|e| format!("Failed to parse response: {}", e))?;
// Check if we have an error or result
if let Some(error) = json_response.get("error") {
return Err(format!("Server error: {}", error));
}
if let Some(result) = json_response.get("result") {
Ok(serde_json::to_string_pretty(result)
.unwrap_or_else(|_| "Failed to format component data".to_string()))
} else {
Err("No result or error in response".to_string())
}
}
/// Parse BRP query response into our RemoteEntity format
fn parse_brp_entities(result: serde_json::Value) -> Result<Vec<RemoteEntity>, String> {
let mut entities = Vec::new();
if let Some(entity_array) = result.as_array() {
for entity_obj in entity_array {
if let Some(entity_data) = entity_obj.as_object() {
// Get entity ID
let entity_id = entity_data
.get("entity")
.and_then(|v| v.as_u64())
.ok_or("Missing or invalid entity ID")?;
// Extract component names
let mut components = Vec::new();
let mut full_component_names = Vec::new();
if let Some(components_obj) = entity_data.get("components").and_then(|v| v.as_object()) {
for component_name in components_obj.keys() {
// Store the full name for API calls
full_component_names.push(component_name.clone());
// Clean up component names (remove module paths for readability)
let clean_name = component_name
.split("::")
.last()
.unwrap_or(component_name)
.to_string();
components.push(clean_name);
}
}
entities.push(RemoteEntity {
id: entity_id as u32,
components,
full_component_names,
});
}
}
} else {
return Err("Expected array of entities in response".to_string());
}
Ok(entities)
}
}
/// Refresh the entity list display
fn refresh_entity_list(
editor_state: Res<EditorState>,
@ -799,7 +658,7 @@ fn handle_expansion_keyboard(
if let Some(selected_entity_id) = editor_state.selected_entity_id {
if let Some(selected_entity) = editor_state.entities.iter().find(|e| e.id == selected_entity_id) {
if !selected_entity.full_component_names.is_empty() {
match remote_client::try_fetch_component_data_with_names(
match client::try_fetch_component_data_with_names(
&remote_conn.base_url,
selected_entity_id,
selected_entity.full_component_names.clone()
@ -828,432 +687,6 @@ fn handle_expansion_keyboard(
}
}
/// Build component display as interactive widgets instead of just text
fn build_component_widgets(
parent: &mut ChildSpawnerCommands,
entity_id: u32,
components_data: &str,
display_state: &ComponentDisplayState,
) {
// Try to parse the JSON response
if let Ok(json_value) = serde_json::from_str::<Value>(components_data) {
if let Some(components_obj) = json_value.get("components").and_then(|v| v.as_object()) {
// Header
parent.spawn((
Text::new(format!("Entity {} - Components", entity_id)),
TextFont {
font_size: 15.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
Node {
margin: UiRect::bottom(Val::Px(12.0)),
..default()
},
));
for (component_name, component_data) in components_obj {
// Clean component name (remove module path)
let clean_name = component_name.split("::").last().unwrap_or(component_name);
build_component_widget(parent, clean_name, component_data, component_name, display_state);
}
return;
}
}
// Fallback to simple text display if parsing fails
parent.spawn((
Text::new(format!("Entity {} - Component Data\n\n{}", entity_id, components_data)),
TextFont {
font_size: 13.0,
..default()
},
TextColor(Color::srgb(0.8, 0.8, 0.8)),
));
}
/// Extract package name from a full component type string
fn extract_package_name(full_component_name: &str) -> String {
// Handle different patterns:
// bevy_transform::components::Transform -> bevy_transform
// bevy_ui::ui_node::Node -> bevy_ui
// cube::server::SomeComponent -> cube
// std::collections::HashMap -> std
// MyComponent -> MyComponent (no package)
if let Some(first_separator) = full_component_name.find("::") {
let package_part = &full_component_name[..first_separator];
// Handle cases where the first part might be a crate prefix
// like "bevy_core" or just "bevy"
if package_part.contains('_') || package_part.len() <= 12 {
format!("[{}]", package_part)
} else {
// For very long first parts, just take first word
format!("[{}]", package_part.split('_').next().unwrap_or(package_part))
}
} else {
// No package separator, use the component name itself
format!("[{}]", full_component_name.split('<').next().unwrap_or(full_component_name))
}
}
/// Build a single component widget with expansion capabilities
fn build_component_widget(
parent: &mut ChildSpawnerCommands,
clean_name: &str,
component_data: &Value,
full_component_name: &str,
display_state: &ComponentDisplayState,
) {
// Component header container
parent.spawn((
Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
margin: UiRect::bottom(Val::Px(8.0)),
..default()
},
)).with_children(|parent| {
// Component title with package name
let package_name = extract_package_name(full_component_name);
parent.spawn((
Text::new(format!("{} {}", package_name, clean_name)),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::srgb(0.95, 0.95, 0.95)),
Node {
margin: UiRect::bottom(Val::Px(4.0)),
..default()
},
));
// Build component fields
let fields = parse_component_fields(full_component_name, component_data);
for field in fields {
build_field_widget(parent, &field, 1, &format!("{}.{}", clean_name, field.name), display_state);
}
});
}
/// Build a field widget with expansion button if needed
fn build_field_widget(
parent: &mut ChildSpawnerCommands,
field: &ComponentField,
indent_level: usize,
path: &str,
display_state: &ComponentDisplayState,
) {
let indent_px = (indent_level as f32) * 16.0;
parent.spawn((
Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
if field.is_expandable {
let is_expanded = display_state.expanded_paths.contains(path);
// Expansion button
parent.spawn((
Button,
Node {
width: Val::Px(18.0),
height: Val::Px(18.0),
margin: UiRect::right(Val::Px(6.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::srgb(0.3, 0.3, 0.3)),
BorderColor::all(Color::srgb(0.5, 0.5, 0.5)),
ExpansionButton {
path: path.to_string(),
is_expanded,
},
)).with_children(|parent| {
parent.spawn((
Text::new(if is_expanded { "-" } else { "+" }),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
});
// Field name and summary
let value_summary = format_value_inline(&field.value);
parent.spawn((
Text::new(format!("{}: {}", field.name, value_summary)),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.8, 0.8, 0.8)),
));
} else {
// No expansion button for simple values
parent.spawn((
Node {
width: Val::Px(16.0),
height: Val::Px(16.0),
margin: UiRect::right(Val::Px(6.0)),
..default()
},
));
// Simple field display
let formatted_value = format_simple_value(&field.value);
parent.spawn((
Text::new(format!("{}: {}", field.name, formatted_value)),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.8, 0.8, 0.8)),
));
}
});
// Show expanded children if the field is expanded
if field.is_expandable && display_state.expanded_paths.contains(path) {
if matches!(field.value, Value::Object(_)) {
build_expanded_object_widgets(parent, &field.value, indent_level + 1, path, display_state);
} else if matches!(field.value, Value::Array(_)) {
build_expanded_array_widgets(parent, &field.value, indent_level + 1);
}
}
}
/// Build widgets for expanded object fields
fn build_expanded_object_widgets(
parent: &mut ChildSpawnerCommands,
value: &Value,
indent_level: usize,
path: &str,
display_state: &ComponentDisplayState,
) {
let indent_px = (indent_level as f32) * 16.0;
if let Some(obj) = value.as_object() {
// Check for common Bevy types (Vec3, Vec2, Color, etc.)
if let (Some(x), Some(y), Some(z)) = (obj.get("x"), obj.get("y"), obj.get("z")) {
if all_numbers(&[x, y, z]) {
for (component, val) in [("x", x), ("y", y), ("z", z)] {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("{}: {:.3}", component, val.as_f64().unwrap_or(0.0))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.7, 0.9, 0.7)),
));
});
}
if let Some(w) = obj.get("w") {
if w.is_number() {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("w: {:.3}", w.as_f64().unwrap_or(0.0))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.7, 0.9, 0.7)),
));
});
}
}
return;
}
} else if let (Some(x), Some(y)) = (obj.get("x"), obj.get("y")) {
if all_numbers(&[x, y]) {
for (component, val) in [("x", x), ("y", y)] {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("{}: {:.3}", component, val.as_f64().unwrap_or(0.0))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.7, 0.9, 0.7)),
));
});
}
return;
}
}
// Generic object handling
for (key, val) in obj {
let child_path = format!("{}.{}", path, key);
let is_simple = is_simple_value(val);
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
if is_simple {
parent.spawn((
Text::new(format!("{}: {}", key, format_simple_value(val))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.7, 0.7, 0.9)),
));
} else {
let is_expanded = display_state.expanded_paths.contains(&child_path);
parent.spawn((
Text::new(format!("{}{}: {}",
if is_expanded { "[-] " } else { "[+] " },
key,
format_value_inline(val)
)),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.8, 0.8, 0.8)),
));
}
});
}
}
}
/// Build widgets for expanded array fields
fn build_expanded_array_widgets(
parent: &mut ChildSpawnerCommands,
value: &Value,
indent_level: usize,
) {
let indent_px = (indent_level as f32) * 16.0;
if let Some(arr) = value.as_array() {
if arr.len() <= 4 && arr.iter().all(|v| v.is_number()) {
// Small numeric arrays (Vec2, Vec3, Vec4, Quat components)
for (i, item) in arr.iter().enumerate() {
let comp_name = match i {
0 => "x", 1 => "y", 2 => "z", 3 => "w",
_ => &format!("[{}]", i),
};
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("{}: {:.3}", comp_name, item.as_f64().unwrap_or(0.0))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.7, 0.9, 0.7)),
));
});
}
} else if arr.len() <= 10 {
// Small arrays - show all items
for (i, item) in arr.iter().enumerate() {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("[{}]: {}", i, format_simple_value(item))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.9, 0.7, 0.7)),
));
});
}
} else {
// Large arrays - show first few items
for (i, item) in arr.iter().take(3).enumerate() {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("[{}]: {}", i, format_simple_value(item))),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.9, 0.7, 0.7)),
));
});
}
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
margin: UiRect::left(Val::Px(indent_px)),
padding: UiRect::all(Val::Px(2.0)),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(format!("... ({} more items)", arr.len() - 3)),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::srgb(0.6, 0.6, 0.6)),
));
});
}
}
}
/// System to setup marker components on ScrollContent areas
/// This ensures the existing entity list and component inspector systems can find their target areas
fn setup_scroll_content_markers(

View File

@ -1,33 +0,0 @@
use bevy::prelude::*;
use serde::Deserialize;
/// Represents a component's data received from the remote server.
#[derive(Clone, Debug, Deserialize)]
pub struct ComponentData {
pub type_name: String,
// Using serde_json::Value to hold arbitrary component data
pub data: serde_json::Value,
}
/// Represents an entity and its components from the remote server.
#[derive(Clone, Debug, Deserialize)]
pub struct EntityData {
pub entity: Entity,
pub components: Vec<ComponentData>,
}
/// Events that are sent to the inspector UI to notify it of changes.
#[derive(Event)]
pub enum InspectorEvent {
/// Sent when new entities are detected.
EntitiesAdded(Vec<EntityData>),
/// Sent when entities are removed.
EntitiesRemoved(Vec<Entity>),
/// Sent when components of an entity are changed.
ComponentsChanged {
entity: Entity,
new_components: Vec<ComponentData>,
},
}
impl bevy::ecs::event::BufferedEvent for InspectorEvent {}

View File

@ -1,6 +0,0 @@
pub mod events;
pub mod plugin;
pub mod remote;
pub mod selection;
pub mod tree;
pub mod ui;

View File

@ -1,35 +0,0 @@
use bevy::prelude::*;
use super::{
events::InspectorEvent,
remote::{RemoteEntities, poll_remote_entities, fetch_entity_components},
selection::{reset_selected_entity_if_entity_despawned, SelectedEntity},
tree::{TreeNodeInteraction, TreeState},
ui::{setup_inspector, update_entity_tree, handle_tree_interactions, update_component_details},
};
/// A plugin that provides an inspector UI.
pub struct InspectorPlugin;
impl Plugin for InspectorPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SelectedEntity>()
.register_type::<SelectedEntity>()
.init_resource::<RemoteEntities>()
.init_resource::<TreeState>()
.add_event::<InspectorEvent>()
.add_event::<TreeNodeInteraction>()
.add_systems(Startup, setup_inspector)
.add_systems(
Update,
(
poll_remote_entities,
fetch_entity_components,
update_entity_tree,
handle_tree_interactions,
update_component_details,
reset_selected_entity_if_entity_despawned,
),
);
}
}

View File

@ -1,93 +0,0 @@
use bevy::prelude::*;
use std::collections::HashMap;
use crate::inspector::events::{ComponentData, EntityData, InspectorEvent};
/// Resource for tracking remote entities and their polling state
#[derive(Resource, Default)]
pub struct RemoteEntities {
pub entities: HashMap<Entity, EntityData>,
pub has_polled: bool,
}
/// System that polls the remote application for entities
pub fn poll_remote_entities(
mut remote_entities: ResMut<RemoteEntities>,
mut inspector_events: EventWriter<InspectorEvent>,
) {
// Only poll once for this demo
if !remote_entities.has_polled {
remote_entities.has_polled = true;
// Mock some entities for demonstration
let mock_entities = vec![
EntityData {
entity: Entity::from_bits(0),
components: vec![
ComponentData {
type_name: "Transform".to_string(),
data: serde_json::json!({
"translation": [0.0, 0.0, 0.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
}),
},
ComponentData {
type_name: "Mesh".to_string(),
data: serde_json::json!({ "handle": "cube" }),
},
],
},
EntityData {
entity: Entity::from_bits(1),
components: vec![
ComponentData {
type_name: "Camera".to_string(),
data: serde_json::json!({ "fov": 45.0 }),
},
ComponentData {
type_name: "Transform".to_string(),
data: serde_json::json!({
"translation": [0.0, 0.0, 5.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
}),
},
],
},
];
// Add to local cache
for entity_data in &mock_entities {
remote_entities.entities.insert(entity_data.entity, entity_data.clone());
}
// Send event with new entities
inspector_events.trigger(InspectorEvent::EntitiesAdded(mock_entities));
}
}
/// System that fetches component data for selected entities
pub fn fetch_entity_components(
selected_entity: Res<crate::inspector::selection::SelectedEntity>,
mut inspector_events: EventWriter<InspectorEvent>,
) {
if let Some(entity) = selected_entity.0 {
// Mock component data for demonstration
let components = vec![
ComponentData {
type_name: "Transform".to_string(),
data: serde_json::json!({
"translation": [0.0, 0.0, 0.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
}),
},
];
inspector_events.trigger(InspectorEvent::ComponentsChanged {
entity,
new_components: components,
});
}
}

View File

@ -1,18 +0,0 @@
use bevy::{prelude::*, ecs::entity::Entities};
/// The currently selected entity in the editor.
#[derive(Resource, Default, Reflect)]
#[reflect(Resource, Default)]
pub struct SelectedEntity(pub Option<Entity>);
/// System to reset [`SelectedEntity`] when the entity is despawned.
pub fn reset_selected_entity_if_entity_despawned(
mut selected_entity: ResMut<SelectedEntity>,
entities: &Entities,
) {
if let Some(e) = selected_entity.0 {
if !entities.contains(e) {
selected_entity.0 = None;
}
}
}

View File

@ -1,16 +0,0 @@
use bevy::prelude::*;
use std::collections::HashSet;
/// The state of the inspector's tree view.
#[derive(Resource, Default)]
pub struct TreeState {
pub expanded_nodes: HashSet<String>,
}
/// An event that signals an interaction with a tree node.
#[derive(Event, Debug)]
pub struct TreeNodeInteraction {
pub node_id: String,
}
impl bevy::ecs::event::BufferedEvent for TreeNodeInteraction {}

View File

@ -1,292 +0,0 @@
use crate::inspector::{
events::*,
selection::SelectedEntity,
tree::{TreeNodeInteraction, TreeState},
};
use bevy::prelude::*;
/// Marker component for the inspector root
#[derive(Component)]
pub struct InspectorRoot;
/// Marker component for the entity tree
#[derive(Component)]
pub struct EntityTree;
/// Marker component for the component details panel
#[derive(Component)]
pub struct ComponentDetails;
/// Set up the main inspector UI
pub fn setup_inspector(mut commands: Commands) {
// Create the main inspector layout
commands
.spawn((
InspectorRoot,
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Row,
..default()
},
))
.with_children(|parent| {
// Left panel: Entity tree
parent
.spawn((
EntityTree,
Node {
width: Val::Px(300.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
border: UiRect::right(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),
BorderColor::all(Color::srgb(0.3, 0.3, 0.3)),
))
.with_children(|parent| {
// Tree header
parent.spawn((
Text::new("Entities"),
TextColor(Color::WHITE),
TextFont {
font_size: 18.0,
..default()
},
Node {
padding: UiRect::all(Val::Px(10.0)),
border: UiRect::bottom(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
BorderColor::all(Color::srgb(0.3, 0.3, 0.3)),
));
});
// Right panel: Component details
parent
.spawn((
ComponentDetails,
Node {
flex_grow: 1.0,
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::srgb(0.12, 0.12, 0.12)),
))
.with_children(|parent| {
parent.spawn((
Text::new("Select an entity to view components"),
TextColor(Color::srgb(0.7, 0.7, 0.7)),
TextFont {
font_size: 14.0,
..default()
},
));
});
});
}
/// System that updates the entity tree when new entities are added
pub fn update_entity_tree(
mut commands: Commands,
mut inspector_events: EventReader<InspectorEvent>,
tree_query: Query<Entity, With<EntityTree>>,
selected_entity: Res<SelectedEntity>,
tree_state: Res<TreeState>,
) {
for event in inspector_events.read() {
match event {
InspectorEvent::EntitiesAdded(entities) => {
if let Ok(tree_entity) = tree_query.single() {
// Clear existing children (except header)
if let Ok(mut entity_commands) = commands.get_entity(tree_entity) {
entity_commands.with_children(|parent| {
// Keep the header, add entity list
for entity_data in entities {
spawn_entity_row(parent, &entity_data, &selected_entity, &tree_state);
}
});
}
}
}
_ => {}
}
}
}
/// System that handles tree node interactions
pub fn handle_tree_interactions(
mut commands: Commands,
mut interaction_events: EventReader<TreeNodeInteraction>,
mut selected_entity: ResMut<SelectedEntity>,
details_query: Query<Entity, With<ComponentDetails>>,
) {
for event in interaction_events.read() {
// Update selected entity
selected_entity.0 = Some(Entity::from_bits(event.node_id.parse::<u64>().unwrap_or(0)));
// Update component details panel
if let Ok(details_entity) = details_query.single() {
if let Ok(mut entity_commands) = commands.get_entity(details_entity) {
entity_commands.despawn_descendants();
entity_commands.with_children(|parent| {
parent.spawn((
Text::new(format!("Entity: {}", event.node_id)),
TextColor(Color::WHITE),
TextFont {
font_size: 16.0,
..default()
},
Node {
margin: UiRect::bottom(Val::Px(10.0)),
..default()
},
));
parent.spawn((
Text::new("Loading components..."),
TextColor(Color::srgb(0.7, 0.7, 0.7)),
TextFont {
font_size: 12.0,
..default()
},
));
});
}
}
}
}
/// System that updates component details when components change
pub fn update_component_details(
mut commands: Commands,
mut inspector_events: EventReader<InspectorEvent>,
details_query: Query<Entity, With<ComponentDetails>>,
selected_entity: Res<SelectedEntity>,
) {
for event in inspector_events.read() {
if let InspectorEvent::ComponentsChanged { entity, new_components } = event {
// Only update if this is the selected entity
if let Some(selected) = selected_entity.0 {
if selected == *entity {
if let Ok(details_entity) = details_query.single() {
if let Ok(mut entity_commands) = commands.get_entity(details_entity) {
entity_commands.despawn_descendants();
entity_commands.with_children(|parent| {
parent.spawn((
Text::new(format!("Entity: {:?}", entity)),
TextColor(Color::WHITE),
TextFont {
font_size: 16.0,
..default()
},
Node {
margin: UiRect::bottom(Val::Px(15.0)),
..default()
},
));
for component in new_components {
spawn_component_details(parent, component);
}
});
}
}
}
}
}
}
}
fn spawn_entity_row(
parent: &mut ChildBuilder,
entity_data: &EntityData,
selected_entity: &SelectedEntity,
_tree_state: &TreeState,
) {
let is_selected = selected_entity.0 == Some(entity_data.entity);
// Convert entity to index for display
let entity_index = entity_data.entity.index();
parent
.spawn((
Button,
Node {
width: Val::Percent(100.0),
padding: UiRect::all(Val::Px(8.0)),
margin: UiRect::vertical(Val::Px(1.0)),
align_items: AlignItems::Center,
..default()
},
BackgroundColor(if is_selected {
Color::srgb(0.3, 0.4, 0.6)
} else {
Color::srgb(0.18, 0.18, 0.18)
}),
))
.with_children(|parent| {
parent.spawn((
Text::new(format!("Entity {}", entity_index)),
TextColor(Color::WHITE),
TextFont {
font_size: 12.0,
..default()
},
));
})
.observe(move |_trigger: On<Pointer<Click>>, mut events: EventWriter<TreeNodeInteraction>| {
events.trigger(TreeNodeInteraction {
node_id: entity_index.to_string(),
});
});
}
/// Spawn component details UI
fn spawn_component_details(parent: &mut ChildBuilder, component: &ComponentData) {
parent
.spawn((
Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
margin: UiRect::bottom(Val::Px(10.0)),
padding: UiRect::all(Val::Px(8.0)),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::srgb(0.18, 0.18, 0.18)),
BorderColor::all(Color::srgb(0.3, 0.3, 0.3)),
))
.with_children(|parent| {
// Component type name
parent.spawn((
Text::new(&component.type_name),
TextColor(Color::srgb(0.4, 0.8, 0.4)),
TextFont {
font_size: 14.0,
..default()
},
Node {
margin: UiRect::bottom(Val::Px(5.0)),
..default()
},
));
// Component data (simplified JSON display)
let data_text = serde_json::to_string_pretty(&component.data)
.unwrap_or_else(|_| "Invalid JSON".to_string());
parent.spawn((
Text::new(data_text),
TextColor(Color::srgb(0.8, 0.8, 0.8)),
TextFont {
font_size: 10.0,
..default()
},
));
});
}

View File

@ -6,7 +6,6 @@ use crate::{
themes::DarkTheme,
remote::types::{EditorState, RemoteEntity},
widgets::{
simple_scrollable::ScrollableContainerPlugin,
spawn_basic_panel,
EditorTheme,
ListView,
@ -21,7 +20,7 @@ pub struct EntityListPlugin;
impl Plugin for EntityListPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((ScrollableContainerPlugin, ListViewPlugin))
app.add_plugins(ListViewPlugin)
.add_systems(Update, (
handle_entity_selection,
update_entity_button_colors,

View File

@ -10,7 +10,7 @@
//! - **CoreScrollArea**: Low-level scroll component for custom implementations
//! - **ExpansionButton**: Collapsible content with expand/collapse functionality
//! - **BasicPanel**: Simple panel with header and content area
//! - **ScrollableContainer**: Legacy scrollable container (simple implementation)
//! - **ListView**: Generic list widget with selection support
//!
//! ## Integration
//!
@ -34,7 +34,6 @@
//! ```
pub mod expansion_button;
pub mod simple_scrollable;
pub mod simple_panel;
pub mod core_scroll_area;
pub mod scroll_view;
@ -46,7 +45,6 @@ pub mod list_view;
// pub mod theme;
pub use expansion_button::*;
pub use simple_scrollable::ScrollableContainer;
pub use simple_panel::{BasicPanel, spawn_basic_panel};
pub use core_scroll_area::*;
pub use scroll_view::*;

View File

@ -1,133 +0,0 @@
//! # Scroll Widget Examples
//!
//! This module provides comprehensive examples and documentation for the scroll
//! widget system, demonstrating how to use both high-level and low-level scroll
//! components effectively.
//!
//! ## Widget Architecture
//!
//! The scroll system provides two main approaches:
//!
//! 1. **ScrollViewBuilder** - High-level, styled scroll widget (recommended)
//! 2. **CoreScrollArea** - Low-level scroll component for custom implementations
//!
//! Both widgets integrate seamlessly with Bevy's native `bevy_core_widgets`
//! scrolling system and provide smooth mouse wheel interaction.
use bevy::prelude::*;
use crate::widgets::{ScrollViewBuilder, CoreScrollArea, ScrollContent, ScrollToEntityEvent};
/// Example of how to use the new scroll widgets
/// This demonstrates the separation of concerns between CoreScrollArea and ScrollView
pub fn scroll_widget_example(mut commands: Commands) {
// Method 1: Using the high-level ScrollView builder (recommended for most cases)
commands.spawn((
Node {
width: Val::Px(400.0),
height: Val::Px(300.0),
..default()
},
)).with_children(|parent| {
let scroll_view_entity = ScrollViewBuilder::new()
.with_background_color(Color::srgb(0.1, 0.1, 0.1))
.with_border_color(Color::srgb(0.3, 0.3, 0.3))
.with_padding(UiRect::all(Val::Px(16.0)))
.with_corner_radius(8.0)
.with_scroll_sensitivity(25.0)
.with_max_scroll(Vec2::new(0.0, 1000.0))
.spawn(parent);
// Add content to the scroll view - it will automatically find the ScrollContent child
if let Some(mut entity_commands) = commands.get_entity(scroll_view_entity) {
entity_commands.with_children(|parent| {
for i in 0..50 {
parent.spawn((
Text::new(format!("Item {}", i)),
TextFont::default(),
TextColor(Color::WHITE),
Node {
height: Val::Px(30.0),
margin: UiRect::bottom(Val::Px(4.0)),
..default()
},
));
}
});
}
});
// Method 2: Using CoreScrollArea directly (for custom implementations)
commands.spawn((
Node {
width: Val::Px(300.0),
height: Val::Px(200.0),
overflow: Overflow::clip(),
..default()
},
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
CoreScrollArea::new(Vec2::new(0.0, 500.0))
.with_sensitivity(30.0)
.with_vertical(true)
.with_horizontal(false),
)).with_children(|parent| {
parent.spawn((
Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
..default()
},
ScrollContent,
)).with_children(|parent| {
for i in 0..30 {
parent.spawn((
Text::new(format!("Core Scroll Item {}", i)),
TextFont::default(),
TextColor(Color::WHITE),
Node {
height: Val::Px(25.0),
margin: UiRect::bottom(Val::Px(2.0)),
..default()
},
));
}
});
});
}
/// Example of programmatic scrolling using events
pub fn programmatic_scroll_example(
mut scroll_events: EventWriter<ScrollToEntityEvent>,
scroll_areas: Query<Entity, With<CoreScrollArea>>,
content_entities: Query<Entity, With<ScrollContent>>,
) {
// Example: Scroll to a specific entity when some condition is met
if let (Ok(scroll_area), Ok(target_entity)) = (scroll_areas.get_single(), content_entities.get_single()) {
scroll_events.send(ScrollToEntityEvent {
scroll_area_entity: scroll_area,
target_entity,
});
}
}
/// Documentation for the scroll widget architecture
///
/// ## CoreScrollArea
/// - Handles mouse wheel events and scroll offset clamping
/// - Provides "scroll into view" functionality for entities
/// - Does NOT include scrollbars or visual styling
/// - Can be used standalone for custom scroll implementations
///
/// ## ScrollView
/// - High-level, opinionated scroll widget with built-in styling
/// - Includes scrollbars, padding, borders, and rounded corners
/// - Uses CoreScrollArea internally for scroll logic
/// - Recommended for most use cases
///
/// ## Usage Guidelines
/// 1. Use ScrollView for standard scroll areas with visual styling
/// 2. Use CoreScrollArea when you need custom scroll behavior or styling
/// 3. Both widgets automatically handle mouse wheel events within their bounds
/// 4. Content should be placed inside a ScrollContent component
/// 5. Use ScrollToEntityEvent for programmatic scrolling
#[allow(dead_code)]
struct ScrollWidgetDocumentation;

View File

@ -1,222 +0,0 @@
use bevy::prelude::*;
use bevy::input::mouse::MouseWheel;
use bevy::window::Window;
use bevy::ui::{UiRect, Val, FlexDirection, Overflow, ComputedNode};
/// A generic scrollable area widget that can contain any content
/// This widget handles mouse wheel scrolling and maintains scroll position
#[derive(Component, Default)]
pub struct ScrollableArea {
/// Current scroll offset
pub scroll_offset: f32,
/// Maximum scroll distance (calculated dynamically)
pub max_scroll: f32,
/// Scroll sensitivity multiplier
pub scroll_sensitivity: f32,
/// Content height calculation method
pub content_height_calc: ContentHeightCalculation,
}
impl ScrollableArea {
pub fn new() -> Self {
Self {
scroll_offset: 0.0,
max_scroll: 0.0,
scroll_sensitivity: 15.0,
content_height_calc: ContentHeightCalculation::ChildrenHeight(40.0),
}
}
pub fn with_sensitivity(mut self, sensitivity: f32) -> Self {
self.scroll_sensitivity = sensitivity;
self
}
pub fn with_content_calculation(mut self, calc: ContentHeightCalculation) -> Self {
self.content_height_calc = calc;
self
}
}
/// Different methods for calculating content height
pub enum ContentHeightCalculation {
/// Calculate based on number of children times item height
ChildrenHeight(f32),
/// Calculate based on explicit content height
ExplicitHeight(f32),
/// Calculate based on sum of children's actual heights
ActualChildrenHeights,
}
impl Default for ContentHeightCalculation {
fn default() -> Self {
Self::ChildrenHeight(40.0)
}
}
/// Marker component for entities that should receive scroll events
#[derive(Component)]
pub struct ScrollTarget {
/// Bounds of the scrollable area in screen space
pub bounds: Rect,
}
/// Bundle for creating a scrollable area
#[derive(Bundle)]
pub struct ScrollableAreaBundle {
pub scrollable: ScrollableArea,
pub target: ScrollTarget,
pub node: Node,
pub background_color: BackgroundColor,
pub transform: Transform,
pub global_transform: GlobalTransform,
pub visibility: Visibility,
pub inherited_visibility: InheritedVisibility,
pub view_visibility: ViewVisibility,
pub z_index: ZIndex,
}
impl Default for ScrollableAreaBundle {
fn default() -> Self {
Self {
scrollable: ScrollableArea::new(),
target: ScrollTarget {
bounds: Rect::new(0.0, 0.0, 0.0, 0.0),
},
node: Node {
overflow: Overflow::clip_y(),
flex_direction: FlexDirection::Column,
..default()
},
background_color: BackgroundColor(Color::NONE),
transform: Transform::IDENTITY,
global_transform: GlobalTransform::IDENTITY,
visibility: Visibility::Inherited,
inherited_visibility: InheritedVisibility::VISIBLE,
view_visibility: ViewVisibility::HIDDEN,
z_index: ZIndex::default(),
}
}
}
/// Plugin for scrollable area functionality
pub struct ScrollableAreaPlugin;
impl Plugin for ScrollableAreaPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, (
update_scroll_bounds,
handle_scroll_input,
apply_scroll_offset,
).chain());
}
}
/// System to update the bounds of scrollable areas based on their computed layout
fn update_scroll_bounds(
mut scroll_query: Query<(&mut ScrollTarget, &ComputedNode, &GlobalTransform), With<ScrollableArea>>,
windows: Query<&Window>,
) {
let Ok(window) = windows.single() else { return };
let window_height = window.height();
for (mut target, computed_node, transform) in &mut scroll_query {
let translation = transform.translation();
let size = computed_node.size;
// Convert UI coordinates to screen coordinates
target.bounds = Rect::new(
translation.x,
window_height - translation.y - size.y,
translation.x + size.x,
window_height - translation.y,
);
}
}
/// System to handle mouse wheel scroll input for scrollable areas
fn handle_scroll_input(
mut scroll_events: EventReader<MouseWheel>,
mut scroll_query: Query<(&mut ScrollableArea, &ScrollTarget)>,
windows: Query<&Window>,
) {
if scroll_events.is_empty() {
return;
}
let Ok(window) = windows.single() else {
scroll_events.clear();
return;
};
let Some(cursor_position) = window.cursor_position() else {
scroll_events.clear();
return;
};
// Find which scrollable area the cursor is over
if let Some((mut scrollable, _)) = scroll_query
.iter_mut()
.find(|(_, target)| target.bounds.contains(cursor_position))
{
for scroll_event in scroll_events.read() {
let scroll_delta = scroll_event.y * scrollable.scroll_sensitivity;
scrollable.scroll_offset = (scrollable.scroll_offset - scroll_delta)
.clamp(-scrollable.max_scroll, 0.0);
}
}
scroll_events.clear();
}
/// System to apply scroll offset to the content within scrollable areas
fn apply_scroll_offset(
mut scroll_query: Query<(&mut ScrollableArea, &Children), Changed<ScrollableArea>>,
mut content_query: Query<&mut Node>,
children_query: Query<&Children>,
) {
for (mut scrollable, children) in &mut scroll_query {
// Calculate content height based on the chosen method
let content_height = match scrollable.content_height_calc {
ContentHeightCalculation::ChildrenHeight(item_height) => {
children.len() as f32 * item_height
},
ContentHeightCalculation::ExplicitHeight(height) => height,
ContentHeightCalculation::ActualChildrenHeights => {
// This would require walking the entire hierarchy and summing actual heights
// For now, fall back to estimated height
children.len() as f32 * 25.0
},
};
// Update max scroll (assuming container height is calculated elsewhere)
// This is a simplified version - in a real implementation you'd want to
// get the actual container height from the computed layout
let container_height = 400.0; // This should come from the actual container
scrollable.max_scroll = (content_height - container_height).max(0.0);
// Apply scroll offset to the first child (content container)
if let Some(&first_child) = children.first() {
if let Ok(mut node) = content_query.get_mut(first_child) {
node.margin.top = Val::Px(scrollable.scroll_offset);
}
}
}
}
/// Helper function to spawn a scrollable area with content
pub fn spawn_scrollable_area(
commands: &mut Commands,
content_bundle: impl Bundle,
scrollable_config: ScrollableArea,
) -> Entity {
commands
.spawn(ScrollableAreaBundle {
scrollable: scrollable_config,
..default()
})
.with_children(|parent| {
parent.spawn(content_bundle);
})
.id()
}

View File

@ -1,100 +0,0 @@
//! Simple scrollable container widget for bevy_feathers extraction
//!
//! This module provides a basic scrollable container with mouse wheel support.
//! Designed to be extracted to the bevy_feathers UI library.
//!
//! # Features
//! - Mouse wheel scrolling with configurable sensitivity
//! - Automatic overflow handling
//! - Minimal dependencies on core Bevy
//! - Plugin-based architecture
//!
//! # Usage
//! ```rust,no_run
//! use bevy::prelude::*;
//! use bevy_editor::widgets::ScrollableContainer;
//! use bevy_editor::widgets::simple_scrollable::ScrollableContainerPlugin;
//!
//! fn main() {
//! App::new()
//! .add_plugins(DefaultPlugins)
//! .add_plugins(ScrollableContainerPlugin)
//! .add_systems(Startup, setup)
//! .run();
//! }
//!
//! fn setup(mut commands: Commands) {
//! commands.spawn((
//! Node {
//! width: Val::Percent(100.0),
//! height: Val::Px(300.0),
//! overflow: Overflow::clip(),
//! ..default()
//! },
//! ScrollableContainer {
//! scroll_offset: 0.0,
//! max_scroll: 1000.0,
//! scroll_sensitivity: 10.0,
//! },
//! ));
//! }
//! ```
use bevy::prelude::*;
/// Basic scrollable container widget
#[derive(Component, Default)]
pub struct ScrollableContainer {
pub scroll_offset: f32,
pub max_scroll: f32,
pub scroll_sensitivity: f32,
}
impl ScrollableContainer {
pub fn new() -> Self {
Self {
scroll_offset: 0.0,
max_scroll: 0.0,
scroll_sensitivity: 15.0,
}
}
}
/// Plugin for scrollable container functionality
pub struct ScrollableContainerPlugin;
impl Plugin for ScrollableContainerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, handle_scroll_input);
}
}
/// Basic scroll handling system
fn handle_scroll_input(
mut scroll_events: EventReader<bevy::input::mouse::MouseWheel>,
mut scrollable_query: Query<&mut ScrollableContainer>,
) {
for scroll_event in scroll_events.read() {
// Simple scroll handling - apply to all scrollable containers for now
for mut scrollable in &mut scrollable_query {
let scroll_delta = scroll_event.y * scrollable.scroll_sensitivity;
scrollable.scroll_offset = (scrollable.scroll_offset - scroll_delta)
.clamp(-scrollable.max_scroll, 0.0);
}
}
}
/// Helper function to spawn a basic scrollable container
pub fn spawn_scrollable_container(commands: &mut Commands) -> Entity {
commands
.spawn((
ScrollableContainer::new(),
Node {
overflow: bevy::ui::Overflow::clip_y(),
flex_direction: bevy::ui::FlexDirection::Column,
..default()
},
BackgroundColor(Color::NONE),
))
.id()
}