diff --git a/rmf_site_editor/src/aabb.rs b/rmf_site_editor/src/aabb.rs index 1a42323d..6bf91f1a 100644 --- a/rmf_site_editor/src/aabb.rs +++ b/rmf_site_editor/src/aabb.rs @@ -17,10 +17,12 @@ use bevy::{ use smallvec::SmallVec; /// Tracks which [`Entities`](Entity) have which meshes for entities whose [`Aabb`]s are managed by -/// the [`calculate_bounds`] and [`update_bounds`] systems. This is needed because `update_bounds` +/// the [`calculate_bounds`][1] and [`update_bounds`] systems. This is needed because `update_bounds` /// recomputes `Aabb`s for entities whose mesh has been mutated. These mutations are visible via /// [`AssetEvent`](AssetEvent) which tells us which mesh was changed but not which entities /// have that mesh. +/// +/// [1]: bevy::render::view::calculate_bounds #[derive(Debug, Default, Clone, Resource)] pub struct EntityMeshMap { entities_with_mesh: HashMap, SmallVec<[Entity; 1]>>, @@ -76,8 +78,6 @@ pub fn register_bounds( /// Updates [`Aabb`]s for [`Entities`](Entity) with [`Mesh`]es. This includes `Entities` that have /// been assigned new `Mesh`es as well as `Entities` whose `Mesh` has been directly mutated. /// -/// To opt out of bound calculation for an `Entity`, give it the [`NoAabbUpdate`] component. -/// /// NOTE: This system needs to remove entities from their collection in /// [`EntityMeshMap`] whenever a mesh handle is reassigned or an entity's mesh handle is /// removed. This may impact performance if meshes with many entities are frequently diff --git a/rmf_site_editor/src/interaction/gizmo.rs b/rmf_site_editor/src/interaction/gizmo.rs index b3b60d55..196dbaa3 100644 --- a/rmf_site_editor/src/interaction/gizmo.rs +++ b/rmf_site_editor/src/interaction/gizmo.rs @@ -218,7 +218,7 @@ impl Default for GizmoState { } /// Instruction to move an entity to a new transform. This should be caught with -/// an EventReader. +/// an `EventReader`. #[derive(Debug, Clone, Copy, Event)] pub struct MoveTo { pub entity: Entity, diff --git a/rmf_site_editor/src/interaction/mode.rs b/rmf_site_editor/src/interaction/mode.rs index 6b25618d..42162586 100644 --- a/rmf_site_editor/src/interaction/mode.rs +++ b/rmf_site_editor/src/interaction/mode.rs @@ -101,8 +101,8 @@ pub struct BackoutParams<'w, 's> { /// cannot expect users to know this or handle the cleanup correctly. /// /// User-defined systems should never -/// use ResMut. Instead they should always use -/// EventWriter. +/// use `ResMut`. Instead they should always use +/// `EventWriter`. // // TODO(MXG): We could enforce this by letting InteractionMode be public but // wrapping it in a newtype to store it in the resource. The inner type would diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index ffc25c81..bc87607a 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -186,7 +186,7 @@ impl Plugin for SiteEditor { WorkcellEditorPlugin, SitePlugin, InteractionPlugin, - StandardUiLayout, + StandardUiPlugin::default(), AnimationPlugin, OccupancyPlugin, WorkspacePlugin, diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index fa18bafe..7d10ba7a 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -17,11 +17,10 @@ use crate::{log::*, widgets::prelude::*}; use bevy::prelude::*; -use bevy_egui::{ - egui::{self, CollapsingHeader, Color32, RichText}, - EguiContexts, -}; +use bevy_egui::egui::{self, CollapsingHeader, Color32, RichText}; +/// This widget provides a console that displays information, warning, and error +/// messages. #[derive(Default)] pub struct ConsoleWidgetPlugin {} @@ -34,15 +33,14 @@ impl Plugin for ConsoleWidgetPlugin { } fn console_widget( - In(_): In, + In(input): In, mut log_history: ResMut, - mut egui_context: EguiContexts, ) { egui::TopBottomPanel::bottom("log_consolse") .resizable(true) .min_height(30.0) .max_height(300.0) - .show(egui_context.ctx_mut(), |ui| { + .show(&input.context, |ui| { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.5; let status = log_history.top(); diff --git a/rmf_site_editor/src/widgets/creation.rs b/rmf_site_editor/src/widgets/creation.rs index cd0fb372..b593d28c 100644 --- a/rmf_site_editor/src/widgets/creation.rs +++ b/rmf_site_editor/src/widgets/creation.rs @@ -18,28 +18,30 @@ use crate::{ inspector::{InspectAssetSourceComponent, InspectScaleComponent}, interaction::{ChangeMode, SelectAnchor, SelectAnchor3D}, - site::{DefaultFile, DrawingBundle, Recall}, + site::{DefaultFile, DrawingBundle, Recall, AssetSource, RecallAssetSource, Scale}, widgets::{prelude::*, AssetGalleryStatus}, - AppState, CurrentWorkspace, PendingDrawing, PendingModel, + AppState, CurrentWorkspace, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{CollapsingHeader, Ui}; use rmf_site_format::{DrawingProperties, Geometry, Model, WorkcellModel}; +/// This widget provides a widget with buttons for creating new site elements. #[derive(Default)] pub struct CreationPlugin {} impl Plugin for CreationPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app + .init_resource::() .init_resource::() .add_plugins(PropertiesTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct Creation<'w, 's> { +struct Creation<'w, 's> { default_file: Query<'w, 's, &'static DefaultFile>, app_state: Res<'w, State>, change_mode: EventWriter<'w, ChangeMode>, @@ -259,3 +261,17 @@ impl<'w, 's> Creation<'w, 's> { }); } } + +#[derive(Resource, Clone, Default)] +struct PendingDrawing { + pub source: AssetSource, + pub recall_source: RecallAssetSource, +} + + +#[derive(Resource, Clone, Default)] +struct PendingModel { + pub source: AssetSource, + pub recall_source: RecallAssetSource, + pub scale: Scale, +} diff --git a/rmf_site_editor/src/widgets/diagnostics.rs b/rmf_site_editor/src/widgets/diagnostics.rs index ac3e7bd1..a05aa5fb 100644 --- a/rmf_site_editor/src/widgets/diagnostics.rs +++ b/rmf_site_editor/src/widgets/diagnostics.rs @@ -25,12 +25,10 @@ use crate::{ AppState, CurrentWorkspace, Icons, Issue, IssueDictionary, ValidateWorkspace, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::{ - egui::{self, Button, Checkbox, Grid, ImageButton, ScrollArea, Ui}, - EguiContexts, -}; +use bevy_egui::egui::{self, Button, Checkbox, Grid, ImageButton, ScrollArea, Ui}; use std::collections::HashSet; +/// Add a [`Diagnostics`] widget to your application. #[derive(Default)] pub struct DiagnosticsPlugin {} @@ -48,23 +46,25 @@ impl Plugin for DiagnosticsPlugin { } fn diagnostics_panel( - In(panel): In, + In(input): In, world: &mut World, - egui_contexts: &mut SystemState, ) { if world.resource::().show { - let ctx = egui_contexts.get_mut(world).ctx_mut().clone(); egui::SidePanel::left("diagnsotics") .resizable(true) .min_width(320.0) - .show(&ctx, |ui| { - if let Err(err) = world.try_show(panel, ui) { + .show(&input.context, |ui| { + if let Err(err) = world.try_show(input.id, ui) { error!("Unable to display diagnostics panel: {err:?}"); } }); } } +/// A widget that displays diagnostic information about the current site. This +/// helps identify potential problems that you should consider addressing. +/// +/// Use [`DiagnosticsPlugin`] to add this to your application. #[derive(SystemParam)] pub struct Diagnostics<'w, 's> { icons: Res<'w, Icons>, diff --git a/rmf_site_editor/src/widgets/fuel_asset_browser.rs b/rmf_site_editor/src/widgets/fuel_asset_browser.rs index 3722deaf..89152e93 100644 --- a/rmf_site_editor/src/widgets/fuel_asset_browser.rs +++ b/rmf_site_editor/src/widgets/fuel_asset_browser.rs @@ -21,12 +21,12 @@ use crate::{ widgets::prelude::*, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::{ - egui::{self, Button, ComboBox, ImageSource, RichText, ScrollArea, Ui, Window}, - EguiContexts, +use bevy_egui::egui::{ + self, Button, ComboBox, ImageSource, RichText, ScrollArea, Ui, Window, }; use gz_fuel::FuelModel; +/// Add a [`FuelAssetBrowser`] widget to your application. #[derive(Default)] pub struct FuelAssetBrowserPlugin {} @@ -50,7 +50,7 @@ pub struct ShowAssetFilters { pub recall_private: Option, } -/// Used to indicate whether to show or hide the left side panel with the asset gallery +/// Used to indicate whether to show or hide the [`FuelAssetBrowser`]. #[derive(Resource, Default)] pub struct AssetGalleryStatus { pub show: bool, @@ -63,6 +63,13 @@ pub struct AssetGalleryStatus { pub show_api_window: bool, } +/// A widget for browsing models that can be downloaded from fuel. +/// +/// This is part of the [`StandardUiPlugin`][1]. If you are not using the +/// `StandardUiLayout` then it is recommended that you use the +/// [`FuelAssetBrowserPlugin`] to add this to the editor. +/// +/// [1]: crate::widgets::StandardUiPlugin #[derive(SystemParam)] pub struct FuelAssetBrowser<'w, 's> { fuel_client: ResMut<'w, FuelClient>, @@ -76,17 +83,15 @@ pub struct FuelAssetBrowser<'w, 's> { } fn fuel_asset_browser_panel( - In(panel): In, + In(input): In, world: &mut World, - egui_contexts: &mut SystemState, ) { if world.resource::().show { - let ctx = egui_contexts.get_mut(world).ctx_mut().clone(); egui::SidePanel::left("asset_gallery") .resizable(true) .min_width(320.0) - .show(&ctx, |ui| { - if let Err(err) = world.try_show(panel, ui) { + .show(&input.context, |ui| { + if let Err(err) = world.try_show(input.id, ui) { error!("Unable to display asset gallery: {err:?}"); } }); diff --git a/rmf_site_editor/src/widgets/icons.rs b/rmf_site_editor/src/widgets/icons.rs index f55ededb..d3fc93fa 100644 --- a/rmf_site_editor/src/widgets/icons.rs +++ b/rmf_site_editor/src/widgets/icons.rs @@ -16,9 +16,24 @@ */ use crate::{recency::RankAdjustment, site::LayerVisibility}; -use bevy::{ecs::system::SystemState, prelude::*}; +use bevy::{ + asset::embedded_asset, + ecs::system::SystemState, + prelude::* +}; use bevy_egui::{egui::ImageSource, egui::TextureId, EguiContexts}; +/// Add a resource for the common icons of the application. +#[derive(Default)] +pub struct IconsPlugin {} + +impl Plugin for IconsPlugin { + fn build(&self, app: &mut App) { + add_widgets_icons(app); + app.init_resource::(); + } +} + struct IconBuilder(Handle); impl IconBuilder { pub fn new(name: &str, asset_server: &AssetServer) -> Self { @@ -48,7 +63,7 @@ impl Icon { } } -// TODO(MXG): Create a struct to manage bevy-egui image pairs +/// A collection of icons used by the standard widgets. #[derive(Clone, Debug, Resource)] pub struct Icons { pub select: Icon, @@ -156,3 +171,56 @@ impl Icons { } } } + +fn add_widgets_icons(app: &mut App) { + // Taken from https://github.com/bevyengine/bevy/issues/10377#issuecomment-1858797002 + // TODO(luca) remove once we migrate to Bevy 0.13 that includes the fix + #[cfg(any(not(target_family = "windows"), target_env = "gnu"))] + { + embedded_asset!(app, "src/", "icons/add.png"); + embedded_asset!(app, "src/", "icons/alignment.png"); + embedded_asset!(app, "src/", "icons/alpha.png"); + embedded_asset!(app, "src/", "icons/confirm.png"); + embedded_asset!(app, "src/", "icons/down.png"); + embedded_asset!(app, "src/", "icons/edit.png"); + embedded_asset!(app, "src/", "icons/empty.png"); + embedded_asset!(app, "src/", "icons/exit.png"); + embedded_asset!(app, "src/", "icons/global.png"); + embedded_asset!(app, "src/", "icons/hidden.png"); + embedded_asset!(app, "src/", "icons/hide.png"); + embedded_asset!(app, "src/", "icons/merge.png"); + embedded_asset!(app, "src/", "icons/opaque.png"); + embedded_asset!(app, "src/", "icons/reject.png"); + embedded_asset!(app, "src/", "icons/search.png"); + embedded_asset!(app, "src/", "icons/select.png"); + embedded_asset!(app, "src/", "icons/selected.png"); + embedded_asset!(app, "src/", "icons/to_bottom.png"); + embedded_asset!(app, "src/", "icons/to_top.png"); + embedded_asset!(app, "src/", "icons/trash.png"); + embedded_asset!(app, "src/", "icons/up.png"); + } + #[cfg(all(target_family = "windows", not(target_env = "gnu")))] + { + embedded_asset!(app, "src\\", "icons\\add.png"); + embedded_asset!(app, "src\\", "icons\\alignment.png"); + embedded_asset!(app, "src\\", "icons\\alpha.png"); + embedded_asset!(app, "src\\", "icons\\confirm.png"); + embedded_asset!(app, "src\\", "icons\\down.png"); + embedded_asset!(app, "src\\", "icons\\edit.png"); + embedded_asset!(app, "src\\", "icons\\empty.png"); + embedded_asset!(app, "src\\", "icons\\exit.png"); + embedded_asset!(app, "src\\", "icons\\global.png"); + embedded_asset!(app, "src\\", "icons\\hidden.png"); + embedded_asset!(app, "src\\", "icons\\hide.png"); + embedded_asset!(app, "src\\", "icons\\merge.png"); + embedded_asset!(app, "src\\", "icons\\opaque.png"); + embedded_asset!(app, "src\\", "icons\\reject.png"); + embedded_asset!(app, "src\\", "icons\\search.png"); + embedded_asset!(app, "src\\", "icons\\select.png"); + embedded_asset!(app, "src\\", "icons\\selected.png"); + embedded_asset!(app, "src\\", "icons\\to_bottom.png"); + embedded_asset!(app, "src\\", "icons\\to_top.png"); + embedded_asset!(app, "src\\", "icons\\trash.png"); + embedded_asset!(app, "src\\", "icons\\up.png"); + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 9bf6dbce..54d0832c 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -33,7 +33,8 @@ pub struct InspectTexturePlugin {} impl Plugin for InspectTexturePlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app + .init_resource::() .add_plugins(InspectionPlugin::::new()); } } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index ab1305e1..e7848021 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -122,6 +122,54 @@ use bevy_egui::egui::{CollapsingHeader, Ui}; use rmf_site_format::*; use smallvec::SmallVec; +/// Use this plugin to add a single inspection tile into the [`MainInspector`] +/// widget. +/// +/// ```no_run +/// use bevy::prelude::{App, Query, Entity, Res}; +/// use librmf_site_editor::{SiteEditor, site::NameInSite, widgets::prelude::*}; +/// +/// #[derive(SystemParam)] +/// pub struct HelloSelection<'w, 's> { +/// names: Query<'w, 's, &'static NameInSite>, +/// } +/// +/// impl<'w, 's> WidgetSystem for HelloSelection<'w, 's> { +/// fn show( +/// Inspect { selection, .. }: Inspect, +/// ui: &mut Ui, +/// state: &mut SystemState, +/// world: &mut World, +/// ) { +/// let mut params = state.get_mut(world); +/// let name = params.names.get(selection) +/// .map(|name| name.as_str()) +/// .unwrap_or(""); +/// ui.add_space(20.0); +/// ui.heading(format!("Hello, {name}!")); +/// ui.add_space(20.0); +/// } +/// } +/// +/// fn main() { +/// let mut app = App::new(); +/// app.add_plugins(( +/// SiteEditor, +/// InspectionPlugin::::new(), +/// )); +/// +/// app.run(); +/// } +/// ``` +pub struct InspectionPlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + _ignore: std::marker::PhantomData, +} + +/// Use this to create a standard inspector plugin that covers the common use +/// cases of the site editor. #[derive(Default)] pub struct StandardInspectorPlugin {} @@ -162,6 +210,9 @@ impl Plugin for StandardInspectorPlugin { } } +/// Use this to create a minimal inspector plugin. You will be able to add your +/// own [`InspectionPlugin`]s to the application, but none of the standard +/// inspection plugins will be included. #[derive(Default)] pub struct MinimalInspectorPlugin {} @@ -171,13 +222,6 @@ impl Plugin for MinimalInspectorPlugin { } } -pub struct InspectionPlugin -where - W: WidgetSystem + 'static + Send + Sync, -{ - _ignore: std::marker::PhantomData, -} - impl InspectionPlugin where W: WidgetSystem + 'static + Send + Sync, @@ -200,13 +244,19 @@ where } } +/// This is the input type for inspection widgets. Use [`InspectionPlugin`] to +/// add the widget to the application. #[derive(Clone, Copy)] pub struct Inspect { + /// What entity should be treated as selected. pub selection: Entity, - pub inspector: Entity, + /// What entity is the current inspection widget attached to. + pub inspection: Entity, + /// What kind of panel is the inspector rendered on. pub panel: PanelSide, } +/// This contains a reference to the main inspector widget of the application. #[derive(Resource)] pub struct MainInspector { id: Entity, @@ -228,7 +278,7 @@ impl FromWorld for MainInspector { } #[derive(SystemParam)] -struct Inspector<'w, 's> { +pub struct Inspector<'w, 's> { children: Query<'w, 's, &'static Children>, heading: Query<'w, 's, (Option<&'static Category>, Option<&'static SiteID>)>, } @@ -288,7 +338,7 @@ impl<'w, 's> WidgetSystem for Inspector<'w, 's> { for child in children { let inspect = Inspect { selection, - inspector: child, + inspection: child, panel, }; let _ = world.try_show_in(child, inspect, ui); diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index 2c2d5b52..98d2bc4f 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -19,13 +19,11 @@ use crate::{widgets::prelude::*, AppState, CreateNewWorkspace, LoadWorkspace, Sa use bevy::ecs::query::Has; use bevy::prelude::*; -use bevy_egui::{ - egui::{self, Button, Ui}, - EguiContexts, -}; +use bevy_egui::egui::{self, Button, Ui}; use std::collections::HashSet; +/// Add the standard menu bar to the application. #[derive(Default)] pub struct MenuBarPlugin {} @@ -41,8 +39,8 @@ impl Plugin for MenuBarPlugin { } } -/// Adding this to an entity to an entity with the MenuItem component -/// will grey out and disable a MenuItem. +/// Adding this to an entity to an entity with the [`MenuItem`] component +/// will grey out and disable a [`MenuItem`]. #[derive(Component)] pub struct MenuDisabled; @@ -276,8 +274,7 @@ struct MenuParams<'w, 's> { } fn top_menu_bar( - In(_): In, - mut egui_context: EguiContexts, + In(input): In, mut new_workspace: EventWriter, mut save: EventWriter, mut load_workspace: EventWriter, @@ -286,7 +283,7 @@ fn top_menu_bar( children: Query<&Children>, mut menu_params: MenuParams, ) { - egui::TopBottomPanel::top("top_panel").show(egui_context.ctx_mut(), |ui| { + egui::TopBottomPanel::top("top_panel").show(&input.context, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("File", |ui| { if ui.add(Button::new("New").shortcut_text("Ctrl+N")).clicked() { diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index cc2b86f4..b22c96b4 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -15,14 +15,39 @@ * */ +//! The site editor allows you to insert your own egui widgets into the UI. +//! +//! There are three categories of widgets that the site editor provides +//! out-of-the-box support for inserting, but the widget system itself is +//! highly extensible, allowing you to define your own categories of widgets. +//! +//! The three categories provided out of the box include: +//! - [Panel widget][1]: Add a new panel to the UI. +//! - Tile widget: Add a tile into a [panel of tiles][2] such as the [`PropertiesPanel`]. Use [`PropertiesTilePlugin`] to make a new tile widget that goes inside of the standard `PropertiesPanel`. +//! - [`InspectionPlugin`]: Add a widget to the [`MainInspector`] to display more information about the currently selected entity. +//! +//! In our terminology, there are two kinds of panels: +//! - Side panels: A vertical column widget on the left or right side of the screen. +//! - [`PropertiesPanel`] is usually a side panel placed on the right side of the screen. +//! - [`FuelAssetBrowser`] is a side panel typically placed on the left side of the screen. +//! - [`Diagnostics`] is a side panel that interactively flags issues that have been found in the site. +//! - Top / Bottom Panels: +//! - The [`MenuBarPlugin`] provides a menu bar at the top of the screen. +//! - Create an entity with a [`Menu`] component to create a new menu inside the menu bar. +//! - Add an entity with a [`MenuItem`] component as a child to a menu entity to add a new item into a menu. +//! - The [`FileMenu`], [`ToolMenu`], and [`ViewMenu`] are resources that provide access to various standard menus. +//! - The [`ConsoleWidgetPlugin`] provides a console at the bottom of the screen to display information, warning, and error messages. +//! +//! [1]: crate::widgets::PanelWidget +//! [2]: crate::widgets::show_panel_of_tiles + use crate::{ interaction::{Hover, PickingBlockers}, AppState, }; use bevy::{ - asset::embedded_asset, ecs::{ - system::{BoxedSystem, SystemParam, SystemState}, + system::{SystemParam, SystemState}, world::EntityWorldMut, }, prelude::*, @@ -31,35 +56,54 @@ use bevy_egui::{ egui::{self, Ui}, EguiContexts, }; -use rmf_site_format::*; -use smallvec::SmallVec; pub mod building_preview; use building_preview::*; +pub mod console; +use console::*; + pub mod creation; use creation::*; +pub mod diagnostics; +use diagnostics::*; + pub mod fuel_asset_browser; pub use fuel_asset_browser::*; +pub mod icons; +pub use icons::*; + +pub mod inspector; +pub use inspector::*; + pub mod menu_bar; -use menu_bar::*; +pub use menu_bar::*; -pub mod view_groups; -use view_groups::*; +pub mod move_layer; +pub use move_layer::*; -pub mod diagnostics; -use diagnostics::*; +pub mod panel_of_tiles; +pub use panel_of_tiles::*; + +pub mod panel; +pub use panel::*; pub mod properties_panel; pub use properties_panel::*; +pub mod selector_widget; +pub use selector_widget::*; + +pub mod view_groups; +use view_groups::*; + pub mod view_layers; use view_layers::*; pub mod view_levels; -use view_levels::{LevelDisplay, ViewLevelsPlugin}; +use view_levels::*; pub mod view_lights; use view_lights::*; @@ -70,104 +114,40 @@ use view_nav_graphs::*; pub mod view_occupancy; use view_occupancy::*; -pub mod console; -pub use console::*; - -pub mod icons; -pub use icons::*; - -pub mod inspector; -// use inspector::{InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture, ExInspectorWidget}; -use inspector::*; - -pub mod move_layer; -pub use move_layer::*; - -pub mod selector_widget; -pub use selector_widget::*; - -#[derive(Resource, Clone, Default)] -pub struct PendingDrawing { - pub source: AssetSource, - pub recall_source: RecallAssetSource, -} +pub mod prelude { + //! This module gives easy access to the traits, structs, and plugins that + //! we expect downstream users are likely to want easy access to if they are + //! implementing and inserting their own widgets. -#[derive(Resource, Clone, Default)] -pub struct PendingModel { - pub source: AssetSource, - pub recall_source: RecallAssetSource, - pub scale: Scale, + pub use super::{ + properties_panel::*, PanelSide, PanelWidget, PropertiesPanel, + ShareableWidget, ShowError, ShowResult, ShowSharedWidget, Tile, + TryShowWidgetEntity, TryShowWidgetWorld, Widget, WidgetSystem, Inspect, + InspectionPlugin, PropertiesTilePlugin, PanelWidgetInput, + }; + pub use bevy::ecs::{ + system::{SystemParam, SystemState}, + world::World, + }; + pub use bevy_egui::egui::Ui; } +/// This plugin provides the standard UI layout that was designed for the common +/// use cases of the site editor. #[derive(Default)] -pub struct StandardUiLayout; - -fn add_widgets_icons(app: &mut App) { - // Taken from https://github.com/bevyengine/bevy/issues/10377#issuecomment-1858797002 - // TODO(luca) remove once we migrate to Bevy 0.13 that includes the fix - #[cfg(any(not(target_family = "windows"), target_env = "gnu"))] - { - embedded_asset!(app, "src/", "icons/add.png"); - embedded_asset!(app, "src/", "icons/alignment.png"); - embedded_asset!(app, "src/", "icons/alpha.png"); - embedded_asset!(app, "src/", "icons/confirm.png"); - embedded_asset!(app, "src/", "icons/down.png"); - embedded_asset!(app, "src/", "icons/edit.png"); - embedded_asset!(app, "src/", "icons/empty.png"); - embedded_asset!(app, "src/", "icons/exit.png"); - embedded_asset!(app, "src/", "icons/global.png"); - embedded_asset!(app, "src/", "icons/hidden.png"); - embedded_asset!(app, "src/", "icons/hide.png"); - embedded_asset!(app, "src/", "icons/merge.png"); - embedded_asset!(app, "src/", "icons/opaque.png"); - embedded_asset!(app, "src/", "icons/reject.png"); - embedded_asset!(app, "src/", "icons/search.png"); - embedded_asset!(app, "src/", "icons/select.png"); - embedded_asset!(app, "src/", "icons/selected.png"); - embedded_asset!(app, "src/", "icons/to_bottom.png"); - embedded_asset!(app, "src/", "icons/to_top.png"); - embedded_asset!(app, "src/", "icons/trash.png"); - embedded_asset!(app, "src/", "icons/up.png"); - } - #[cfg(all(target_family = "windows", not(target_env = "gnu")))] - { - embedded_asset!(app, "src\\", "icons\\add.png"); - embedded_asset!(app, "src\\", "icons\\alignment.png"); - embedded_asset!(app, "src\\", "icons\\alpha.png"); - embedded_asset!(app, "src\\", "icons\\confirm.png"); - embedded_asset!(app, "src\\", "icons\\down.png"); - embedded_asset!(app, "src\\", "icons\\edit.png"); - embedded_asset!(app, "src\\", "icons\\empty.png"); - embedded_asset!(app, "src\\", "icons\\exit.png"); - embedded_asset!(app, "src\\", "icons\\global.png"); - embedded_asset!(app, "src\\", "icons\\hidden.png"); - embedded_asset!(app, "src\\", "icons\\hide.png"); - embedded_asset!(app, "src\\", "icons\\merge.png"); - embedded_asset!(app, "src\\", "icons\\opaque.png"); - embedded_asset!(app, "src\\", "icons\\reject.png"); - embedded_asset!(app, "src\\", "icons\\search.png"); - embedded_asset!(app, "src\\", "icons\\select.png"); - embedded_asset!(app, "src\\", "icons\\selected.png"); - embedded_asset!(app, "src\\", "icons\\to_bottom.png"); - embedded_asset!(app, "src\\", "icons\\to_top.png"); - embedded_asset!(app, "src\\", "icons\\trash.png"); - embedded_asset!(app, "src\\", "icons\\up.png"); - } -} +pub struct StandardUiPlugin {} -impl Plugin for StandardUiLayout { +impl Plugin for StandardUiPlugin { fn build(&self, app: &mut App) { - add_widgets_icons(app); - app.init_resource::() + app .add_plugins(( + IconsPlugin::default(), MenuBarPlugin::default(), StandardPropertiesPanelPlugin::default(), FuelAssetBrowserPlugin::default(), DiagnosticsPlugin::default(), ConsoleWidgetPlugin::default(), )) - .init_resource::() - .init_resource::() .add_systems(Startup, init_ui_style) .add_systems( Update, @@ -184,26 +164,13 @@ impl Plugin for StandardUiLayout { } } -#[derive(Default)] -pub struct StandardPropertiesPanelPlugin {} - -impl Plugin for StandardPropertiesPanelPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - PropertiesPanelPlugin::new(PanelSide::Right), - ViewLevelsPlugin::default(), - ViewNavGraphsPlugin::default(), - ViewLayersPlugin::default(), - StandardInspectorPlugin::default(), - CreationPlugin::default(), - ViewGroupsPlugin::default(), - ViewLightsPlugin::default(), - ViewOccupancyPlugin::default(), - BuildingPreviewPlugin::default(), - )); - } -} - +/// This component should be given to an entity that needs to be rendered as a +/// nested widget in the UI. +/// +/// For standard types of widgets you don't need to create this component yourself, +/// instead use one of the generic convenience plugins: +/// - [`InspectionPlugin`] +/// - [`PropertiesTilePlugin`] #[derive(Component)] pub struct Widget { inner: Option + 'static + Send + Sync>>, @@ -231,10 +198,17 @@ where } } +/// Do not implement this widget directly. Instead create a struct that derives +/// [`SystemParam`] and then implement [`WidgetSystem`] for that struct. pub trait ExecuteWidget { fn show(&mut self, input: Input, ui: &mut Ui, world: &mut World) -> Output; } +/// Implement this on a [`SystemParam`] struct to make it a widget that can be +/// plugged into the site editor UI. +/// +/// See documentation of [`PropertiesTilePlugin`] or [`InspectionPlugin`] to see +/// examples of using this. pub trait WidgetSystem: SystemParam { fn show(input: Input, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> Output; } @@ -257,6 +231,7 @@ where pub type ShowResult = Result; +/// Errors that can happen while attempting to show a widget. #[derive(Debug)] pub enum ShowError { /// The entity whose widget you are trying to show is missing from the world @@ -269,11 +244,17 @@ pub enum ShowError { Recursion, } +/// Trait implemented on [`World`] to let it render child widgets. Note that +/// this is not able to render widgets recursively, so you should make sure not +/// to have circular dependencies in your widget structure. pub trait TryShowWidgetWorld { + /// Try to show a widget that has `()` for input and output belonging to the + /// specified entity. fn try_show(&mut self, entity: Entity, ui: &mut Ui) -> ShowResult<()> { self.try_show_out(entity, (), ui) } + /// Same as [`Self::try_show`] but takes an input that will be fed to the widget. fn try_show_in(&mut self, entity: Entity, input: Input, ui: &mut Ui) -> ShowResult<()> where Input: 'static + Send + Sync, @@ -281,6 +262,8 @@ pub trait TryShowWidgetWorld { self.try_show_out(entity, input, ui) } + /// Same as [`Self::try_show`] but takes an input for the widget and provides + /// an output from the widget. fn try_show_out( &mut self, entity: Entity, @@ -310,7 +293,10 @@ impl TryShowWidgetWorld for World { } } +/// Same as [`TryShowWidgetWorld`] but is implemented for [`EntityWorldMut`] so +/// you do not need to specify the target entity. pub trait TryShowWidgetEntity { + /// Try to show a widget that has `()` for input and output fn try_show(&mut self, ui: &mut Ui) -> ShowResult<()> { self.try_show_out((), ui) } @@ -357,6 +343,9 @@ impl<'w> TryShowWidgetEntity for EntityWorldMut<'w> { /// parameters do not use the [`Changed`] filter. It is the responsibility of /// the user to ensure that sharing this widget will not have any bad side /// effects. +/// +/// [`ShareableWidget`]s can be used by the [`ShowSharedWidget`] trait which is +/// implemented for the [`World`] struct. pub trait ShareableWidget {} /// A resource to store a widget so that it can be reused multiple times in one @@ -393,75 +382,15 @@ impl ShowSharedWidget for World { } } -#[derive(Clone, Copy, Debug)] -pub struct Panel { - pub id: Entity, - pub side: PanelSide, -} - -pub struct Tile { - pub id: Entity, - pub panel: PanelSide, -} - -pub mod prelude { - pub use super::{ - properties_panel::*, Panel, PanelSide, PanelWidget, PropertiesPanel, ShareableWidget, - ShowError, ShowResult, ShowSharedWidget, Tile, TryShowWidgetEntity, TryShowWidgetWorld, - Widget, WidgetSystem, - }; - pub use bevy::ecs::{ - system::{SystemParam, SystemState}, - world::World, - }; - pub use bevy_egui::egui::Ui; -} - -/// To create a panel widget (a widget that renders itself directly to one of -/// the egui side panels), add this component to an entity. -#[derive(Component)] -pub struct PanelWidget { - inner: Option>, -} - -impl PanelWidget { - pub fn new>(system: S, world: &mut World) -> Self { - let mut system = Box::new(IntoSystem::into_system(system)); - system.initialize(world); - Self { - inner: Some(system), - } - } -} - -fn site_ui_layout( +/// This system renders all UI panels in the application and makes sure that the +/// UI rendering works correctly with the picking system, and any other systems +/// as needed. +pub fn site_ui_layout( world: &mut World, panel_widgets: &mut QueryState<(Entity, &mut PanelWidget)>, egui_context_state: &mut SystemState, ) { - let mut panels: SmallVec<[_; 16]> = panel_widgets - .iter_mut(world) - .map(|(entity, mut widget)| { - ( - entity, - widget - .inner - .take() - .expect("Inner system of PanelWidget is missing"), - ) - }) - .collect(); - - for (e, inner) in &mut panels { - inner.run(*e, world); - inner.apply_deferred(world); - } - - for (e, inner) in panels { - if let Some(mut widget) = world.get_mut::(e) { - let _ = widget.inner.insert(inner); - } - } + render_panels(world, panel_widgets, egui_context_state); let mut egui_context = egui_context_state.get_mut(world); let ctx = egui_context.ctx_mut(); @@ -482,143 +411,6 @@ fn site_ui_layout( } } -#[derive(Clone, Copy, Debug, Component)] -pub enum PanelSide { - Top, - Bottom, - Left, - Right, -} - -pub enum EguiPanel { - Vertical(egui::SidePanel), - Horizontal(egui::TopBottomPanel), -} - -impl EguiPanel { - pub fn map_vertical(self, f: impl FnOnce(egui::SidePanel) -> egui::SidePanel) -> Self { - match self { - Self::Vertical(panel) => Self::Vertical(f(panel)), - other => other, - } - } - - pub fn map_horizontal( - self, - f: impl FnOnce(egui::TopBottomPanel) -> egui::TopBottomPanel, - ) -> Self { - match self { - Self::Horizontal(panel) => Self::Horizontal(f(panel)), - other => other, - } - } - - pub fn show( - self, - ctx: &egui::Context, - add_content: impl FnOnce(&mut Ui) -> R, - ) -> egui::InnerResponse { - match self { - Self::Vertical(panel) => panel.show(ctx, add_content), - Self::Horizontal(panel) => panel.show(ctx, add_content), - } - } -} - -impl PanelSide { - /// Is the long direction of the panel horizontal - pub fn is_horizontal(&self) -> bool { - matches!(self, Self::Top | Self::Bottom) - } - - /// Is the long direction of the panel vertical - pub fn is_vertical(&self) -> bool { - matches!(self, Self::Left | Self::Right) - } - - /// Align the Ui to line up with the long direction of the panel - pub fn align(self, ui: &mut Ui, f: impl FnOnce(&mut Ui) -> R) -> egui::InnerResponse { - if self.is_horizontal() { - ui.horizontal(f) - } else { - ui.vertical(f) - } - } - - /// Align the Ui to run orthogonal to long direction of the panel, - /// i.e. the Ui will run along the short direction of the panel. - pub fn orthogonal( - self, - ui: &mut Ui, - f: impl FnOnce(&mut Ui) -> R, - ) -> egui::InnerResponse { - if self.is_horizontal() { - ui.vertical(f) - } else { - ui.horizontal(f) - } - } - - pub fn get_panel(self) -> EguiPanel { - match self { - Self::Left => EguiPanel::Vertical(egui::SidePanel::left("left_panel")), - Self::Right => EguiPanel::Vertical(egui::SidePanel::right("right_panel")), - Self::Top => EguiPanel::Horizontal(egui::TopBottomPanel::top("top_panel")), - Self::Bottom => EguiPanel::Horizontal(egui::TopBottomPanel::bottom("bottom_panel")), - } - } -} - -/// Reusable widget that defines a panel with "tiles" where each tile is a child widget. -pub fn tile_panel_widget( - In(panel): In, - world: &mut World, - egui_contexts: &mut SystemState, -) { - let children: Option> = world - .get::(panel) - .map(|children| children.iter().copied().collect()); - - let Some(children) = children else { - return; - }; - if children.is_empty() { - // Do not even begin to create a panel if there are no children to render - return; - } - - let Some(side) = world.get::(panel) else { - error!("Side component missing for tile_panel_widget {panel:?}"); - return; - }; - - let side = *side; - let ctx = egui_contexts.get_mut(world).ctx_mut().clone(); - side.get_panel() - .map_vertical(|panel| { - // TODO(@mxgrey): Make this configurable via a component - panel.resizable(true).default_width(300.0) - }) - .show(&ctx, |ui| { - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - for child in children { - let tile = Tile { - id: child, - panel: side, - }; - if let Err(err) = world.try_show_in(child, tile, ui) { - error!( - "Could not render child widget {child:?} in \ - tile panel {panel:?} on side {side:?}: {err:?}" - ); - } - } - }); - }); -} - fn init_ui_style(mut egui_context: EguiContexts) { // I think the default egui dark mode text color is too dim, so this changes // it to a brighter white. @@ -626,5 +418,3 @@ fn init_ui_style(mut egui_context: EguiContexts) { visuals.override_text_color = Some(egui::Color32::from_rgb(250, 250, 250)); egui_context.ctx_mut().set_visuals(visuals); } - -pub struct PropertiesTilePlugin {} diff --git a/rmf_site_editor/src/widgets/move_layer.rs b/rmf_site_editor/src/widgets/move_layer.rs index 4abe6dcb..c85940dd 100644 --- a/rmf_site_editor/src/widgets/move_layer.rs +++ b/rmf_site_editor/src/widgets/move_layer.rs @@ -22,6 +22,7 @@ use crate::{ use bevy::prelude::*; use bevy_egui::egui::{ImageButton, Ui}; +/// A widget that helps move layers up or down in their ranking. pub struct MoveLayer<'a, 'w, T: Component> { entity: Entity, rank_events: &'a mut EventWriter<'w, ChangeRank>, diff --git a/rmf_site_editor/src/widgets/panel.rs b/rmf_site_editor/src/widgets/panel.rs new file mode 100644 index 00000000..ece27aad --- /dev/null +++ b/rmf_site_editor/src/widgets/panel.rs @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::{prelude::*, ecs::system::{BoxedSystem, SystemState}}; +use bevy_egui::{EguiContexts, egui::{self, Ui}}; +use smallvec::SmallVec; + +/// To create a panel widget (a widget that renders itself directly to one of +/// the egui side or top/bottom panels), add this component to an entity. +/// +/// Use the context field of the input to create a panel with one of the following: +/// - [`EguiPanel::show`] +/// - [`egui::SidePanel::left`] +/// - [`egui::SidePanel::right`] +/// - [`egui::TopBottomPanel::top`] +/// - [`egui::TopBottomPanel::bottom`] +#[derive(Component)] +pub struct PanelWidget { + inner: Option>, +} + +/// Input provided to panel widgets. +pub struct PanelWidgetInput { + /// The entity of the panel widget. + pub id: Entity, + /// The context that the panel should use for rendering. + pub context: egui::Context, +} + +impl PanelWidget { + /// Pass in a system that takes takes [`PanelWidgetInput`] as its input parameter. + pub fn new>(system: S, world: &mut World) -> Self { + let mut system = Box::new(IntoSystem::into_system(system)); + system.initialize(world); + Self { + inner: Some(system), + } + } +} + +/// This function can be used to render all panels in an application, either by +/// adding this function to a schedule as a system or by calling it from inside +/// of an exclusive system. Note that this is automatically run by +/// [`site_ui_layout`][1] so there is no need to use this function yourself +/// unless you are not using the [`StandardUiLayout`][1]. +/// +/// [1]: crate::widgets::site_ui_layout +/// [2]: crate::widgets::StandardUiLayout +pub fn render_panels( + world: &mut World, + panel_widgets: &mut QueryState<(Entity, &mut PanelWidget)>, + egui_contexts: &mut SystemState, +) { + let context = egui_contexts.get_mut(world).ctx_mut().clone(); + let mut panels: SmallVec<[_; 16]> = panel_widgets + .iter_mut(world) + .map(|(entity, mut widget)| { + ( + entity, + widget + .inner + .take() + .expect("Inner system of PanelWidget is missing"), + ) + }) + .collect(); + + + for (e, inner) in &mut panels { + inner.run(PanelWidgetInput{ id: *e, context: context.clone() }, world); + inner.apply_deferred(world); + } + + for (e, inner) in panels { + if let Some(mut widget) = world.get_mut::(e) { + let _ = widget.inner.insert(inner); + } + } +} + +/// Indicate which side a panel is on +#[derive(Clone, Copy, Debug, Component)] +pub enum PanelSide { + Top, + Bottom, + Left, + Right, +} + +/// Wrapper to hold either a vertical or horizontal egui panel +pub enum EguiPanel { + Vertical(egui::SidePanel), + Horizontal(egui::TopBottomPanel), +} + +impl EguiPanel { + /// Modify this panel if it's a vertical panel + pub fn map_vertical(self, f: impl FnOnce(egui::SidePanel) -> egui::SidePanel) -> Self { + match self { + Self::Vertical(panel) => Self::Vertical(f(panel)), + other => other, + } + } + + /// Modify this panel if it's a horizontal panel + pub fn map_horizontal( + self, + f: impl FnOnce(egui::TopBottomPanel) -> egui::TopBottomPanel, + ) -> Self { + match self { + Self::Horizontal(panel) => Self::Horizontal(f(panel)), + other => other, + } + } + + /// Display something in this panel. + pub fn show( + self, + ctx: &egui::Context, + add_content: impl FnOnce(&mut Ui) -> R, + ) -> egui::InnerResponse { + match self { + Self::Vertical(panel) => panel.show(ctx, add_content), + Self::Horizontal(panel) => panel.show(ctx, add_content), + } + } +} + +impl PanelSide { + /// Is the long direction of the panel horizontal + pub fn is_horizontal(&self) -> bool { + matches!(self, Self::Top | Self::Bottom) + } + + /// Is the long direction of the panel vertical + pub fn is_vertical(&self) -> bool { + matches!(self, Self::Left | Self::Right) + } + + /// Align the Ui to line up with the long direction of the panel + pub fn align(self, ui: &mut Ui, f: impl FnOnce(&mut Ui) -> R) -> egui::InnerResponse { + if self.is_horizontal() { + ui.horizontal(f) + } else { + ui.vertical(f) + } + } + + /// Align the Ui to run orthogonal to long direction of the panel, + /// i.e. the Ui will run along the short direction of the panel. + pub fn orthogonal( + self, + ui: &mut Ui, + f: impl FnOnce(&mut Ui) -> R, + ) -> egui::InnerResponse { + if self.is_horizontal() { + ui.vertical(f) + } else { + ui.horizontal(f) + } + } + + /// Get the egui panel that is associated with this panel type. + pub fn get_panel(self) -> EguiPanel { + match self { + Self::Left => EguiPanel::Vertical(egui::SidePanel::left("left_panel")), + Self::Right => EguiPanel::Vertical(egui::SidePanel::right("right_panel")), + Self::Top => EguiPanel::Horizontal(egui::TopBottomPanel::top("top_panel")), + Self::Bottom => EguiPanel::Horizontal(egui::TopBottomPanel::bottom("bottom_panel")), + } + } +} diff --git a/rmf_site_editor/src/widgets/panel_of_tiles.rs b/rmf_site_editor/src/widgets/panel_of_tiles.rs new file mode 100644 index 00000000..ed14d416 --- /dev/null +++ b/rmf_site_editor/src/widgets/panel_of_tiles.rs @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::prelude::*; +use bevy_egui::egui; +use smallvec::SmallVec; +use crate::widgets::prelude::*; + +/// Input type for [`WidgetSystem`]s that can be put into a "Panel of Tiles" +/// widget, such as the [`PropertiesPanel`]. See [`PropertiesTilePlugin`] for a +/// usage example. +pub struct Tile { + /// The entity of the tile widget which is being rendered. This lets you + /// store additional component data inside the entity which may be relevant + /// to your widget. + pub id: Entity, + /// What kind of panel is this tile inside of. Use this if you want your + /// widget layout to be different based on what kind of panel it was placed + /// in. + pub panel: PanelSide, +} + +/// Reusable widget that defines a panel with "tiles" where each tile is a child widget. +pub fn show_panel_of_tiles( + In(PanelWidgetInput { id, context }): In, + world: &mut World, +) { + let children: Option> = world + .get::(id) + .map(|children| children.iter().copied().collect()); + + let Some(children) = children else { + return; + }; + if children.is_empty() { + // Do not even begin to create a panel if there are no children to render + return; + } + + let Some(side) = world.get::(id) else { + error!("Side component missing for panel_of_tiles_widget {id:?}"); + return; + }; + + let side = *side; + side.get_panel() + .map_vertical(|panel| { + // TODO(@mxgrey): Make this configurable via a component + panel.resizable(true).default_width(300.0) + }) + .show(&context, |ui| { + egui::ScrollArea::both() + .auto_shrink([false, false]) + .show(ui, |ui| { + for child in children { + let tile = Tile { + id: child, + panel: side, + }; + if let Err(err) = world.try_show_in(child, tile, ui) { + error!( + "Could not render child widget {child:?} in \ + tile panel {id:?} on side {side:?}: {err:?}" + ); + } + } + }); + }); +} diff --git a/rmf_site_editor/src/widgets/properties_panel.rs b/rmf_site_editor/src/widgets/properties_panel.rs index 51127286..5fda4d95 100644 --- a/rmf_site_editor/src/widgets/properties_panel.rs +++ b/rmf_site_editor/src/widgets/properties_panel.rs @@ -15,9 +15,104 @@ * */ -use crate::widgets::{tile_panel_widget, PanelSide, PanelWidget, Tile, Widget, WidgetSystem}; +use crate::widgets::{ + show_panel_of_tiles, + ViewLevelsPlugin, ViewNavGraphsPlugin, ViewLayersPlugin, + StandardInspectorPlugin, CreationPlugin, ViewGroupsPlugin, ViewLightsPlugin, + ViewOccupancyPlugin, BuildingPreviewPlugin, PanelSide, PanelWidget, Tile, + Widget, WidgetSystem, +}; use bevy::prelude::*; +/// This plugins produces the standard properties panel. This is the panel which +/// includes widgets to display and edit all the properties in a site that we +/// expect are needed by common use cases of the editor. +#[derive(Default)] +pub struct StandardPropertiesPanelPlugin {} + +impl Plugin for StandardPropertiesPanelPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + PropertiesPanelPlugin::new(PanelSide::Right), + ViewLevelsPlugin::default(), + ViewNavGraphsPlugin::default(), + ViewLayersPlugin::default(), + StandardInspectorPlugin::default(), + CreationPlugin::default(), + ViewGroupsPlugin::default(), + ViewLightsPlugin::default(), + ViewOccupancyPlugin::default(), + BuildingPreviewPlugin::default(), + )); + } +} + +/// Use this plugin to add a single tile into the properties panel. +/// +/// ```no_run +/// use bevy::prelude::{App, Query, Entity, Res}; +/// use librmf_site_editor::{ +/// SiteEditor, workspace::CurrentWorkspace, +/// site::NameOfSite, +/// widgets::prelude::*, +/// }; +/// +/// #[derive(SystemParam)] +/// pub struct HelloSiteWidget<'w, 's> { +/// sites: Query<'w, 's, &'static NameOfSite>, +/// current: Res<'w, CurrentWorkspace>, +/// } +/// +/// impl<'w, 's> WidgetSystem for HelloSiteWidget<'w, 's> { +/// fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { +/// let mut params = state.get_mut(world); +/// if let Some(name) = params.current.root.map(|e| params.sites.get(e).ok()).flatten() { +/// ui.add_space(20.0); +/// ui.heading(format!("Hello, {}!", name.0)); +/// } +/// } +/// } +/// +/// fn main() { +/// let mut app = App::new(); +/// app.add_plugins(( +/// SiteEditor, +/// PropertiesTilePlugin::::new(), +/// )); +/// +/// app.run(); +/// } +/// ``` +pub struct PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + _ignore: std::marker::PhantomData, +} + +impl PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + pub fn new() -> Self { + Self { + _ignore: Default::default(), + } + } +} + +impl Plugin for PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + fn build(&self, app: &mut App) { + let widget = Widget::::new::(&mut app.world); + let properties_panel = app.world.resource::().id; + app.world.spawn(widget).set_parent(properties_panel); + } +} + +/// Get the ID of the properties panel. #[derive(Resource)] pub struct PropertiesPanel { side: PanelSide, @@ -34,6 +129,9 @@ impl PropertiesPanel { } } +/// This plugin builds a properties panel for the editor. It is usually recommended +/// to use [`StandardPropertiesPanelPlugin`] unless you need very specific +/// customization of the properties panel. pub struct PropertiesPanelPlugin { side: PanelSide, } @@ -52,7 +150,7 @@ impl Default for PropertiesPanelPlugin { impl Plugin for PropertiesPanelPlugin { fn build(&self, app: &mut App) { - let widget = PanelWidget::new(tile_panel_widget, &mut app.world); + let widget = PanelWidget::new(show_panel_of_tiles, &mut app.world); let id = app.world.spawn((widget, self.side)).id(); app.world.insert_resource(PropertiesPanel { side: self.side, @@ -60,33 +158,3 @@ impl Plugin for PropertiesPanelPlugin { }); } } - -/// Create a plugin for a single tile in the properties panel -pub struct PropertiesTilePlugin -where - W: WidgetSystem + 'static + Send + Sync, -{ - _ignore: std::marker::PhantomData, -} - -impl PropertiesTilePlugin -where - W: WidgetSystem + 'static + Send + Sync, -{ - pub fn new() -> Self { - Self { - _ignore: Default::default(), - } - } -} - -impl Plugin for PropertiesTilePlugin -where - W: WidgetSystem + 'static + Send + Sync, -{ - fn build(&self, app: &mut App) { - let widget = Widget::::new::(&mut app.world); - let properties_panel = app.world.resource::().id; - app.world.spawn(widget).set_parent(properties_panel); - } -} diff --git a/rmf_site_editor/src/widgets/selector_widget.rs b/rmf_site_editor/src/widgets/selector_widget.rs index 151196dd..5db89737 100644 --- a/rmf_site_editor/src/widgets/selector_widget.rs +++ b/rmf_site_editor/src/widgets/selector_widget.rs @@ -23,6 +23,7 @@ use crate::{ use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{Button, Ui}; +/// A widget that can be used to select entities. #[derive(SystemParam)] pub struct SelectorWidget<'w, 's> { pub site_id: Query<'w, 's, &'static SiteID>, diff --git a/rmf_site_editor/src/widgets/view_groups.rs b/rmf_site_editor/src/widgets/view_groups.rs index 69af2c49..b495462a 100644 --- a/rmf_site_editor/src/widgets/view_groups.rs +++ b/rmf_site_editor/src/widgets/view_groups.rs @@ -23,6 +23,7 @@ use crate::{ use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{Button, CollapsingHeader, Ui}; +/// Add a widget for viewing different kinds of groups. #[derive(Default)] pub struct ViewGroupsPlugin {} @@ -54,7 +55,7 @@ pub struct ViewGroupsEvents<'w, 's> { } impl<'w, 's> WidgetSystem for ViewGroups<'w, 's> { - fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { let mut params = state.get_mut(world); if *params.app_state.get() != AppState::SiteEditor { return; diff --git a/rmf_site_editor/src/widgets/view_layers.rs b/rmf_site_editor/src/widgets/view_layers.rs index 862af512..3b159f7d 100644 --- a/rmf_site_editor/src/widgets/view_layers.rs +++ b/rmf_site_editor/src/widgets/view_layers.rs @@ -29,6 +29,7 @@ use crate::{ use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{Button, CollapsingHeader, DragValue, ScrollArea, Ui}; +/// Add a widget for viewing a list of layers #[derive(Default)] pub struct ViewLayersPlugin {} diff --git a/rmf_site_editor/src/widgets/view_levels.rs b/rmf_site_editor/src/widgets/view_levels.rs index 0b4730d5..ef7ebb84 100644 --- a/rmf_site_editor/src/widgets/view_levels.rs +++ b/rmf_site_editor/src/widgets/view_levels.rs @@ -27,6 +27,7 @@ use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{CollapsingHeader, DragValue, ImageButton, Ui}; use std::cmp::{Ordering, Reverse}; +/// Add a plugin for viewing and editing a list of all levels #[derive(Default)] pub struct ViewLevelsPlugin {} diff --git a/rmf_site_editor/src/widgets/view_lights.rs b/rmf_site_editor/src/widgets/view_lights.rs index 6423cdba..aa2df1c4 100644 --- a/rmf_site_editor/src/widgets/view_lights.rs +++ b/rmf_site_editor/src/widgets/view_lights.rs @@ -40,6 +40,7 @@ use rfd::AsyncFileDialog; use std::cmp::Reverse; use std::collections::BTreeMap; +/// Add a plugin for viewing and editing a list of all lights #[derive(Default)] pub struct ViewLightsPlugin {} diff --git a/rmf_site_editor/src/widgets/view_nav_graphs.rs b/rmf_site_editor/src/widgets/view_nav_graphs.rs index 7a423eab..d05744ae 100644 --- a/rmf_site_editor/src/widgets/view_nav_graphs.rs +++ b/rmf_site_editor/src/widgets/view_nav_graphs.rs @@ -35,6 +35,7 @@ use futures_lite::future; #[cfg(not(target_arch = "wasm32"))] use rfd::AsyncFileDialog; +/// Add a widget for viewing and editing navigation graphs. #[derive(Default)] pub struct ViewNavGraphsPlugin {} diff --git a/rmf_site_editor/src/widgets/view_occupancy.rs b/rmf_site_editor/src/widgets/view_occupancy.rs index 44493874..2580c3a8 100644 --- a/rmf_site_editor/src/widgets/view_occupancy.rs +++ b/rmf_site_editor/src/widgets/view_occupancy.rs @@ -19,6 +19,8 @@ use crate::{occupancy::CalculateGrid, widgets::prelude::*, AppState}; use bevy::prelude::*; use bevy_egui::egui::{CollapsingHeader, DragValue, Ui}; +/// Add a widget that provides a button for producing an occupancy grid +/// visualization. #[derive(Default)] pub struct ViewOccupancyPlugin {}