diff --git a/crates/bevy_editor/src/editor.rs b/crates/bevy_editor/src/editor.rs index 3f182a4edc..2feb12193a 100644 --- a/crates/bevy_editor/src/editor.rs +++ b/crates/bevy_editor/src/editor.rs @@ -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, 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) -> Result { - // 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, 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, @@ -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::(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( diff --git a/crates/bevy_editor/src/inspector/events.rs b/crates/bevy_editor/src/inspector/events.rs deleted file mode 100644 index 3f6e110027..0000000000 --- a/crates/bevy_editor/src/inspector/events.rs +++ /dev/null @@ -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, -} - -/// 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), - /// Sent when entities are removed. - EntitiesRemoved(Vec), - /// Sent when components of an entity are changed. - ComponentsChanged { - entity: Entity, - new_components: Vec, - }, -} - -impl bevy::ecs::event::BufferedEvent for InspectorEvent {} diff --git a/crates/bevy_editor/src/inspector/mod.rs b/crates/bevy_editor/src/inspector/mod.rs deleted file mode 100644 index 743e0d8ca7..0000000000 --- a/crates/bevy_editor/src/inspector/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod events; -pub mod plugin; -pub mod remote; -pub mod selection; -pub mod tree; -pub mod ui; diff --git a/crates/bevy_editor/src/inspector/plugin.rs b/crates/bevy_editor/src/inspector/plugin.rs deleted file mode 100644 index e69df5829e..0000000000 --- a/crates/bevy_editor/src/inspector/plugin.rs +++ /dev/null @@ -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::() - .register_type::() - .init_resource::() - .init_resource::() - .add_event::() - .add_event::() - .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, - ), - ); - } -} diff --git a/crates/bevy_editor/src/inspector/remote.rs b/crates/bevy_editor/src/inspector/remote.rs deleted file mode 100644 index 2bd398206b..0000000000 --- a/crates/bevy_editor/src/inspector/remote.rs +++ /dev/null @@ -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, - pub has_polled: bool, -} - -/// System that polls the remote application for entities -pub fn poll_remote_entities( - mut remote_entities: ResMut, - mut inspector_events: EventWriter, -) { - // 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, - mut inspector_events: EventWriter, -) { - 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, - }); - } -} diff --git a/crates/bevy_editor/src/inspector/selection.rs b/crates/bevy_editor/src/inspector/selection.rs deleted file mode 100644 index 42673cebc7..0000000000 --- a/crates/bevy_editor/src/inspector/selection.rs +++ /dev/null @@ -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); - -/// System to reset [`SelectedEntity`] when the entity is despawned. -pub fn reset_selected_entity_if_entity_despawned( - mut selected_entity: ResMut, - entities: &Entities, -) { - if let Some(e) = selected_entity.0 { - if !entities.contains(e) { - selected_entity.0 = None; - } - } -} diff --git a/crates/bevy_editor/src/inspector/tree.rs b/crates/bevy_editor/src/inspector/tree.rs deleted file mode 100644 index 43dc511d87..0000000000 --- a/crates/bevy_editor/src/inspector/tree.rs +++ /dev/null @@ -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, -} - -/// 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 {} diff --git a/crates/bevy_editor/src/inspector/ui.rs b/crates/bevy_editor/src/inspector/ui.rs deleted file mode 100644 index ac9f057b8b..0000000000 --- a/crates/bevy_editor/src/inspector/ui.rs +++ /dev/null @@ -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, - tree_query: Query>, - selected_entity: Res, - tree_state: Res, -) { - 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, - mut selected_entity: ResMut, - details_query: Query>, -) { - for event in interaction_events.read() { - // Update selected entity - selected_entity.0 = Some(Entity::from_bits(event.node_id.parse::().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, - details_query: Query>, - selected_entity: Res, -) { - 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>, mut events: EventWriter| { - 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() - }, - )); - }); -} diff --git a/crates/bevy_editor/src/panels/entity_list.rs b/crates/bevy_editor/src/panels/entity_list.rs index 263ebaed3e..af94a32b1f 100644 --- a/crates/bevy_editor/src/panels/entity_list.rs +++ b/crates/bevy_editor/src/panels/entity_list.rs @@ -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, diff --git a/crates/bevy_editor/src/widgets/mod.rs b/crates/bevy_editor/src/widgets/mod.rs index 68eea1044b..9febe0200a 100644 --- a/crates/bevy_editor/src/widgets/mod.rs +++ b/crates/bevy_editor/src/widgets/mod.rs @@ -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::*; diff --git a/crates/bevy_editor/src/widgets/scroll_examples.rs b/crates/bevy_editor/src/widgets/scroll_examples.rs deleted file mode 100644 index d62579ef8c..0000000000 --- a/crates/bevy_editor/src/widgets/scroll_examples.rs +++ /dev/null @@ -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, - scroll_areas: Query>, - content_entities: Query>, -) { - // 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; diff --git a/crates/bevy_editor/src/widgets/scrollable_area.rs b/crates/bevy_editor/src/widgets/scrollable_area.rs deleted file mode 100644 index 9fa6807bc2..0000000000 --- a/crates/bevy_editor/src/widgets/scrollable_area.rs +++ /dev/null @@ -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>, - 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, - 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>, - 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() -} diff --git a/crates/bevy_editor/src/widgets/simple_scrollable.rs b/crates/bevy_editor/src/widgets/simple_scrollable.rs deleted file mode 100644 index f09b590702..0000000000 --- a/crates/bevy_editor/src/widgets/simple_scrollable.rs +++ /dev/null @@ -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, - 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() -}