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:
parent
c683f3c4a4
commit
9e889396db
@ -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(
|
||||
|
@ -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 {}
|
@ -1,6 +0,0 @@
|
||||
pub mod events;
|
||||
pub mod plugin;
|
||||
pub mod remote;
|
||||
pub mod selection;
|
||||
pub mod tree;
|
||||
pub mod ui;
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
@ -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,
|
||||
|
@ -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::*;
|
||||
|
@ -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;
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user