diff --git a/Cargo.toml b/Cargo.toml index ec49f284..8e2cc5b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ unsafe_op_in_unsafe_fn = "warn" unused_qualifications = "warn" [workspace.dependencies] -bevy = { git = "https://github.com/bevyengine/bevy.git", rev = "89d094e50f10fc56ec3c4b046c830c650f9f09d5", features = ["wayland"] } +bevy = "0.15.0" bevy_derive = { git = "https://github.com/bevyengine/bevy.git", rev = "89d094e50f10fc56ec3c4b046c830c650f9f09d5" } thiserror = "1" serde = { version = "1", features = ["derive"] } diff --git a/bevy_editor_panes/bevy_2d_viewport/src/lib.rs b/bevy_editor_panes/bevy_2d_viewport/src/lib.rs index 0da438e3..dd397953 100644 --- a/bevy_editor_panes/bevy_2d_viewport/src/lib.rs +++ b/bevy_editor_panes/bevy_2d_viewport/src/lib.rs @@ -1,5 +1,9 @@ //! 2d Viewport for Bevy use bevy::{ + ecs::system::{ + lifetimeless::{SCommands, SRes, SResMut}, + StaticSystemParam, + }, prelude::*, render::{ camera::RenderTarget, @@ -11,28 +15,28 @@ use bevy::{ use bevy_editor_camera::{EditorCamera2d, EditorCamera2dPlugin}; use bevy_editor_styles::Theme; use bevy_infinite_grid::{InfiniteGrid, InfiniteGridPlugin, InfiniteGridSettings}; -use bevy_pane_layout::prelude::*; +use bevy_pane_layout::{pane::Pane, prelude::*}; /// The identifier for the 2D Viewport. /// This is present on any pane that is a 2D Viewport. #[derive(Component)] -pub struct Bevy2dViewport { +pub struct Bevy2dViewportPane { camera_id: Entity, } -impl Default for Bevy2dViewport { +impl Default for Bevy2dViewportPane { fn default() -> Self { - Bevy2dViewport { + Bevy2dViewportPane { camera_id: Entity::PLACEHOLDER, } } } -/// Plugin for the 2D Viewport pane. -pub struct Viewport2dPanePlugin; +impl Pane for Bevy2dViewportPane { + const NAME: &str = "Viewport 2D"; + const ID: &str = "viewport_2d"; -impl Plugin for Viewport2dPanePlugin { - fn build(&self, app: &mut App) { + fn build(app: &mut App) { if !app.is_plugin_added::() { app.add_plugins(InfiniteGridPlugin); } @@ -43,17 +47,75 @@ impl Plugin for Viewport2dPanePlugin { update_render_target_size.after(ui_layout_system), ) .add_observer( - |trigger: Trigger, + |trigger: Trigger, mut commands: Commands, - query: Query<&Bevy2dViewport>| { + query: Query<&Bevy2dViewportPane>| { // Despawn the viewport camera commands .entity(query.get(trigger.entity()).unwrap().camera_id) .despawn_recursive(); }, ); + } - app.register_pane("Viewport 2D", on_pane_creation); + type Param = (SCommands, SResMut>, SRes); + fn on_create(structure: In, param: StaticSystemParam) { + let (mut commands, mut images, theme) = param.into_inner(); + let mut image = Image::default(); + + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + image.texture_descriptor.format = TextureFormat::Bgra8UnormSrgb; + + let image_handle = images.add(image); + + let image_id = commands + .spawn(( + ImageNode::new(image_handle.clone()), + Node { + position_type: PositionType::Absolute, + top: Val::ZERO, + bottom: Val::ZERO, + left: Val::ZERO, + right: Val::ZERO, + ..default() + }, + )) + .set_parent(structure.root) + .id(); + + let camera_id = commands + .spawn(( + Camera2d, + EditorCamera2d { + enabled: false, + ..default() + }, + Camera { + target: RenderTarget::Image(image_handle), + clear_color: ClearColorConfig::Custom(theme.viewport.background_color), + ..default() + }, + RenderLayers::from_layers(&[0, 2]), + )) + .id(); + + commands + .entity(image_id) + .observe( + move |_trigger: Trigger>, mut query: Query<&mut EditorCamera2d>| { + let mut editor_camera = query.get_mut(camera_id).unwrap(); + editor_camera.enabled = true; + }, + ) + .observe( + move |_trigger: Trigger>, mut query: Query<&mut EditorCamera2d>| { + query.get_mut(camera_id).unwrap().enabled = false; + }, + ); + + commands + .entity(structure.root) + .insert(Bevy2dViewportPane { camera_id }); } } @@ -74,87 +136,17 @@ fn setup(mut commands: Commands, theme: Res) { )); } -fn on_pane_creation( - structure: In, - mut commands: Commands, - mut images: ResMut>, - theme: Res, -) { - let mut image = Image::default(); - - image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; - image.texture_descriptor.format = TextureFormat::Bgra8UnormSrgb; - - let image_handle = images.add(image); - - let image_id = commands - .spawn(( - ImageNode::new(image_handle.clone()), - Node { - position_type: PositionType::Absolute, - top: Val::ZERO, - bottom: Val::ZERO, - left: Val::ZERO, - right: Val::ZERO, - ..default() - }, - )) - .set_parent(structure.content) - .id(); - - let camera_id = commands - .spawn(( - Camera2d, - EditorCamera2d { - enabled: false, - ..default() - }, - Camera { - target: RenderTarget::Image(image_handle), - clear_color: ClearColorConfig::Custom(theme.viewport.background_color), - ..default() - }, - RenderLayers::from_layers(&[0, 2]), - )) - .id(); - - commands - .entity(image_id) - .observe( - move |_trigger: Trigger>, mut query: Query<&mut EditorCamera2d>| { - let mut editor_camera = query.get_mut(camera_id).unwrap(); - editor_camera.enabled = true; - }, - ) - .observe( - move |_trigger: Trigger>, mut query: Query<&mut EditorCamera2d>| { - query.get_mut(camera_id).unwrap().enabled = false; - }, - ); - - commands - .entity(structure.root) - .insert(Bevy2dViewport { camera_id }); -} - fn update_render_target_size( - query: Query<(Entity, &Bevy2dViewport)>, + query: Query<(Entity, &Bevy2dViewportPane)>, mut camera_query: Query<(&Camera, &mut EditorCamera2d)>, - content: Query<&PaneContentNode>, - children_query: Query<&Children>, pos_query: Query< (&ComputedNode, &GlobalTransform), Or<(Changed, Changed)>, >, mut images: ResMut>, ) { - for (pane_root, viewport) in &query { - let content_node_id = children_query - .iter_descendants(pane_root) - .find(|e| content.contains(*e)) - .unwrap(); - - let Ok((computed_node, global_transform)) = pos_query.get(content_node_id) else { + for (pane_root_id, viewport) in &query { + let Ok((computed_node, global_transform)) = pos_query.get(pane_root_id) else { continue; }; // TODO Convert to physical pixels diff --git a/bevy_editor_panes/bevy_3d_viewport/src/lib.rs b/bevy_editor_panes/bevy_3d_viewport/src/lib.rs index 94036acd..34b48dd8 100644 --- a/bevy_editor_panes/bevy_3d_viewport/src/lib.rs +++ b/bevy_editor_panes/bevy_3d_viewport/src/lib.rs @@ -1,5 +1,9 @@ //! 3D Viewport for Bevy use bevy::{ + ecs::system::{ + lifetimeless::{SCommands, SRes, SResMut}, + StaticSystemParam, + }, picking::{ pointer::{Location, PointerId, PointerInput, PointerLocation}, PickSet, @@ -15,31 +19,32 @@ use bevy::{ use bevy_editor_cam::prelude::{DefaultEditorCamPlugins, EditorCam}; use bevy_editor_styles::Theme; use bevy_infinite_grid::{InfiniteGrid, InfiniteGridPlugin, InfiniteGridSettings}; -use bevy_pane_layout::prelude::*; +use bevy_pane_layout::{pane::Pane, prelude::*}; /// The identifier for the 3D Viewport. /// This is present on any pane that is a 3D Viewport. #[derive(Component)] -pub struct Bevy3dViewport { +pub struct Bevy3dViewportPane { camera_id: Entity, } -impl Default for Bevy3dViewport { +impl Default for Bevy3dViewportPane { fn default() -> Self { - Bevy3dViewport { + Bevy3dViewportPane { camera_id: Entity::PLACEHOLDER, } } } -/// Plugin for the 3D Viewport pane. -pub struct Viewport3dPanePlugin; +impl Pane for Bevy3dViewportPane { + const NAME: &str = "3d Viewport"; + const ID: &str = "3d_viewport"; -impl Plugin for Viewport3dPanePlugin { - fn build(&self, app: &mut App) { + fn build(app: &mut App) { if !app.is_plugin_added::() { app.add_plugins(InfiniteGridPlugin); } + app.add_plugins(DefaultEditorCamPlugins) .add_systems(Startup, setup) .add_systems( @@ -51,17 +56,65 @@ impl Plugin for Viewport3dPanePlugin { update_render_target_size.after(ui_layout_system), ) .add_observer( - |trigger: Trigger, + |trigger: Trigger, mut commands: Commands, - query: Query<&Bevy3dViewport>| { + query: Query<&Bevy3dViewportPane>| { // Despawn the viewport camera commands .entity(query.get(trigger.entity()).unwrap().camera_id) .despawn_recursive(); }, ); + } + + type Param = (SCommands, SResMut>, SRes); + fn on_create(structure: In, param: StaticSystemParam) { + let (mut commands, mut images, theme) = param.into_inner(); + + let mut image = Image::default(); - app.register_pane("Viewport 3D", on_pane_creation); + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + image.texture_descriptor.format = TextureFormat::Bgra8UnormSrgb; + + let image_handle = images.add(image); + + commands + .spawn(( + ImageNode::new(image_handle.clone()), + Node { + position_type: PositionType::Absolute, + top: Val::ZERO, + bottom: Val::ZERO, + left: Val::ZERO, + right: Val::ZERO, + ..default() + }, + )) + .set_parent(structure.root) + .observe(|trigger: Trigger>, mut commands: Commands| { + commands.entity(trigger.entity()).insert(Active); + }) + .observe(|trigger: Trigger>, mut commands: Commands| { + commands.entity(trigger.entity()).remove::(); + }); + + let camera_id = commands + .spawn(( + Camera3d::default(), + Camera { + target: RenderTarget::Image(image_handle), + clear_color: ClearColorConfig::Custom(theme.viewport.background_color), + ..default() + }, + EditorCam::default(), + Transform::from_translation(Vec3::ONE * 5.).looking_at(Vec3::ZERO, Vec3::Y), + RenderLayers::from_layers(&[0, 1]), + )) + .id(); + + commands + .entity(structure.root) + .insert(Bevy3dViewportPane { camera_id }); } } @@ -72,8 +125,7 @@ struct Active; /// Copies picking events and moves pointers through render-targets. fn render_target_picking_passthrough( mut commands: Commands, - viewports: Query<(Entity, &Bevy3dViewport)>, - content: Query<&PaneContentNode>, + viewports: Query<(Entity, &Bevy3dViewportPane)>, children_query: Query<&Children>, node_query: Query<(&ComputedNode, &GlobalTransform, &ImageNode), With>, mut pointers: Query<(&PointerId, &mut PointerLocation)>, @@ -84,13 +136,8 @@ fn render_target_picking_passthrough( if !matches!(event.location.target, NormalizedRenderTarget::Window(..)) { continue; } - for (pane_root, _viewport) in &viewports { - let content_node_id = children_query - .iter_descendants(pane_root) - .find(|e| content.contains(*e)) - .unwrap(); - - let image_id = children_query.get(content_node_id).unwrap()[0]; + for (pane_root_id, _viewport) in &viewports { + let image_id = children_query.get(pane_root_id).unwrap()[0]; let Ok((computed_node, global_transform, ui_image)) = node_query.get(image_id) else { // Inactive viewport @@ -136,73 +183,14 @@ fn setup(mut commands: Commands, theme: Res) { )); } -fn on_pane_creation( - structure: In, - mut commands: Commands, - mut images: ResMut>, - theme: Res, -) { - let mut image = Image::default(); - - image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; - image.texture_descriptor.format = TextureFormat::Bgra8UnormSrgb; - - let image_handle = images.add(image); - - commands - .spawn(( - ImageNode::new(image_handle.clone()), - Node { - position_type: PositionType::Absolute, - top: Val::ZERO, - bottom: Val::ZERO, - left: Val::ZERO, - right: Val::ZERO, - ..default() - }, - )) - .set_parent(structure.content) - .observe(|trigger: Trigger>, mut commands: Commands| { - commands.entity(trigger.entity()).insert(Active); - }) - .observe(|trigger: Trigger>, mut commands: Commands| { - commands.entity(trigger.entity()).remove::(); - }); - - let camera_id = commands - .spawn(( - Camera3d::default(), - Camera { - target: RenderTarget::Image(image_handle), - clear_color: ClearColorConfig::Custom(theme.viewport.background_color), - ..default() - }, - EditorCam::default(), - Transform::from_translation(Vec3::ONE * 5.).looking_at(Vec3::ZERO, Vec3::Y), - RenderLayers::from_layers(&[0, 1]), - )) - .id(); - - commands - .entity(structure.root) - .insert(Bevy3dViewport { camera_id }); -} - fn update_render_target_size( - query: Query<(Entity, &Bevy3dViewport)>, + query: Query<(Entity, &Bevy3dViewportPane)>, mut camera_query: Query<&Camera>, - content: Query<&PaneContentNode>, - children_query: Query<&Children>, computed_node_query: Query<&ComputedNode, Changed>, mut images: ResMut>, ) { - for (pane_root, viewport) in &query { - let content_node_id = children_query - .iter_descendants(pane_root) - .find(|e| content.contains(*e)) - .unwrap(); - - let Ok(computed_node) = computed_node_query.get(content_node_id) else { + for (pane_root_id, viewport) in &query { + let Ok(computed_node) = computed_node_query.get(pane_root_id) else { continue; }; // TODO Convert to physical pixels diff --git a/bevy_editor_panes/bevy_asset_browser/src/lib.rs b/bevy_editor_panes/bevy_asset_browser/src/lib.rs index e404c82c..d592b331 100644 --- a/bevy_editor_panes/bevy_asset_browser/src/lib.rs +++ b/bevy_editor_panes/bevy_asset_browser/src/lib.rs @@ -9,9 +9,14 @@ use bevy::{ io::{file::FileAssetReader, AssetSourceId}, AssetPlugin, }, + ecs::system::{ + lifetimeless::{SCommands, SRes}, + StaticSystemParam, + }, prelude::*, }; -use bevy_pane_layout::prelude::*; +use bevy_editor_styles::Theme; +use bevy_pane_layout::{pane::Pane, prelude::*}; use bevy_scroll_box::ScrollBoxPlugin; use ui::top_bar::location_as_changed; @@ -19,16 +24,18 @@ mod io; mod ui; /// The bevy asset browser plugin -pub struct AssetBrowserPanePlugin; +#[derive(Component)] +pub struct AssetBrowserPane; + +impl Pane for AssetBrowserPane { + const NAME: &str = "Asset Browser"; + const ID: &str = "asset_browser"; -impl Plugin for AssetBrowserPanePlugin { - fn build(&self, app: &mut App) { + fn build(app: &mut App) { embedded_asset!(app, "assets/directory_icon.png"); embedded_asset!(app, "assets/source_icon.png"); embedded_asset!(app, "assets/file_icon.png"); - app.register_pane("Asset Browser", ui::on_pane_creation); - // Fetch the AssetPlugin file path, this is used to create assets at the correct location let default_source_absolute_file_path = { let asset_plugins: Vec<&AssetPlugin> = app.get_added_plugins(); @@ -69,6 +76,25 @@ impl Plugin for AssetBrowserPanePlugin { .run_if(location_as_changed), ); } + + type Param = ( + SCommands, + SRes, + SRes, + SRes, + SRes, + ); + fn on_create(structure: In, param: StaticSystemParam) { + let (commands, theme, location, asset_server, directory_content) = param.into_inner(); + ui::on_pane_creation( + structure, + commands, + theme, + location, + asset_server, + directory_content, + ); + } } /// One entry of [`DirectoryContent`] diff --git a/bevy_editor_panes/bevy_asset_browser/src/ui/mod.rs b/bevy_editor_panes/bevy_asset_browser/src/ui/mod.rs index b44a5a5a..cfb3c5ad 100644 --- a/bevy_editor_panes/bevy_asset_browser/src/ui/mod.rs +++ b/bevy_editor_panes/bevy_asset_browser/src/ui/mod.rs @@ -25,7 +25,7 @@ pub fn on_pane_creation( directory_content: Res, ) { let asset_browser = commands - .entity(structure.content) + .entity(structure.root) .insert(Node { width: Val::Percent(100.0), height: Val::Percent(100.0), diff --git a/bevy_editor_panes/bevy_properties_pane/Cargo.toml b/bevy_editor_panes/bevy_properties_pane/Cargo.toml index 39f86caf..46574678 100644 --- a/bevy_editor_panes/bevy_properties_pane/Cargo.toml +++ b/bevy_editor_panes/bevy_properties_pane/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [dependencies] bevy.workspace = true +bevy_editor_styles.workspace = true +bevy_pane_layout.workspace = true [lints] workspace = true diff --git a/bevy_editor_panes/bevy_properties_pane/src/lib.rs b/bevy_editor_panes/bevy_properties_pane/src/lib.rs index 6ef72e49..20898530 100644 --- a/bevy_editor_panes/bevy_properties_pane/src/lib.rs +++ b/bevy_editor_panes/bevy_properties_pane/src/lib.rs @@ -2,6 +2,27 @@ //! //! Data can be viewed and modified in real-time, with changes being reflected in the application. +use bevy::{ecs::system::StaticSystemParam, prelude::*}; +use bevy_pane_layout::prelude::*; + +/// Pane for displaying the properties of the selected object. +#[derive(Component)] +pub struct PropertiesPane; + +impl Pane for PropertiesPane { + const NAME: &str = "Properties"; + const ID: &str = "properties"; + + fn build(_app: &mut App) { + // todo + } + + type Param = (); + fn on_create(_structure: In, _param: StaticSystemParam) { + // todo + } +} + /// an add function that adds two numbers pub fn add(left: u64, right: u64) -> u64 { left + right diff --git a/bevy_editor_panes/bevy_scene_tree/src/lib.rs b/bevy_editor_panes/bevy_scene_tree/src/lib.rs index 3e04cc06..c579d776 100644 --- a/bevy_editor_panes/bevy_scene_tree/src/lib.rs +++ b/bevy_editor_panes/bevy_scene_tree/src/lib.rs @@ -1,28 +1,30 @@ //! An interactive, collapsible tree view for hierarchical ECS data in Bevy. use bevy::{ - app::Plugin, color::palettes::tailwind, core::Name, ecs::{ component::{ComponentHooks, StorageType}, entity::Entities, + system::{lifetimeless::SCommands, StaticSystemParam}, }, picking::focus::PickingInteraction, prelude::*, }; use bevy_editor_core::SelectedEntity; use bevy_i_cant_believe_its_not_bsn::WithChild; -use bevy_pane_layout::prelude::{PaneAppExt, PaneStructure}; +use bevy_pane_layout::{pane::Pane, prelude::PaneStructure}; use std::ops::Deref; /// Plugin for the editor scene tree pane. -pub struct SceneTreePlugin; +#[derive(Component)] +pub struct SceneTreePane; -impl Plugin for SceneTreePlugin { - fn build(&self, app: &mut App) { - app.register_pane("Scene Tree", setup_pane); +impl Pane for SceneTreePane { + const NAME: &str = "Scene Tree"; + const ID: &str = "scene_tree"; + fn build(app: &mut App) { app.add_systems( PostUpdate, ( @@ -31,30 +33,34 @@ impl Plugin for SceneTreePlugin { update_scene_tree_rows, ) .chain(), - ); + ) + .init_resource::(); } -} + type Param = SCommands; + fn on_create(structure: In, param: StaticSystemParam) { + let mut commands = param.into_inner(); -fn setup_pane(pane: In, mut commands: Commands) { - commands - .entity(pane.content) - .insert(( - SceneTreeRoot, - Node { - flex_direction: FlexDirection::Column, - flex_grow: 1.0, - column_gap: Val::Px(2.0), - padding: UiRect::all(Val::Px(8.0)), - ..Default::default() - }, - BackgroundColor(tailwind::NEUTRAL_600.into()), - )) - .observe( - |mut trigger: Trigger>, mut selected_entity: ResMut| { - selected_entity.0 = None; - trigger.propagate(false); - }, - ); + commands + .entity(structure.root) + .insert(( + SceneTreeRoot, + Node { + flex_direction: FlexDirection::Column, + flex_grow: 1.0, + column_gap: Val::Px(2.0), + padding: UiRect::all(Val::Px(8.0)), + ..Default::default() + }, + BackgroundColor(tailwind::NEUTRAL_600.into()), + )) + .observe( + |mut trigger: Trigger>, + mut selected_entity: ResMut| { + selected_entity.0 = None; + trigger.propagate(false); + }, + ); + } } fn reset_selected_entity_if_entity_despawned( diff --git a/crates/bevy_editor/Cargo.toml b/crates/bevy_editor/Cargo.toml index e19dd848..1dc165db 100644 --- a/crates/bevy_editor/Cargo.toml +++ b/crates/bevy_editor/Cargo.toml @@ -20,6 +20,7 @@ bevy_3d_viewport.workspace = true bevy_2d_viewport.workspace = true bevy_scene_tree.workspace = true bevy_asset_browser.workspace = true +bevy_properties_pane.workspace = true [lints] workspace = true diff --git a/crates/bevy_editor/src/lib.rs b/crates/bevy_editor/src/lib.rs index 8d8685a6..693fde30 100644 --- a/crates/bevy_editor/src/lib.rs +++ b/crates/bevy_editor/src/lib.rs @@ -20,9 +20,12 @@ use bevy_context_menu::ContextMenuPlugin; use bevy_editor_styles::StylesPlugin; // Panes -use bevy_2d_viewport::Viewport2dPanePlugin; -use bevy_3d_viewport::Viewport3dPanePlugin; -use bevy_asset_browser::AssetBrowserPanePlugin; +use bevy_2d_viewport::Bevy2dViewportPane; +use bevy_3d_viewport::Bevy3dViewportPane; +use bevy_asset_browser::AssetBrowserPane; +use bevy_pane_layout::prelude::PaneAppExt; +use bevy_properties_pane::PropertiesPane; +use bevy_scene_tree::SceneTreePane; use crate::load_gltf::LoadGltfPlugin; @@ -47,14 +50,18 @@ impl Plugin for EditorPlugin { // Update/register this project to the editor project list project::update_project_info(); + bevy_app + .register_pane::() + .register_pane::() + .register_pane::() + .register_pane::() + .register_pane::(); + bevy_app .add_plugins(( ContextMenuPlugin, StylesPlugin, - Viewport2dPanePlugin, - Viewport3dPanePlugin, ui::EditorUIPlugin, - AssetBrowserPanePlugin, LoadGltfPlugin, )) .add_systems(Startup, dummy_setup); @@ -64,16 +71,11 @@ impl Plugin for EditorPlugin { /// Your game application /// This appllication allow your game to run, and the editor to be attached to it #[derive(Default)] -pub struct App; +pub struct App(pub BevyApp); impl App { /// create new instance of [`App`] pub fn new() -> Self { - Self - } - - /// Run the application - pub fn run(&self) -> AppExit { let args = std::env::args().collect::>(); let editor_mode = !args.iter().any(|arg| arg == "-game"); @@ -83,7 +85,12 @@ impl App { bevy_app.add_plugins(EditorPlugin); } - bevy_app.run() + Self(bevy_app) + } + + /// Run the application + pub fn run(&mut self) -> AppExit { + self.0.run() } } diff --git a/crates/bevy_editor/src/ui.rs b/crates/bevy_editor/src/ui.rs index 9c0a41f7..5cd9df5e 100644 --- a/crates/bevy_editor/src/ui.rs +++ b/crates/bevy_editor/src/ui.rs @@ -1,27 +1,28 @@ use bevy::prelude::*; +use bevy_3d_viewport::Bevy3dViewportPane; +use bevy_asset_browser::AssetBrowserPane; use bevy_editor_styles::Theme; use bevy_footer_bar::{FooterBarNode, FooterBarPlugin, FooterBarSet}; use bevy_menu_bar::{MenuBarNode, MenuBarPlugin, MenuBarSet}; -use bevy_pane_layout::{PaneLayoutPlugin, PaneLayoutSet, RootPaneLayoutNode}; -use bevy_scene_tree::SceneTreePlugin; +use bevy_pane_layout::{prelude::*, PaneLayoutPlugin, PaneLayoutSet, RootPaneLayoutNode}; +use bevy_properties_pane::PropertiesPane; +use bevy_scene_tree::SceneTreePane; /// The Bevy Editor UI Plugin. pub struct EditorUIPlugin; impl Plugin for EditorUIPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, ui_setup.in_set(UISet)) - .configure_sets( - Startup, - (PaneLayoutSet, MenuBarSet, FooterBarSet).after(UISet), - ) - .add_plugins(( - PaneLayoutPlugin, - MenuBarPlugin, - FooterBarPlugin, - SceneTreePlugin, - )); + app.add_systems( + Startup, + (ui_setup.in_set(UISet), initial_layout.in_set(PaneLayoutSet)), + ) + .configure_sets( + Startup, + (PaneLayoutSet, MenuBarSet, FooterBarSet).after(UISet), + ) + .add_plugins((PaneLayoutPlugin, MenuBarPlugin, FooterBarPlugin)); } } @@ -43,6 +44,7 @@ fn ui_setup(mut commands: Commands, theme: Res) { commands .spawn(( + Name::new("UI Root"), Node { width: Val::Percent(100.0), height: Val::Percent(100.0), @@ -56,8 +58,35 @@ fn ui_setup(mut commands: Commands, theme: Res) { RootUINode, )) .with_children(|parent| { - parent.spawn(MenuBarNode); - parent.spawn(RootPaneLayoutNode); - parent.spawn(FooterBarNode); + parent.spawn((Name::new("Menu Bar"), MenuBarNode)); + parent.spawn((Name::new("Layout Root"), RootPaneLayoutNode)); + parent.spawn((Name::new("Footer Bar"), FooterBarNode)); }); } + +/// This is temporary, until we can load maps from the asset browser +fn initial_layout( + mut commands: Commands, + theme: Res, + panes_root: Single>, +) { + let mut root_divider = + spawn_root_divider(&mut commands, Divider::Horizontal, Some(*panes_root), 1.); + + let mut sidebar_divider = root_divider.add_divider(0.2); + sidebar_divider + .add_pane_group(&theme, 0.4) + .add_pane(&theme, SceneTreePane); + sidebar_divider + .add_pane_group(&theme, 0.6) + .add_pane(&theme, PropertiesPane) + .add_pane(&theme, AssetBrowserPane); + + let mut asset_browser_divider = root_divider.add_divider(0.8); + asset_browser_divider + .add_pane_group(&theme, 0.7) + .add_pane(&theme, Bevy3dViewportPane::default()); + asset_browser_divider + .add_pane_group(&theme, 0.3) + .add_pane(&theme, AssetBrowserPane); +} diff --git a/crates/bevy_editor_styles/src/lib.rs b/crates/bevy_editor_styles/src/lib.rs index bcbf812e..2927282e 100644 --- a/crates/bevy_editor_styles/src/lib.rs +++ b/crates/bevy_editor_styles/src/lib.rs @@ -66,6 +66,8 @@ pub struct TextStyles { /// The styles for panes in the editor. pub struct PaneStyles { + /// The background color of an active header tab button for a pane. + pub header_tab_background_color: BackgroundColor, /// The background color of the header of the pane. pub header_background_color: BackgroundColor, /// The background color of the content area of the pane. @@ -122,7 +124,7 @@ impl FromWorld for Theme { Theme { general: GeneralStyles { border_radius: BorderRadius::all(Val::Px(8.)), - background_color: BackgroundColor(Color::oklch(0.209, 0.0, 0.0)), + background_color: BackgroundColor(Color::srgb(0.09, 0.09, 0.09)), }, button: ButtonStyles { border_radius: BorderRadius::all(Val::Px(3.)), @@ -137,8 +139,9 @@ impl FromWorld for Theme { .load("embedded://bevy_editor_styles/assets/fonts/Inter-Regular.ttf"), }, pane: PaneStyles { - header_background_color: BackgroundColor(Color::oklch(0.3407, 0.0, 0.0)), - area_background_color: BackgroundColor(Color::oklch(0.3677, 0.0, 0.0)), + header_tab_background_color: BackgroundColor(Color::srgb(0.31, 0.31, 0.31)), + header_background_color: BackgroundColor(Color::srgb(0.180, 0.180, 0.180)), + area_background_color: BackgroundColor(Color::srgb(0.1294, 0.1294, 0.1294)), header_border_radius: BorderRadius::top(Val::Px(8.)), }, menu: MenuStyles { diff --git a/crates/bevy_pane_layout/Cargo.toml b/crates/bevy_pane_layout/Cargo.toml index 900de045..05c29bad 100644 --- a/crates/bevy_pane_layout/Cargo.toml +++ b/crates/bevy_pane_layout/Cargo.toml @@ -3,11 +3,19 @@ name = "bevy_pane_layout" version = "0.1.0" edition = "2021" - [dependencies] bevy.workspace = true bevy_editor_styles.workspace = true bevy_context_menu.workspace = true +[dependencies.derive_more] +version = "1" +features = [ + "error", + "from", + "display", +] +default-features = false + [lints] workspace = true diff --git a/crates/bevy_pane_layout/src/handlers.rs b/crates/bevy_pane_layout/src/handlers.rs index a6b20871..e0d38cdb 100644 --- a/crates/bevy_pane_layout/src/handlers.rs +++ b/crates/bevy_pane_layout/src/handlers.rs @@ -1,119 +1,35 @@ use bevy::prelude::*; -use bevy_editor_styles::Theme; use crate::{ - ui::{spawn_divider, spawn_pane, spawn_resize_handle}, - Divider, PaneRootNode, RootPaneLayoutNode, Size, + prelude::DividerCommandsQuery, + ui::{ + pane::PaneNode, + pane_group::{PaneGroup, PaneGroupCommandsQuery}, + }, }; pub(crate) fn remove_pane( - target: In, - mut commands: Commands, - parent_query: Query<&Parent>, - children_query: Query<&Children>, - root_query: Query<(), With>, - mut size_query: Query<&mut Size>, + target_pane_id: In, + pane_query: Query<&PaneNode>, + mut pane_group_query: PaneGroupCommandsQuery, ) { - // Grab the id of the pane root - let target = parent_query.iter_ancestors(*target).nth(1).unwrap(); + // Grab the pane information + let target_pane = pane_query.get(target_pane_id.0).unwrap(); - let parent = parent_query.get(target).unwrap().get(); - - // Prevent the removal of the last panel - if root_query.contains(parent) { - return; - } - - // Find the index of this pane among its siblings - let siblings = children_query.get(parent).unwrap(); - let index = siblings - .iter() - .position(|entity| *entity == target) - .unwrap(); - - let size = size_query.get(target).unwrap().0; - - let not_first_child = index != 0; - - let a = not_first_child.then(|| siblings.get(index - 2)).flatten(); - let b = siblings.get(index + 2); - - match (a, b) { - (None, None) => unreachable!(), - (None, Some(e)) | (Some(e), None) => { - size_query.get_mut(*e).unwrap().0 += size; - } - (Some(a), Some(b)) => { - size_query.get_mut(*a).unwrap().0 += size / 2.; - size_query.get_mut(*b).unwrap().0 += size / 2.; - } - } - - // Despawn the resize handle next to this pane - let resize_handle_index = if not_first_child { index - 1 } else { 1 }; - commands - .entity(siblings[resize_handle_index]) - .despawn_recursive(); - // Despawn this pane - commands.entity(target).despawn_recursive(); + // Get the pane's group and remove the pane from it + let mut pane_group = pane_group_query.get(target_pane.group).unwrap(); + pane_group.remove_pane(target_pane_id.0); } -/// Right clicking dividers the pane horizontally -/// Holding left shift and right clicking dividers the pane vertically -#[expect(clippy::too_many_arguments)] -pub(crate) fn split_pane( - In((target, vertical)): In<(Entity, bool)>, - mut commands: Commands, - theme: Res, - divider_query: Query<&Divider>, - pane_root_query: Query<&PaneRootNode>, - mut size_query: Query<&mut Size>, - children_query: Query<&Children>, - parent_query: Query<&Parent>, +pub(crate) fn remove_pane_group( + target_group_id: In, + mut divider_query: DividerCommandsQuery, + parent_query: Query<&Parent, With>, ) { - let divider = if vertical { - Divider::Vertical - } else { - Divider::Horizontal - }; - - // Grab the id of the pane root - let target = parent_query.iter_ancestors(target).nth(1).unwrap(); - - let pane = pane_root_query.get(target).unwrap(); - - let parent = parent_query.get(target).unwrap().get(); - - // Find the index of this pane among its siblings - let siblings = children_query.get(parent).unwrap(); - let index = siblings - .iter() - .position(|entity| *entity == target) - .unwrap(); - - // Parent has a matching divider direction - let matching_direction = divider_query - .get(parent) - .map(|parent_divider| *parent_divider == divider) - .unwrap_or(false); - - let mut size = size_query.get_mut(target).unwrap(); - let new_size = if matching_direction { size.0 / 2. } else { 0.5 }; - - // TODO The new pane should inherit the state of the existing pane - let new_pane = spawn_pane(&mut commands, &theme, new_size, &pane.name).id(); - - let resize_handle = spawn_resize_handle(&mut commands, divider).id(); + // Grab the divider commands for the divider containing this group + let target_parent = parent_query.get(target_group_id.0).unwrap(); + let mut divider = divider_query.get(target_parent.get()).unwrap(); - if matching_direction { - commands - .entity(parent) - .insert_children(index + 1, &[resize_handle, new_pane]); - } else { - let divider = spawn_divider(&mut commands, divider, size.0) - .add_children(&[target, resize_handle, new_pane]) - .id(); - commands.entity(parent).insert_children(index, &[divider]); - } - size.0 = new_size; + // Remove the group from the divider. + divider.remove(target_group_id.0); } diff --git a/crates/bevy_pane_layout/src/lib.rs b/crates/bevy_pane_layout/src/lib.rs index 9bcdec96..f4a9d21a 100644 --- a/crates/bevy_pane_layout/src/lib.rs +++ b/crates/bevy_pane_layout/src/lib.rs @@ -1,6 +1,7 @@ //! Resizable, divider-able panes for Bevy. mod handlers; +pub mod pane; mod pane_drop_area; pub mod registry; mod ui; @@ -18,20 +19,21 @@ mod ui; /// - Panes cannot have min/max sizes, they must be able to be resized to any size. /// - If a pane can not be sensibly resized, it can overflow under the other panes. /// - Panes must not interfere with each other, only temporary/absolute positioned elements are allowed to overlap panes. -use bevy::prelude::*; +use bevy::{prelude::*, utils::HashSet}; use bevy_editor_styles::Theme; -use crate::{ - registry::{PaneAppExt, PaneRegistryPlugin, PaneStructure}, - ui::{spawn_divider, spawn_pane, spawn_resize_handle}, -}; +use crate::registry::PaneRegistryPlugin; /// Crate prelude. pub mod prelude { - pub use crate::{ - registry::{PaneAppExt, PaneStructure}, - PaneAreaNode, PaneContentNode, PaneHeaderNode, + pub use crate::pane::Pane; + pub use crate::registry::PaneAppExt; + pub use crate::ui::divider::{spawn_root_divider, DividerCommands, DividerCommandsQuery}; + pub use crate::ui::pane::PaneStructure; + pub use crate::ui::pane_group::{ + PaneGroupAreaNode, PaneGroupCommands, PaneGroupContentNode, PaneGroupHeaderNode, }; + pub use crate::{Divider, RootPaneLayoutNode}; } /// The Bevy Pane Layout Plugin. @@ -39,23 +41,58 @@ pub struct PaneLayoutPlugin; impl Plugin for PaneLayoutPlugin { fn build(&self, app: &mut App) { - // TODO Move these registrations to their respective crates. - app.register_pane("Properties", |_pane_structure: In| { - // Todo - }); - app.add_plugins(PaneRegistryPlugin) .init_resource::() - .add_systems(Startup, setup.in_set(PaneLayoutSet)) + .add_systems(Startup, setup_background.in_set(PaneLayoutSet)) .add_systems( Update, - (cleanup_divider_single_child, apply_size) - .chain() - .in_set(PaneLayoutSet), + (normalize_size, apply_size).chain().in_set(PaneLayoutSet), ); } } +/// Makes sure the sizes for each divider's contents adds up to 1 +fn normalize_size( + mut size_query: Query<(Mut, &Parent)>, + divider_query: Query<(Entity, &Divider, Ref)>, +) { + let mut changed_dividers = HashSet::new(); + + for (size, parent) in size_query.iter() { + if size.is_changed() { + changed_dividers.insert(parent.get()); + } + } + + for (entity, _, children) in divider_query.iter() { + if children.is_changed() { + changed_dividers.insert(entity); + } + } + + for entity in changed_dividers.iter() { + let Ok((.., div_children)) = divider_query.get(*entity) else { + continue; + }; + + let mut total_size = 0.0; + for child in div_children.iter() { + if let Ok((size, _)) = size_query.get(*child) { + total_size += size.0; + } + } + + if total_size != 1.0 { + for child in div_children.iter() { + if let Ok((mut size, _)) = size_query.get_mut(*child) { + size.0 /= total_size; + } + } + } + } +} + +/// Updates the Node's size to match the Size component fn apply_size( mut query: Query<(Entity, &Size, &mut Node), Changed>, divider_query: Query<&Divider>, @@ -97,7 +134,7 @@ pub struct PaneLayoutSet; // TODO There is no way to save or load layouts at this moment. // The setup system currently just creates a default layout at startup. -fn setup( +fn setup_background( mut commands: Commands, theme: Res, panes_root: Single>, @@ -105,75 +142,67 @@ fn setup( commands.entity(*panes_root).insert(( Node { padding: UiRect::all(Val::Px(1.)), - flex_grow: 1., width: Val::Percent(100.), - + height: Val::Px(0.0), + flex_grow: 1.0, ..default() }, theme.general.background_color, )); - - let divider = spawn_divider(&mut commands, Divider::Horizontal, 1.) - .set_parent(*panes_root) - .id(); - - let sub_divider = spawn_divider(&mut commands, Divider::Vertical, 0.2) - .set_parent(divider) - .id(); - - spawn_pane(&mut commands, &theme, 0.4, "Scene Tree").set_parent(sub_divider); - spawn_resize_handle(&mut commands, Divider::Vertical).set_parent(sub_divider); - spawn_pane(&mut commands, &theme, 0.6, "Properties").set_parent(sub_divider); - - spawn_resize_handle(&mut commands, Divider::Horizontal).set_parent(divider); - - let asset_browser_divider = spawn_divider(&mut commands, Divider::Vertical, 0.8) - .set_parent(divider) - .id(); - - spawn_pane(&mut commands, &theme, 0.70, "Viewport 3D").set_parent(asset_browser_divider); - spawn_resize_handle(&mut commands, Divider::Vertical).set_parent(asset_browser_divider); - spawn_pane(&mut commands, &theme, 0.30, "Asset Browser").set_parent(asset_browser_divider); } +// TODO: Reimplement /// Removes a divider from the hierarchy when it has only one child left, replacing itself with that child. -fn cleanup_divider_single_child( - mut commands: Commands, - mut query: Query<(Entity, &Children, &Parent), (Changed, With)>, - mut size_query: Query<&mut Size>, - children_query: Query<&Children>, - resize_handle_query: Query<(), With>, -) { - for (entity, children, parent) in &mut query { - let mut iter = children - .iter() - .filter(|child| !resize_handle_query.contains(**child)); - let child = *iter.next().unwrap(); - if iter.next().is_some() { - continue; - } - - let size = size_query.get(entity).unwrap().0; - size_query.get_mut(child).unwrap().0 = size; - - // Find the index of this divider among its siblings - let siblings = children_query.get(parent.get()).unwrap(); - let index = siblings.iter().position(|s| *s == entity).unwrap(); - - commands - .entity(parent.get()) - .insert_children(index, &[child]); - commands.entity(entity).despawn_recursive(); - } -} - +// fn cleanup_divider_single_child( +// mut commands: Commands, +// mut query: Query<(Entity, &Divider, &Parent), Changed>, +// mut size_query: Query<&mut Size>, +// children_query: Query<&Children>, +// resize_handle_query: Query<(), With>, +// ) { +// for (entity, children, parent) in &mut query { +// let mut iter = children +// .iter() +// .filter(|child| !resize_handle_query.contains(**child)); +// let child = *iter.next().unwrap(); +// if iter.next().is_some() { +// continue; +// } + +// let size = size_query.get(entity).unwrap().0; +// size_query.get_mut(child).unwrap().0 = size; + +// // Find the index of this divider among its siblings +// let siblings = children_query.get(parent.get()).unwrap(); +// let index = siblings.iter().position(|s| *s == entity).unwrap(); + +// commands +// .entity(parent.get()) +// .insert_children(index, &[child]); +// commands.entity(entity).despawn_recursive(); +// } +// } + +// I would prefer the divider component be private, but it is currently used by the spawn_root_divider function. /// A node that divides an area into multiple areas along an axis. #[derive(Component, Clone, Copy, PartialEq, Eq)] -enum Divider { +pub enum Divider { + /// A divider that stacks its contents horizontally Horizontal, + /// A divider that stacks its contents vertically Vertical, } +impl Divider { + /// Gets the reversed direction of the divider + pub fn flipped(&self) -> Self { + match self { + Divider::Horizontal => Divider::Vertical, + Divider::Vertical => Divider::Horizontal, + } + } +} + #[derive(Component)] struct ResizeHandle; @@ -184,21 +213,3 @@ struct Size(f32); /// Root node to capture all editor UI elements, nothing but the layout system should modify this. #[derive(Component)] pub struct RootPaneLayoutNode; - -/// Root node for each pane, holds all event nodes for layout and the basic structure for all Panes. -#[derive(Component)] -struct PaneRootNode { - name: String, -} - -/// Node to denote the area of the Pane. -#[derive(Component)] -pub struct PaneAreaNode; - -/// Node to add widgets into the header of a Pane. -#[derive(Component)] -pub struct PaneHeaderNode; - -/// Node to denote the content space of the Pane. -#[derive(Component)] -pub struct PaneContentNode; diff --git a/crates/bevy_pane_layout/src/pane.rs b/crates/bevy_pane_layout/src/pane.rs new file mode 100644 index 00000000..c96c2986 --- /dev/null +++ b/crates/bevy_pane_layout/src/pane.rs @@ -0,0 +1,51 @@ +//! [`Pane`] module. + +use std::marker::PhantomData; + +use bevy::{ + ecs::system::{StaticSystemParam, SystemParam}, + prelude::*, +}; + +use crate::prelude::PaneStructure; + +/// Trait for pane definitions +pub trait Pane: Component { + /// The parameter(s) used in the creation function. + type Param: SystemParam + 'static; + + /// The default name displayed in the pane's header + const NAME: &str; + + /// The id that the pane will be serialized under in saved formats + const ID: &str; + + /// Similar to Plugin::build, this code is run when the Pane is registered + fn build(app: &mut App); + + /// A system that should be run when the Pane is added. Should create the pane's content. + fn on_create(pane: In, param: StaticSystemParam); +} + +#[derive(Default)] +pub(crate) struct PanePlugin { + marker: PhantomData, +} + +impl PanePlugin { + pub fn new() -> Self { + Self { + marker: PhantomData, + } + } +} + +impl Plugin for PanePlugin { + fn build(&self, app: &mut App) { + T::build(app) + } + + fn is_unique(&self) -> bool { + false + } +} diff --git a/crates/bevy_pane_layout/src/registry.rs b/crates/bevy_pane_layout/src/registry.rs index e4a90114..5cdae32a 100644 --- a/crates/bevy_pane_layout/src/registry.rs +++ b/crates/bevy_pane_layout/src/registry.rs @@ -1,12 +1,19 @@ //! [`PaneRegistry`] module. +use derive_more::derive::{Display, Error}; + use bevy::{ ecs::system::{BoxedSystem, SystemId}, prelude::*, utils::HashMap, }; -use crate::{PaneLayoutSet, PaneRootNode}; +use crate::{ + pane::{Pane, PanePlugin}, + prelude::PaneStructure, + ui::pane::PaneNode, + PaneLayoutSet, +}; pub(crate) struct PaneRegistryPlugin; @@ -17,70 +24,80 @@ impl Plugin for PaneRegistryPlugin { } } +/// Returned when Pane registry fails +#[derive(Debug, Error, Display)] +enum PaneRegistryError { + DuplicatePane, +} + /// A registry of pane types. #[derive(Resource, Default)] pub struct PaneRegistry { - panes: Vec, -} - -/// The node structure of a pane. -#[derive(Component, Clone, Copy)] -pub struct PaneStructure { - /// The root of the pane. - pub root: Entity, - /// The area node. Child of the root node. - pub area: Entity, - /// The header node. Child of the area node. - pub header: Entity, - /// The content node. Child of the area node. - pub content: Entity, + panes: HashMap, } impl PaneRegistry { /// Register a new pane type. - pub fn register( - &mut self, - name: impl Into, - system: impl IntoSystem, (), M>, - ) { - self.panes.push(Pane { - name: name.into(), - creation_callback: Some(Box::new(IntoSystem::into_system(system))), - }); + fn register(&mut self) -> Result<(), PaneRegistryError> { + let key = T::ID.into(); + + if self.panes.contains_key(key) { + Err(PaneRegistryError::DuplicatePane) + } else { + self.panes.insert( + T::ID.into(), + PaneCreationHandle::new(Box::new(IntoSystem::into_system(T::on_create))), + ); + Ok(()) + } } } -struct Pane { - name: String, - creation_callback: Option>>, +/// Contains the Creation system for a Pane. +/// At all times, one (and only one) of the two fields should be None. +struct PaneCreationHandle { + callback: Option>>, + callback_id: Option>>, +} + +impl PaneCreationHandle { + fn new(callback: BoxedSystem>) -> Self { + Self { + callback: Some(callback), + callback_id: None, + } + } + + fn id_for(&mut self, world: &mut World) -> SystemId> { + if let Some(id) = self.callback_id { + id + } else { + let id = world.register_boxed_system(self.callback.take().unwrap()); + self.callback_id = Some(id); + id + } + } } pub(crate) fn on_pane_creation( world: &mut World, - roots_query: &mut QueryState>, - pane_root_node_query: &mut QueryState<(&PaneRootNode, &PaneStructure)>, - mut system_ids: Local>>>, + pane_entity_query: &mut QueryState>, + pane_node_query: &mut QueryState<&PaneNode>, ) { - let roots: Vec<_> = roots_query.iter(world).collect(); - for entity in roots { + let new_panes: Vec<_> = pane_entity_query.iter(world).collect(); + for entity in new_panes { world.resource_scope(|world, mut pane_registry: Mut| { - let (pane_root, &structure) = pane_root_node_query.get(world, entity).unwrap(); - let pane = pane_registry - .panes - .iter_mut() - .find(|pane| pane.name == pane_root.name); - - if let Some(pane) = pane { - let id = system_ids.entry(pane.name.clone()).or_insert_with(|| { - world.register_boxed_system(pane.creation_callback.take().unwrap()) - }); - - world.run_system_with_input(*id, structure).unwrap(); + let pane = pane_node_query.get(world, entity).unwrap(); + let creation_handle = pane_registry.panes.get_mut(&pane.id); + let pane_structure = PaneStructure::new(entity, pane.container, pane.header); + + if let Some(creation_handle) = creation_handle { + let system_id = creation_handle.id_for(world); + world + .run_system_with_input(system_id, pane_structure) + .unwrap(); } else { - warn!( - "No pane found in the registry with name: '{}'", - pane_root.name - ); + warn!("No pane found in the registry with id: '{}'", pane.id); } }); } @@ -89,21 +106,20 @@ pub(crate) fn on_pane_creation( /// Extension trait for [`App`]. pub trait PaneAppExt { /// Register a new pane type. - fn register_pane( - &mut self, - name: impl Into, - system: impl IntoSystem, (), M>, - ); + fn register_pane(&mut self) -> &mut Self; } impl PaneAppExt for App { - fn register_pane( - &mut self, - name: impl Into, - system: impl IntoSystem, (), M>, - ) { - self.world_mut() + fn register_pane(&mut self) -> &mut Self { + let result = self + .world_mut() .get_resource_or_init::() - .register(name, system); + .register::(); + + if matches!(result, Ok(..)) { + self.add_plugins(PanePlugin::::new()); + } + + self } } diff --git a/crates/bevy_pane_layout/src/ui/divider.rs b/crates/bevy_pane_layout/src/ui/divider.rs new file mode 100644 index 00000000..7d94aeb8 --- /dev/null +++ b/crates/bevy_pane_layout/src/ui/divider.rs @@ -0,0 +1,169 @@ +use bevy::{ + ecs::{query::QueryEntityError, system::SystemParam}, + prelude::*, +}; +use bevy_editor_styles::Theme; + +use crate::{Divider, Size}; + +use super::{ + pane_group::{spawn_pane_group, PaneGroupCommands}, + resize_handle::spawn_resize_handle, +}; + +/// A list of commands that will be run to modify a Divider Entity. +pub struct DividerCommands<'a> { + entity: Entity, + commands: Commands<'a, 'a>, + direction: Divider, + contents: Vec, + resize_handles: Vec, +} + +impl<'a> DividerCommands<'a> { + pub(crate) fn new(entity: Entity, commands: Commands<'a, 'a>, divider: Divider) -> Self { + DividerCommands { + entity, + commands, + direction: divider, + contents: Vec::new(), + resize_handles: Vec::new(), + } + } + + /// Adds a divider to this divider, returns a DividerCommands object for the new divider. + pub fn add_divider(&mut self, size: f32) -> DividerCommands { + // If a respawn handle is needed, add it + if self.contents.len() != 0 { + self.add_resize_handle(); + } + + // Create sub divider + let new_dir = self.direction.flipped(); + let child = create_divider(&mut self.commands, new_dir, size) + .set_parent(self.entity) + .id(); + self.contents.push(child); + + DividerCommands::new(child, self.commands.reborrow(), new_dir) + } + + /// Adds a Pane Group to this divider, return the new Pane Group's commands. + pub fn add_pane_group(&mut self, theme: &Theme, size: f32) -> PaneGroupCommands { + // If a respawn handle is needed, add it + if self.contents.len() != 0 { + self.add_resize_handle(); + } + + let (mut pane_group, data) = spawn_pane_group(&mut self.commands, theme, size); + pane_group.set_parent(self.entity); + self.contents.push(pane_group.id()); + + PaneGroupCommands::new( + pane_group.id(), + self.commands.reborrow(), + data.header, + data.content, + ) + } + + /// Removes and despawns the given content entity. + pub fn remove(&mut self, entity: Entity) { + let i = self.contents.iter().position(|val| *val == entity).unwrap(); + self.remove_at(i) + } + + /// Removes and despawns the content element in the ith position. Note, this is not the necesarily the ith child + /// of the divider entity (because resize handles are inserted between the content entities). + pub fn remove_at(&mut self, index: usize) { + let group_id = self.contents.remove(index); + self.commands.entity(group_id).despawn_recursive(); + + if self.resize_handles.len() != 0 { + let index = if index >= self.resize_handles.len() { + self.resize_handles.len() - 1 + } else { + index + }; + + self.commands + .entity(self.resize_handles[index]) + .despawn_recursive(); + self.resize_handles.remove(index); + } + } + + fn add_resize_handle(&mut self) { + let handle = spawn_resize_handle(&mut self.commands, self.direction) + .set_parent(self.entity) + .id(); + self.resize_handles.push(handle); + } +} + +/// A system parameter that can be used to get divider commands for each Divider Entity. +#[derive(SystemParam)] +pub struct DividerCommandsQuery<'w, 's> { + commands: Commands<'w, 's>, + dividers: Query<'w, 's, (&'static Divider, &'static Children)>, + sizes: Query<'w, 's, (), With>, +} + +impl<'w, 's> DividerCommandsQuery<'w, 's> { + /// Gets the DividerCommands for a given entity. + pub fn get(&mut self, entity: Entity) -> Result { + let (divider, children) = self.dividers.get(entity)?; + let mut contents = Vec::new(); + let mut resize_handles = Vec::new(); + + for child in children.iter() { + if self.sizes.contains(*child) { + contents.push(*child); + } else { + // Non contents should be resize handles, we could check this in the future + resize_handles.push(*child); + } + } + + Ok(DividerCommands { + entity, + commands: self.commands.reborrow(), + direction: divider.clone(), + contents, + resize_handles, + }) + } +} + +pub(crate) fn create_divider<'a>( + commands: &'a mut Commands, + divider: Divider, + size: f32, +) -> EntityCommands<'a> { + commands.spawn(( + Name::new("Divider"), + Node { + flex_direction: match divider { + Divider::Horizontal => FlexDirection::Row, + Divider::Vertical => FlexDirection::Column, + }, + ..default() + }, + Size(size), + divider, + )) +} + +/// The entry point for a layout. Creates a divider that panes can be added to. +pub fn spawn_root_divider<'a>( + commands: &'a mut Commands, + divider: Divider, + parent: Option, + size: f32, +) -> DividerCommands<'a> { + let mut child = create_divider(commands, divider, size); + if let Some(parent) = parent { + child.set_parent(parent); + }; + DividerCommands::new(child.id(), commands.reborrow(), divider) +} diff --git a/crates/bevy_pane_layout/src/ui/mod.rs b/crates/bevy_pane_layout/src/ui/mod.rs new file mode 100644 index 00000000..f925e1ee --- /dev/null +++ b/crates/bevy_pane_layout/src/ui/mod.rs @@ -0,0 +1,4 @@ +pub mod divider; +pub mod pane; +pub mod pane_group; +pub mod resize_handle; diff --git a/crates/bevy_pane_layout/src/ui/pane.rs b/crates/bevy_pane_layout/src/ui/pane.rs new file mode 100644 index 00000000..77aee7df --- /dev/null +++ b/crates/bevy_pane_layout/src/ui/pane.rs @@ -0,0 +1,223 @@ +use bevy::prelude::*; +use bevy_context_menu::{ContextMenu, ContextMenuOption}; +use bevy_editor_styles::Theme; + +use crate::{handlers::remove_pane, pane::Pane}; + +use super::pane_group::PaneGroupCommandsQuery; + +/// Tag applied to Panes that are currently active. +#[derive(Component)] +pub struct SelectedPane; + +/// Tag applied to a pane container. (Should this be kept?) +#[derive(Component)] +pub struct PaneContainer; + +/// Root node for each pane. +#[derive(Component)] +pub struct PaneNode { + pub(crate) id: String, + pub(crate) header: Entity, + pub(crate) header_text: Entity, + pub(crate) container: Entity, + pub(crate) group: Entity, +} + +impl PaneNode { + pub(crate) fn new( + id: String, + header: Entity, + header_text: Entity, + container: Entity, + group: Entity, + ) -> Self { + Self { + id, + header, + header_text, + container, + group, + } + } +} + +/// The node structure of a pane. +#[derive(Clone, Copy)] +pub struct PaneStructure { + /// The root of the pane. + pub root: Entity, + /// The container holding the root of the pane. + pub(crate) container: Entity, + /// The header node. Child of the area node. + pub header_tag: Entity, +} + +impl PaneStructure { + pub(crate) fn new(root: Entity, container: Entity, header_tag: Entity) -> Self { + Self { + root, + container, + header_tag, + } + } +} + +/// Root node for each pane. +#[derive(Component)] +pub struct PaneHeaderTab { + pane: Entity, +} + +impl PaneHeaderTab { + fn new(pane: Entity) -> Self { + Self { pane } + } +} + +pub(crate) fn spawn_pane_node( + commands: &mut Commands, + theme: &Theme, + pane: T, + group_id: Entity, + group_header: Entity, + group_content: Entity, +) -> PaneStructure { + let pane_entity = commands.spawn_empty().id(); + + let pane_header = commands + .spawn(( + Name::new(format!("{} Header", T::NAME)), + BorderRadius::all(Val::Px(3.0)), + Node { + flex_shrink: 0.0, + padding: UiRect::axes(Val::Px(6.0), Val::Px(3.0)), + ..default() + }, + BackgroundColor(Color::hsla(0., 0., 0., 0.)), + ContextMenu::new([ContextMenuOption::new( + "Close Pane", + move |mut commands, _entity| { + commands.run_system_cached_with(remove_pane, pane_entity); + }, + )]), + )) + .set_parent(group_header) + .observe( + |trigger: Trigger>, + tab_query: Query<&PaneHeaderTab>, + pane_query: Query<&PaneNode>, + mut pane_commands_query: PaneGroupCommandsQuery| { + if trigger.button == PointerButton::Primary { + let pane_entity = tab_query.get(trigger.entity()).unwrap().pane; + let pane_info = pane_query.get(pane_entity).unwrap(); + let mut pane_commands = pane_commands_query.get(pane_info.group).unwrap(); + let index = pane_commands + .panes() + .iter() + .position(|val| val.root == pane_entity); + + if let Some(index) = index { + pane_commands.select_pane(index); + } else { + warn!("Failed to select Pane.") + } + } + }, + ) + .id(); + + let pane_header_text = commands + .spawn(( + Text::new(Into::::into(T::NAME)), + TextFont { + font: theme.text.font.clone(), + font_size: 11., + ..default() + }, + TextColor(theme.text.low_priority), + )) + .set_parent(pane_header) + .id(); + + // This exists between the pane group content node and the pane root so that we can toggle the Pane's visibility without interfering with its contents + let pane_container = commands + .spawn(( + Name::new(format!("{} Pane Container", T::NAME)), + Node { + display: Display::None, // Panes start unselected/invisible + flex_grow: 1., + ..default() + }, + PaneContainer, + )) + .set_parent(group_content) + .id(); + + commands + .entity(pane_entity) + .insert(( + Name::new(format!("{} Pane", T::NAME)), + Node { + display: Display::Flex, + flex_grow: 1., + ..default() + }, + PaneNode::new( + T::ID.into(), + pane_header, + pane_header_text, + pane_container, + group_id, + ), + pane, + )) + .observe( + |trigger: Trigger, + panes: Query<&PaneNode>, + mut containers: Query<&mut Node, With>, + mut backgrounds: Query<&mut BackgroundColor, With>, + mut text_colors: Query<&mut TextColor>, + theme: Res| { + let pane = panes.get(trigger.entity()).unwrap(); + + let mut node = containers.get_mut(pane.container).unwrap(); + node.display = Display::Flex; + + let mut header_background = backgrounds.get_mut(pane.header).unwrap(); + header_background.0 = theme.pane.header_tab_background_color.0; + + let mut header_tab_text = text_colors.get_mut(pane.header_text).unwrap(); + header_tab_text.0 = theme.text.text_color; + }, + ) + .observe( + |trigger: Trigger, + panes: Query<&PaneNode>, + mut containers: Query<&mut Node, With>, + mut backgrounds: Query<&mut BackgroundColor, With>, + mut text_colors: Query<&mut TextColor>, + theme: Res| { + let pane = panes.get(trigger.entity()).unwrap(); + + if let Ok(mut node) = containers.get_mut(pane.container) { + node.display = Display::None; + } + + if let Ok(mut header_background) = backgrounds.get_mut(pane.header) { + header_background.0 = Color::hsla(0., 0., 0., 0.); + } + + if let Ok(mut header_tab_text) = text_colors.get_mut(pane.header_text) { + header_tab_text.0 = theme.text.low_priority; + } + }, + ) + .set_parent(pane_container); + + commands + .entity(pane_header) + .insert(PaneHeaderTab::new(pane_entity)); + + PaneStructure::new(pane_entity, pane_container, pane_header) +} diff --git a/crates/bevy_pane_layout/src/ui/pane_group.rs b/crates/bevy_pane_layout/src/ui/pane_group.rs new file mode 100644 index 00000000..6457d4cc --- /dev/null +++ b/crates/bevy_pane_layout/src/ui/pane_group.rs @@ -0,0 +1,306 @@ +use bevy::{ + ecs::{query::QueryEntityError, system::SystemParam}, + prelude::*, + window::SystemCursorIcon, + winit::cursor::CursorIcon, +}; +use bevy_context_menu::{ContextMenu, ContextMenuOption}; +use bevy_editor_styles::Theme; + +use crate::prelude::PaneStructure; +use crate::{handlers::remove_pane_group, pane::Pane, Size}; + +use super::pane::{spawn_pane_node, PaneNode, SelectedPane}; + +#[derive(Component, Clone)] +pub(crate) struct PaneGroup { + pub(crate) header: Entity, + pub(crate) content: Entity, + pub(crate) panes: Vec, + pub(crate) selected_pane: Option, +} + +impl PaneGroup { + fn new(header: Entity, content: Entity) -> Self { + Self { + header, + content, + panes: Vec::new(), + selected_pane: None, + } + } +} + +/// Node to add widgets into the header of a Pane Group. +#[derive(Component)] +pub struct PaneGroupHeaderNode; + +/// Node to denote the area of the Pane Group. +#[derive(Component)] +pub struct PaneGroupAreaNode; + +/// Node to denote the content section of the Pane Group. +#[derive(Component)] +pub struct PaneGroupContentNode; + +/// A list of commands that will be run to modify a Pane Group. +pub struct PaneGroupCommands<'a> { + entity: Entity, + commands: Commands<'a, 'a>, + header: Entity, + content: Entity, + panes: Vec, + selected_pane: Option, +} + +impl<'a> PaneGroupCommands<'a> { + pub(crate) fn new( + entity: Entity, + commands: Commands<'a, 'a>, + header: Entity, + content: Entity, + ) -> Self { + Self { + entity, + commands, + header, + content, + panes: Vec::new(), + selected_pane: None, + } + } + + /// Adds a Pane to the end of this Pane Group. + pub fn add_pane(&mut self, theme: &Theme, pane: T) -> &'a mut PaneGroupCommands { + let pane = spawn_pane_node( + &mut self.commands, + theme, + pane, + self.entity, + self.header, + self.content, + ); + self.panes.push(pane); + + if self.panes.len() == 1 { + self.set_selected_pane(0); + } + + self.update_group(); + self + } + + /// Sets the given Pane as the active pane + pub fn select_pane(&mut self, index: usize) -> &'a mut PaneGroupCommands { + if index >= self.panes.len() { + warn!("Tried to select invalid Pane"); // Panic? Return Error? + return self; + }; + + self.unselect_pane(); + self.set_selected_pane(index); + + self.update_group(); + self + } + + /// Removes and despawns a pane in this group. + pub fn remove_pane(&mut self, pane: Entity) -> &'a mut PaneGroupCommands { + let index = self.panes.iter().position(|val| val.root == pane).unwrap(); + self.remove_pane_at(index) + } + + /// Removes and despawns the pane in this group at the given index. + pub fn remove_pane_at(&mut self, index: usize) -> &'a mut PaneGroupCommands { + let pane = self.panes.remove(index); + self.commands.entity(pane.container).despawn_recursive(); + self.commands.entity(pane.header_tag).despawn_recursive(); + + if let Some(selected) = self.selected_pane { + if selected > index { + self.selected_pane = Some(selected - 1); + } else if selected == index && self.panes.len() > 0 { + self.set_selected_pane(0); + } + } + + self.update_group(); + self + } + + /// Returns the panes in the selected group + pub fn panes(&self) -> &Vec { + &self.panes + } + + pub(crate) fn group(&self) -> PaneGroup { + PaneGroup { + header: self.header, + content: self.content, + panes: self.panes.iter().map(|pane| pane.root).collect(), + selected_pane: self.selected_pane.clone(), + } + } + + // Selects a given pane, without checking bounds. Does not update the Pane Group. + fn set_selected_pane(&mut self, index: usize) { + self.selected_pane = Some(index); + self.commands + .entity(self.panes[index].root) + .insert(SelectedPane); + } + + // Unselects the selected pane, if one exists. Does not update the Pane Group. + fn unselect_pane(&mut self) { + if let Some(cur_index) = self.selected_pane { + self.commands + .entity(self.panes[cur_index].root) + .remove::(); + + self.selected_pane = None; + }; + } + + fn update_group(&mut self) { + let group_info = self.group(); + self.commands.entity(self.entity).insert(group_info); + } +} + +/// A system parameter that can be used to get divider commands for each Divider Entity. +#[derive(SystemParam)] +pub struct PaneGroupCommandsQuery<'w, 's> { + commands: Commands<'w, 's>, + pane_groups: Query<'w, 's, &'static PaneGroup>, + panes: Query<'w, 's, &'static PaneNode>, +} + +impl<'w, 's> PaneGroupCommandsQuery<'w, 's> { + /// Gets the DividerCommands for a given entity. + pub fn get(&mut self, entity: Entity) -> Result { + let pane_group = self.pane_groups.get(entity)?; + let panes = pane_group + .panes + .iter() + .map(|val| { + let node = self.panes.get(*val).unwrap(); + PaneStructure { + container: node.container, + root: *val, + header_tag: node.header, + } + }) + .collect(); + + Ok(PaneGroupCommands { + entity, + commands: self.commands.reborrow(), + header: pane_group.header, + content: pane_group.content, + panes, + selected_pane: pane_group.selected_pane, + }) + } +} + +pub(crate) fn spawn_pane_group<'a>( + commands: &'a mut Commands, + theme: &Theme, + size: f32, +) -> (EntityCommands<'a>, PaneGroup) { + // Unstyled root node + let root = commands + .spawn(( + Name::new("Pane Group"), + Node { + padding: UiRect::all(Val::Px(1.5)), + ..default() + }, + Size(size), + )) + .id(); + + // Area + let area = commands + .spawn(( + Name::new("Pane Group Area"), + Node { + overflow: Overflow::clip(), + width: Val::Percent(100.), + height: Val::Percent(100.), + flex_direction: FlexDirection::Column, + ..default() + }, + theme.pane.area_background_color, + theme.general.border_radius, + PaneGroupAreaNode, + )) + .set_parent(root) + .id(); + + // Header + let header = commands + .spawn(( + Name::new("Pane Group Header"), + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + column_gap: Val::Px(6.0), + padding: UiRect::axes(Val::Px(5.), Val::Px(3.)), + width: Val::Percent(100.), + height: Val::Px(27.), + align_items: AlignItems::Center, + flex_shrink: 0., + ..default() + }, + theme.pane.header_background_color, + theme.pane.header_border_radius, + ContextMenu::new([ContextMenuOption::new( + "Close Group", + move |mut commands, _entity| { + commands.run_system_cached_with(remove_pane_group, root); + }, + )]), + PaneGroupHeaderNode, + )) + .observe( + move |_trigger: Trigger>, + window_query: Query>, + mut commands: Commands| { + let window = window_query.single(); + commands + .entity(window) + .insert(CursorIcon::System(SystemCursorIcon::Pointer)); + }, + ) + .observe( + |_trigger: Trigger>, + window_query: Query>, + mut commands: Commands| { + let window = window_query.single(); + commands + .entity(window) + .insert(CursorIcon::System(SystemCursorIcon::Default)); + }, + ) + .set_parent(area) + .id(); + + // Content + let content = commands + .spawn(( + Name::new("Pane Group Content"), + Node { + flex_grow: 1., + ..default() + }, + PaneGroupContentNode, + )) + .set_parent(area) + .id(); + + let pane_group_data = PaneGroup::new(header, content); + commands.entity(root).insert(pane_group_data.clone()); + + (commands.entity(root), pane_group_data) +} diff --git a/crates/bevy_pane_layout/src/ui.rs b/crates/bevy_pane_layout/src/ui/resize_handle.rs similarity index 55% rename from crates/bevy_pane_layout/src/ui.rs rename to crates/bevy_pane_layout/src/ui/resize_handle.rs index 58a722ff..cc53a75c 100644 --- a/crates/bevy_pane_layout/src/ui.rs +++ b/crates/bevy_pane_layout/src/ui/resize_handle.rs @@ -1,162 +1,6 @@ use bevy::{prelude::*, window::SystemCursorIcon, winit::cursor::CursorIcon}; -use bevy_context_menu::{ContextMenu, ContextMenuOption}; -use bevy_editor_styles::Theme; -use crate::{ - handlers::*, registry::PaneStructure, Divider, DragState, PaneAreaNode, PaneContentNode, - PaneHeaderNode, PaneRootNode, ResizeHandle, Size, -}; - -pub(crate) fn spawn_pane<'a>( - commands: &'a mut Commands, - theme: &Theme, - size: f32, - name: impl Into, -) -> EntityCommands<'a> { - let name: String = name.into(); - // Unstyled root node - let root = commands - .spawn(( - Node { - padding: UiRect::all(Val::Px(1.5)), - ..default() - }, - Size(size), - PaneRootNode { name: name.clone() }, - )) - .id(); - - // Area - let area = commands - .spawn(( - Node { - overflow: Overflow::clip(), - width: Val::Percent(100.), - height: Val::Percent(100.), - flex_direction: FlexDirection::Column, - ..default() - }, - PaneAreaNode, - theme.pane.area_background_color, - theme.general.border_radius, - )) - .set_parent(root) - .id(); - - // Header - let header = commands - .spawn(( - Node { - padding: UiRect::axes(Val::Px(5.), Val::Px(3.)), - width: Val::Percent(100.), - height: Val::Px(27.), - align_items: AlignItems::Center, - flex_shrink: 0., - ..default() - }, - theme.pane.header_background_color, - theme.pane.header_border_radius, - ContextMenu::new([ - ContextMenuOption::new("Close", |mut commands, entity| { - commands.run_system_cached_with(remove_pane, entity); - }), - ContextMenuOption::new("Split - Horizontal", |mut commands, entity| { - commands.run_system_cached_with(split_pane, (entity, false)); - }), - ContextMenuOption::new("Split - Vertical", |mut commands, entity| { - commands.run_system_cached_with(split_pane, (entity, true)); - }), - ]), - PaneHeaderNode, - )) - .observe( - move |_trigger: Trigger>, - window_query: Query>, - mut commands: Commands| { - let window = window_query.single(); - commands - .entity(window) - .insert(CursorIcon::System(SystemCursorIcon::Pointer)); - }, - ) - .observe( - |_trigger: Trigger>, - window_query: Query>, - mut commands: Commands| { - let window = window_query.single(); - commands - .entity(window) - .insert(CursorIcon::System(SystemCursorIcon::Default)); - }, - ) - .set_parent(area) - .with_children(|parent| { - // Drop down button for selecting the pane type. - // Once a drop down menu is implemented, this will have that added. - parent.spawn(( - Node { - width: Val::Px(31.), - height: Val::Px(19.), - margin: UiRect::right(Val::Px(5.)), - ..default() - }, - theme.button.background_color, - theme.button.border_radius, - )); - parent.spawn(( - Text::new(name), - TextFont { - font: theme.text.font.clone(), - font_size: 14., - ..default() - }, - Node { - flex_shrink: 0., - ..default() - }, - )); - }) - .id(); - - // Content - let content = commands - .spawn(( - Node { - flex_grow: 1., - ..default() - }, - PaneContentNode, - )) - .set_parent(area) - .id(); - - commands.entity(root).insert(PaneStructure { - root, - area, - header, - content, - }); - - commands.entity(root) -} - -pub(crate) fn spawn_divider<'a>( - commands: &'a mut Commands, - divider: Divider, - size: f32, -) -> EntityCommands<'a> { - commands.spawn(( - Node { - flex_direction: match divider { - Divider::Horizontal => FlexDirection::Row, - Divider::Vertical => FlexDirection::Column, - }, - ..default() - }, - Size(size), - divider, - )) -} +use crate::{Divider, DragState, ResizeHandle, Size}; pub(crate) fn spawn_resize_handle<'a>( commands: &'a mut Commands, @@ -165,6 +9,7 @@ pub(crate) fn spawn_resize_handle<'a>( const SIZE: f32 = 7.; // Add a root node with zero size along the divider axis to avoid messing up the layout let mut ec = commands.spawn(( + Name::new("Resize Handle"), Node { width: match divider_parent { Divider::Horizontal => Val::Px(SIZE),