entity creator window

- replaces chained context menu with a dedicated window
- old functionality preserved, new will be added in following commits
This commit is contained in:
Dmitry Stepanov
2026-09-10 23:00:25 +03:00
parent 0e245ad3f5
commit 477f0366e5
9 changed files with 417 additions and 560 deletions
+41 -24
View File
@@ -57,7 +57,7 @@ pub mod world;
pub use fyrox;
use crate::world::WorldViewerItemContextMenu;
use crate::world::create::EntityCreator;
use crate::{
asset::{item::AssetItem, AssetBrowser},
audio::{preview::AudioPreviewPanel, AudioPanel},
@@ -658,6 +658,7 @@ pub struct Editor {
pub engine: Engine,
pub plugins: EditorPluginsContainer,
pub focused: bool,
pub entity_creator: EntityCreator,
pub update_loop_state: UpdateLoopState,
pub is_suspended: bool,
pub scene_node_context_menu: Rc<RefCell<SceneNodeContextMenu>>,
@@ -836,15 +837,8 @@ impl Editor {
)
.build(ctx);
let navmesh_panel = NavmeshPanel::new(scene_viewer.frame(), ctx, message_sender.clone());
let scene_node_context_menu = Rc::new(RefCell::new(SceneNodeContextMenu::new(
&engine.serialization_context,
&engine.widget_constructors,
ctx,
)));
let widget_context_menu = Rc::new(RefCell::new(WidgetContextMenu::new(
&engine.widget_constructors,
ctx,
)));
let scene_node_context_menu = Rc::new(RefCell::new(SceneNodeContextMenu::new(ctx)));
let widget_context_menu = Rc::new(RefCell::new(WidgetContextMenu::new(ctx)));
let world_outliner = WorldViewer::new(ctx, message_sender.clone(), &settings);
let command_stack_viewer = CommandStackViewer::new(ctx, message_sender.clone());
let log = LogPanel::new(
@@ -1017,6 +1011,7 @@ impl Editor {
)
.with_buttons(MessageBoxButtons::Ok)
.build(ctx);
let entity_creator = EntityCreator::new(ctx);
let save_scene_dialog = SaveSceneConfirmationDialog::new(ctx);
@@ -1086,6 +1081,7 @@ impl Editor {
// was created. So we must assume that the editor is focused by default, otherwise editor's thread
// will sleep forever and the window won't come up.
focused: true,
entity_creator,
update_loop_state: UpdateLoopState::default(),
is_suspended: false,
scene_node_context_menu,
@@ -1482,6 +1478,13 @@ impl Editor {
);
if let Some(game_scene) = current_scene_entry.controller.downcast_mut::<GameScene>() {
self.entity_creator.handle_ui_message_with_game_scene(
game_scene,
engine,
message,
&current_scene_entry.selection,
&self.message_sender,
);
self.particle_system_control_panel.handle_ui_message(
message,
&current_scene_entry.selection,
@@ -1560,6 +1563,14 @@ impl Editor {
self.light_panel
.handle_ui_message(message, game_scene, engine);
} else if let Some(ui_scene) = current_scene_entry.controller.downcast_mut::<UiScene>() {
self.entity_creator.handle_ui_message_with_ui_scene(
ui_scene,
engine,
message,
&current_scene_entry.selection,
&self.message_sender,
);
self.bbcode_panel.handle_ui_message(
message,
&current_scene_entry.selection,
@@ -2232,7 +2243,9 @@ impl Editor {
}
fn on_scene_changed(&mut self, old_selection: Selection) {
let ui = &self.engine.user_interfaces.first();
self.refresh_entity_creator();
let ui = self.engine.user_interfaces.first_mut();
let entry = self.scenes.current_scene_entry_ref();
if let Some(game_scene) = entry.controller.downcast_ref::<GameScene>() {
self.world_viewer.item_context_menu = Some(self.scene_node_context_menu.clone());
@@ -2247,7 +2260,6 @@ impl Editor {
self.message_sender
.send(Message::SelectionChanged { old_selection });
self.menu.on_scene_changed(&*entry.controller, ui);
self.world_viewer.clear(ui);
@@ -2685,6 +2697,10 @@ impl Editor {
)
}
}
Message::OpenEntityCreator(mode) => {
self.entity_creator
.open(mode, self.engine.user_interfaces.first());
}
Message::SetAssetBrowserCurrentDir(path) => {
self.asset_browser
.request_current_path(path, self.engine.user_interfaces.first());
@@ -2841,21 +2857,22 @@ impl Editor {
);
}
fn on_game_plugin_added(&mut self) {
let mut widget_context_menu = self.widget_context_menu.borrow_mut();
let mut scene_node_context_menu = self.scene_node_context_menu.borrow_mut();
for menu in [
&mut *widget_context_menu as &mut dyn WorldViewerItemContextMenu,
&mut *scene_node_context_menu as &mut dyn WorldViewerItemContextMenu,
] {
menu.on_plugin_added(
&self.engine.serialization_context,
&self.engine.widget_constructors,
self.engine.user_interfaces.first_mut(),
);
fn refresh_entity_creator(&mut self) {
let ui = self.engine.user_interfaces.first_mut();
let entry = self.scenes.current_scene_entry_ref();
if entry.controller.downcast_ref::<GameScene>().is_some() {
self.entity_creator
.on_constructors_changed(ui, &self.engine.serialization_context.node_constructors);
} else if entry.controller.downcast_ref::<UiScene>().is_some() {
self.entity_creator
.on_constructors_changed(ui, &self.engine.widget_constructors);
}
}
fn on_game_plugin_added(&mut self) {
self.refresh_entity_creator();
}
pub fn add_game_plugin<P>(&mut self, plugin: P)
where
P: Plugin + 'static,
-209
View File
@@ -1,209 +0,0 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
command::{Command, CommandGroup},
fyrox::{
core::{algebra::Vector3, pool::Handle},
engine::{Engine, SerializationContext},
fxhash::FxHashMap,
gui::{
menu::MenuItemMessage, message::UiMessage, widget::WidgetMessage, BuildContext,
UserInterface,
},
scene::node::Node,
},
menu::{create_menu_item, create_root_menu_item, ui::UiMenu},
message::MessageSender,
scene::{
commands::graph::{AddNodeCommand, MoveNodeCommand},
controller::SceneController,
GameScene, Selection,
},
ui_scene::UiScene,
Mode,
};
use fyrox::core::log::Log;
use fyrox::core::uuid::{uuid, Uuid};
use fyrox::graph::constructor::{VariantConstructor, VariantResult};
use fyrox::gui::constructor::WidgetConstructorContainer;
use fyrox::gui::menu::{MenuItem, SortingPredicate};
use fyrox::scene::graph::Graph;
pub struct CreateEntityRootMenu {
pub menu: Handle<MenuItem>,
pub sub_menus: CreateEntityMenu,
}
impl CreateEntityRootMenu {
pub const CREATE: Uuid = uuid!("edc4d319-2e8e-4173-8b35-73ff4982f380");
pub fn new(
serialization_context: &SerializationContext,
widget_constructors_container: &WidgetConstructorContainer,
ctx: &mut BuildContext,
) -> Self {
let sub_menus =
CreateEntityMenu::new(serialization_context, widget_constructors_container, ctx);
let menu = create_root_menu_item("Create", Self::CREATE, sub_menus.root_items.clone(), ctx);
ctx.inner().send(
menu,
MenuItemMessage::Sort(SortingPredicate::sort_by_text()),
);
Self { menu, sub_menus }
}
pub fn handle_ui_message(
&mut self,
message: &UiMessage,
sender: &MessageSender,
controller: &mut dyn SceneController,
selection: &Selection,
engine: &mut Engine,
) {
if let Some(node) = self
.sub_menus
.handle_ui_message(message, sender, controller, selection, engine)
{
if let Some(game_scene) = controller.downcast_ref::<GameScene>() {
let scene = &engine.scenes[game_scene.scene];
let position = game_scene
.camera_controller
.placement_position(&scene.graph, Default::default());
let node_handle = scene.graph.generate_free_handles(1)[0];
sender.do_command(CommandGroup::from(vec![
Command::new(AddNodeCommand::new(node, Handle::NONE, true)),
Command::new(MoveNodeCommand::new(
node_handle,
Vector3::default(),
position,
)),
]));
}
}
}
pub fn on_scene_changed(&self, controller: &dyn SceneController, ui: &UserInterface) {
self.sub_menus.on_scene_changed(controller, ui);
}
pub fn on_mode_changed(&mut self, ui: &UserInterface, mode: &Mode) {
ui.send(self.menu, WidgetMessage::Enabled(mode.is_edit()));
}
}
pub struct CreateEntityMenu {
ui_menu: UiMenu,
pub root_items: Vec<Handle<MenuItem>>,
constructor_views: FxHashMap<Handle<MenuItem>, VariantConstructor<Node, Graph>>,
}
impl CreateEntityMenu {
pub fn new(
serialization_context: &SerializationContext,
widget_constructor_container: &WidgetConstructorContainer,
ctx: &mut BuildContext,
) -> Self {
let ui_menu = UiMenu::new(widget_constructor_container, "UI", ctx);
let mut root_items = vec![ui_menu.menu];
let mut groups = FxHashMap::default();
let mut constructor_views = FxHashMap::default();
let constructors = serialization_context.node_constructors.map();
for constructor in constructors.values() {
for variant in constructor.variants.iter() {
let item = create_menu_item(&variant.name, Uuid::new_v4(), vec![], ctx);
constructor_views.insert(item, variant.constructor.clone());
if constructor.group.is_empty() {
root_items.push(item);
} else {
let group = *groups.entry(constructor.group).or_insert_with(|| {
let group =
create_menu_item(constructor.group, Uuid::new_v4(), vec![], ctx);
root_items.push(group);
group
});
ctx.inner().send(group, MenuItemMessage::AddItem(item))
}
}
}
for root_item in root_items.iter() {
ctx.inner().send(
*root_item,
MenuItemMessage::Sort(SortingPredicate::sort_by_text()),
)
}
Self {
ui_menu,
constructor_views,
root_items,
}
}
pub fn on_scene_changed(&self, controller: &dyn SceneController, ui: &UserInterface) {
let is_ui_scene = controller.downcast_ref::<UiScene>().is_some();
ui.send(self.ui_menu.menu, WidgetMessage::Enabled(is_ui_scene));
for widget in self.root_items.iter() {
if *widget == self.ui_menu.menu {
continue;
}
ui.send(*widget, WidgetMessage::Enabled(!is_ui_scene));
}
}
pub fn handle_ui_message(
&mut self,
message: &UiMessage,
sender: &MessageSender,
controller: &mut dyn SceneController,
selection: &Selection,
engine: &mut Engine,
) -> Option<Node> {
if let Some(ui_scene) = controller.downcast_mut::<UiScene>() {
self.ui_menu
.handle_ui_message(sender, message, ui_scene, selection);
} else if let Some(game_scene) = controller.downcast_mut::<GameScene>() {
let graph = &mut engine.scenes[game_scene.scene].graph;
if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if let Some(constructor) = self
.constructor_views
.get(&message.destination().to_variant())
{
if let VariantResult::Owned(node) = constructor(graph) {
return Some(node);
} else {
Log::err("Unsupported");
}
}
}
}
None
}
}
+9 -37
View File
@@ -22,45 +22,38 @@ use crate::{
asset::preview::cache::IconRequest,
export::ExportWindow,
fyrox::{
asset::manager::ResourceManager,
core::uuid::Uuid,
core::{algebra::Vector2, pool::Handle},
gui::{
menu::{MenuBuilder, MenuItemBuilder, MenuItemContent},
file_browser::FileType,
image::{Image, ImageBuilder},
menu::{MenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent},
message::UiMessage,
texture::TextureResource,
widget::{WidgetBuilder, WidgetMessage},
window::Window,
BuildContext, Thickness, UserInterface,
},
},
menu::{
create::CreateEntityRootMenu, edit::EditMenu, file::FileMenu, help::HelpMenu,
utils::UtilsMenu, view::ViewMenu,
},
menu::{edit::EditMenu, file::FileMenu, help::HelpMenu, utils::UtilsMenu, view::ViewMenu},
message::MessageSender,
scene::{container::EditorSceneEntry, controller::SceneController},
scene::container::EditorSceneEntry,
settings::Settings,
stats::StatisticsWindow,
Engine, Mode, SceneSettingsWindow,
};
use fyrox::asset::manager::ResourceManager;
use fyrox::core::uuid::Uuid;
use fyrox::gui::file_browser::FileType;
use fyrox::gui::image::{Image, ImageBuilder};
use fyrox::gui::menu::MenuItem;
use fyrox::gui::texture::TextureResource;
use fyrox::gui::window::Window;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
pub mod create;
pub mod edit;
pub mod file;
pub mod help;
pub mod ui;
pub mod utils;
pub mod view;
pub struct Menu {
pub menu: Handle<fyrox::gui::menu::Menu>,
pub create_entity_menu: CreateEntityRootMenu,
pub edit_menu: EditMenu,
pub file_menu: FileMenu,
pub view_menu: ViewMenu,
@@ -160,11 +153,6 @@ impl Menu {
pub fn new(engine: &mut Engine, message_sender: MessageSender, settings: &Settings) -> Self {
let file_menu = FileMenu::new(engine, settings);
let ctx = &mut engine.user_interfaces.first_mut().build_ctx();
let create_entity_menu = CreateEntityRootMenu::new(
&engine.serialization_context,
&engine.widget_constructors,
ctx,
);
let edit_menu = EditMenu::new(ctx);
let view_menu = ViewMenu::new(ctx);
let utils_menu = UtilsMenu::new(ctx);
@@ -174,7 +162,6 @@ impl Menu {
.with_items(vec![
file_menu.menu,
edit_menu.menu,
create_entity_menu.menu,
view_menu.menu,
utils_menu.menu,
help_menu.menu,
@@ -183,7 +170,6 @@ impl Menu {
Self {
menu,
create_entity_menu,
edit_menu,
message_sender,
file_menu,
@@ -216,7 +202,6 @@ impl Menu {
self.file_menu.close_scene,
self.file_menu.save,
self.file_menu.save_as,
self.create_entity_menu.menu,
self.edit_menu.menu,
self.file_menu.open_scene_settings,
]
@@ -236,14 +221,6 @@ impl Menu {
ctx.engine,
);
self.create_entity_menu.handle_ui_message(
message,
&self.message_sender,
&mut *entry.controller,
&entry.selection,
ctx.engine,
);
self.utils_menu.handle_ui_message(
message,
&mut ctx.panels,
@@ -266,12 +243,7 @@ impl Menu {
self.help_menu.handle_ui_message(message);
}
pub fn on_scene_changed(&self, controller: &dyn SceneController, ui: &UserInterface) {
self.create_entity_menu.on_scene_changed(controller, ui);
}
pub fn on_mode_changed(&mut self, ui: &UserInterface, mode: &Mode) {
self.create_entity_menu.on_mode_changed(ui, mode);
self.edit_menu.on_mode_changed(ui, mode);
self.file_menu.on_mode_changed(ui, mode);
}
-115
View File
@@ -1,115 +0,0 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
fyrox::{
core::{log::Log, pool::Handle},
fxhash::FxHashMap,
graph::constructor::{VariantConstructor, VariantResult},
gui::{
constructor::WidgetConstructorContainer, menu::MenuItemMessage, message::UiMessage,
BuildContext, UiNode, UserInterface,
},
},
menu::create_menu_item,
message::MessageSender,
scene::Selection,
ui_scene::{commands::graph::AddWidgetCommand, UiScene},
};
use fyrox::core::uuid::{uuid, Uuid};
use fyrox::gui::menu::{MenuItem, SortingPredicate};
pub struct UiMenu {
pub menu: Handle<MenuItem>,
constructor_views: FxHashMap<Handle<MenuItem>, VariantConstructor<UiNode, UserInterface>>,
}
impl UiMenu {
pub const MENU: Uuid = uuid!("3a3f7035-529e-4d79-a413-a4d887d5e32e");
pub fn new(
constructors: &WidgetConstructorContainer,
name: &str,
ctx: &mut BuildContext,
) -> Self {
let mut root_items = vec![];
let mut groups = FxHashMap::default();
let mut constructor_views = FxHashMap::default();
let constructors = constructors.map();
for constructor in constructors.values() {
for variant in constructor.variants.iter() {
let item = create_menu_item(&variant.name, Uuid::new_v4(), vec![], ctx);
constructor_views.insert(item, variant.constructor.clone());
if constructor.group.is_empty() {
root_items.push(item);
} else {
let group = *groups.entry(constructor.group).or_insert_with(|| {
let group =
create_menu_item(constructor.group, Uuid::new_v4(), vec![], ctx);
root_items.push(group);
group
});
ctx.inner().send(group, MenuItemMessage::AddItem(item));
}
}
}
let menu = create_menu_item(name, Self::MENU, root_items.clone(), ctx);
for root_item in root_items.iter().chain(&[menu]) {
ctx.inner().send(
*root_item,
MenuItemMessage::Sort(SortingPredicate::sort_by_text()),
)
}
Self {
menu,
constructor_views,
}
}
pub fn handle_ui_message(
&mut self,
sender: &MessageSender,
message: &UiMessage,
scene: &mut UiScene,
selection: &Selection,
) {
if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if let Some(constructor) = self
.constructor_views
.get_mut(&message.destination().to_variant())
{
if let VariantResult::Handle(ui_node_handle) = constructor(&mut scene.ui) {
let sub_graph = scene.ui.take_reserve_sub_graph(ui_node_handle);
let parent = if let Some(selection) = selection.as_ui() {
selection.widgets.first().cloned().unwrap_or_default()
} else {
Handle::NONE
};
sender.do_command(AddWidgetCommand::new(sub_graph, parent, true));
} else {
Log::err("Unsupported");
}
}
}
}
}
+2
View File
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::world::create::EntityCreatorMode;
use crate::{
command::{Command, CommandGroup, CommandTrait},
fyrox::{
@@ -122,6 +123,7 @@ pub enum Message {
ViewSurfaceData(SurfaceResource),
SyncInteractionModes,
SetAssetBrowserCurrentDir(PathBuf),
OpenEntityCreator(EntityCreatorMode),
}
#[derive(Clone, Debug, Reflect)]
+20 -38
View File
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::world::create::EntityCreatorMode;
use crate::{
fyrox::{
asset::manager::ResourceManager,
@@ -25,19 +26,17 @@ use crate::{
pool::Handle,
uuid::{uuid, Uuid},
},
engine::SerializationContext,
graph::{NodeWrapper, SceneGraph},
gui::{
constructor::WidgetConstructorContainer,
menu::{ContextMenuBuilder, MenuItem, MenuItemMessage},
message::UiMessage,
popup::{Placement, PopupBuilder, PopupMessage},
stack_panel::{StackPanel, StackPanelBuilder},
stack_panel::StackPanelBuilder,
widget::{WidgetBuilder, WidgetMessage},
BuildContext, RcUiNodeHandle, UiNode, UserInterface,
BuildContext, RcUiNodeHandle, UiNode,
},
},
menu::{create_menu_item, create_menu_item_shortcut, ui::UiMenu},
menu::{create_menu_item, create_menu_item_shortcut},
message::MessageSender,
scene::{controller::SceneController, Selection},
ui_scene::{
@@ -54,36 +53,17 @@ pub struct WidgetContextMenu {
menu: RcUiNodeHandle,
delete_selection: Handle<MenuItem>,
copy_selection: Handle<MenuItem>,
widgets_menu: UiMenu,
create_child: Handle<MenuItem>,
placement_target: Handle<UiNode>,
paste: Handle<MenuItem>,
make_root: Handle<MenuItem>,
open_asset: Handle<MenuItem>,
stack_panel: Handle<StackPanel>,
}
impl WorldViewerItemContextMenu for WidgetContextMenu {
fn menu(&self) -> RcUiNodeHandle {
self.menu.clone()
}
fn on_plugin_added(
&mut self,
_serialization_context: &SerializationContext,
widget_constructors_container: &WidgetConstructorContainer,
ui: &mut UserInterface,
) {
ui.send(self.widgets_menu.menu, WidgetMessage::Remove);
self.widgets_menu = UiMenu::new(
widget_constructors_container,
"Create Child Widget",
&mut ui.build_ctx(),
);
ui.send(
self.widgets_menu.menu,
WidgetMessage::LinkWith(self.stack_panel.to_base()),
);
}
}
fn resource_path_of_first_selected_node(
@@ -110,21 +90,17 @@ impl WidgetContextMenu {
pub const DELETE_SELECTION: Uuid = uuid!("30eef2a7-9f12-4e64-9142-b604f25e9e06");
pub const COPY_SELECTION: Uuid = uuid!("0a2d10bc-de1e-4196-aba6-0a51c54eb238");
pub const PASTE_AS_CHILD: Uuid = uuid!("d3b86c8c-1efd-4917-9543-b0f5f32f8cbd");
pub const CREATE_CHILD_WIDGET: Uuid = uuid!("26b2ed1f-c4b8-4ec4-8112-9f29fc600101");
pub const MAKE_ROOT: Uuid = uuid!("968318f6-21c7-430f-a13d-36aefb61cde2");
pub const OPEN_ASSET: Uuid = uuid!("f3f7d0fa-e905-4371-8973-dfc1eb758e5a");
pub fn new(
widget_constructors_container: &WidgetConstructorContainer,
ctx: &mut BuildContext,
) -> Self {
pub fn new(ctx: &mut BuildContext) -> Self {
let delete_selection;
let copy_selection;
let paste;
let make_root;
let open_asset;
let widgets_menu = UiMenu::new(widget_constructors_container, "Create Child Widget", ctx);
let create_child;
let stack_panel;
let menu = ContextMenuBuilder::new(
PopupBuilder::new(WidgetBuilder::new().with_visibility(false))
@@ -172,7 +148,15 @@ impl WidgetContextMenu {
create_menu_item("Open Asset", Self::OPEN_ASSET, vec![], ctx);
open_asset
})
.with_child(widgets_menu.menu),
.with_child({
create_child = create_menu_item(
"Create Child Widget",
Self::CREATE_CHILD_WIDGET,
vec![],
ctx,
);
create_child
}),
)
.build(ctx);
stack_panel
@@ -183,7 +167,7 @@ impl WidgetContextMenu {
let menu = RcUiNodeHandle::new(menu, ctx.sender());
Self {
widgets_menu,
create_child,
menu,
delete_selection,
copy_selection,
@@ -191,7 +175,6 @@ impl WidgetContextMenu {
paste,
make_root,
open_asset,
stack_panel,
}
}
@@ -204,9 +187,6 @@ impl WidgetContextMenu {
sender: &MessageSender,
) {
if let Some(ui_scene) = controller.downcast_mut::<UiScene>() {
self.widgets_menu
.handle_ui_message(sender, message, ui_scene, editor_selection);
if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if message.destination() == self.delete_selection {
if let Some(ui_selection) = editor_selection.as_ui() {
@@ -247,6 +227,8 @@ impl WidgetContextMenu {
sender.send(Message::LoadScene(path));
}
}
} else if message.destination() == self.create_child {
sender.send(Message::OpenEntityCreator(EntityCreatorMode::CreateChild));
}
} else if let Some(PopupMessage::Placement(Placement::Cursor(target))) = message.data()
{
+327
View File
@@ -0,0 +1,327 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
command::{Command, CommandGroup},
fyrox::{
core::{algebra::Vector3, pool::Handle},
engine::Engine,
fxhash::FxHashMap,
graph::constructor::{ConstructorVariantId, GraphNodeConstructorContainer, VariantResult},
gui::{
button::{Button, ButtonBuilder, ButtonMessage},
grid::{Column, GridBuilder, Row},
list_view::{ListView, ListViewBuilder},
message::UiMessage,
scroll_viewer::ScrollViewerBuilder,
stack_panel::StackPanelBuilder,
text::TextBuilder,
tree::{Tree, TreeBuilder, TreeMessage, TreeRoot, TreeRootBuilder, TreeRootMessage},
widget::WidgetBuilder,
window::{Window, WindowAlignment, WindowBuilder, WindowMessage, WindowTitle},
BuildContext, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
},
scene::{graph::Graph, node::Node},
},
message::MessageSender,
scene::commands::graph::{
AddNodeCommand, LinkNodesCommand, MoveNodeCommand, ReplaceNodeCommand, SetGraphRootCommand,
},
scene::{GameScene, Selection},
ui_scene::{commands::graph::AddWidgetCommand, UiScene},
};
use fyrox::gui::widget::WidgetMessage;
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
pub enum EntityCreatorMode {
#[default]
CreateChild,
CreateParent,
CreateReplacement,
}
pub struct EntityCreator {
window: Handle<Window>,
recent_list: Handle<ListView>,
groups_tree: Handle<TreeRoot>,
items_map: FxHashMap<Handle<Tree>, ConstructorVariantId>,
create: Handle<Button>,
cancel: Handle<Button>,
selection: Option<ConstructorVariantId>,
mode: EntityCreatorMode,
}
fn make_tree_item(ui: &mut UserInterface, text: &str) -> Handle<Tree> {
let ctx = &mut ui.build_ctx();
TreeBuilder::new(WidgetBuilder::new())
.with_content(
TextBuilder::new(WidgetBuilder::new())
.with_text(text)
.build(ctx),
)
.build(ctx)
}
impl EntityCreator {
pub fn new(ctx: &mut BuildContext) -> Self {
let recent_list = ListViewBuilder::new(WidgetBuilder::new().on_column(0)).build(ctx);
let groups_tree = TreeRootBuilder::new(WidgetBuilder::new()).build(ctx);
let groups_scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new().on_column(1))
.with_content(groups_tree)
.build(ctx);
let create = ButtonBuilder::new(
WidgetBuilder::new()
.with_enabled(false)
.with_width(100.0)
.with_margin(Thickness::uniform(2.0)),
)
.with_text("Create")
.build(ctx);
let cancel = ButtonBuilder::new(
WidgetBuilder::new()
.with_width(100.0)
.with_margin(Thickness::uniform(2.0)),
)
.with_text("Cancel")
.build(ctx);
let buttons = StackPanelBuilder::new(
WidgetBuilder::new()
.with_height(28.0)
.with_horizontal_alignment(HorizontalAlignment::Right)
.on_row(1)
.with_child(create)
.with_child(cancel),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let grid = GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_child(recent_list)
.with_child(groups_scroll_viewer),
)
.add_column(Column::strict(200.0))
.add_column(Column::stretch())
.add_row(Row::stretch())
.build(ctx);
let content = GridBuilder::new(WidgetBuilder::new().with_child(grid).with_child(buttons))
.add_column(Column::stretch())
.add_row(Row::stretch())
.add_row(Row::auto())
.build(ctx);
let window = WindowBuilder::new(WidgetBuilder::new().with_width(500.0).with_height(600.0))
.with_content(content)
.open(false)
.with_title(WindowTitle::text("Entity Creator"))
.build(ctx);
Self {
window,
recent_list,
groups_tree,
items_map: FxHashMap::default(),
create,
cancel,
selection: None,
mode: EntityCreatorMode::default(),
}
}
pub fn open(&mut self, mode: EntityCreatorMode, ui: &UserInterface) {
self.mode = mode;
ui.send(
self.window,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
)
}
pub fn on_constructors_changed<N, C>(
&mut self,
ui: &mut UserInterface,
constructors: &GraphNodeConstructorContainer<N, C>,
) {
self.selection = None;
ui.send(self.create, WidgetMessage::Enabled(false));
ui.send(self.groups_tree, TreeRootMessage::Items(Vec::new()));
self.items_map.clear();
let constructors = constructors.map();
let mut groups = FxHashMap::default();
for (type_uuid, constructor) in constructors.iter() {
for (variant_index, variant) in constructor.variants.iter().enumerate() {
let item = make_tree_item(ui, &variant.name);
self.items_map
.insert(item, ConstructorVariantId::new(type_uuid, variant_index));
if constructor.group.is_empty() {
ui.send(self.groups_tree, TreeRootMessage::AddItem(item));
} else {
let group = *groups.entry(constructor.group).or_insert_with(|| {
let group = make_tree_item(ui, constructor.group);
ui.send(self.groups_tree, TreeRootMessage::AddItem(group));
group
});
ui.send(group, TreeMessage::AddItem(item))
}
}
}
}
fn handle_ui_message_internal<N, Ctx>(
&mut self,
constructors: &GraphNodeConstructorContainer<N, Ctx>,
message: &UiMessage,
ui: &UserInterface,
ctx: &mut Ctx,
) -> Option<VariantResult<N>> {
if let Some(TreeRootMessage::Select(selection)) = message.data_from(self.groups_tree) {
if let Some(first) = selection.first() {
let constructor_id = self.items_map.get(first);
if let Some(constructor_id) = constructor_id {
self.selection = Some(constructor_id.clone());
}
let can_create = constructor_id.is_some();
ui.send(self.create, WidgetMessage::Enabled(can_create));
}
} else if let Some(ButtonMessage::Click) = message.data_from(self.create) {
if let Some(constructor_id) = self.selection.as_ref() {
ui.send(self.window, WindowMessage::Close);
return constructors.try_create_variant(constructor_id, ctx);
}
} else if let Some(ButtonMessage::Click) = message.data_from(self.cancel) {
ui.send(self.window, WindowMessage::Close);
}
None
}
pub fn handle_ui_message_with_game_scene(
&mut self,
game_scene: &GameScene,
engine: &mut Engine,
message: &UiMessage,
editor_selection: &Selection,
sender: &MessageSender,
) {
let ui = engine.user_interfaces.first();
let graph = &mut engine.scenes[game_scene.scene].graph;
if let Some(VariantResult::Owned(node)) = self.handle_ui_message_internal::<Node, Graph>(
&engine.serialization_context.node_constructors,
message,
ui,
graph,
) {
match self.mode {
EntityCreatorMode::CreateChild => {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(parent) = graph_selection.nodes().first() {
sender.do_command(AddNodeCommand::new(node, *parent, true));
}
}
}
EntityCreatorMode::CreateParent => {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(first) = graph_selection.nodes().first() {
let scene = &engine.scenes[game_scene.scene];
let position = game_scene
.camera_controller
.placement_position(&scene.graph, *first);
let first_ref = &scene.graph[*first];
let parent = if first_ref.parent().is_some() {
first_ref.parent()
} else {
game_scene.scene_content_root
};
let new_parent_handle = scene.graph.generate_free_handles(1)[0];
let mut commands = CommandGroup::from(vec![
Command::new(AddNodeCommand::new(node, parent, true)),
Command::new(LinkNodesCommand::new(*first, new_parent_handle)),
]);
if parent == game_scene.scene_content_root {
commands.push(MoveNodeCommand::new(
new_parent_handle,
Vector3::default(),
position,
));
}
if *first == game_scene.scene_content_root {
commands.push(SetGraphRootCommand {
root: new_parent_handle,
link_scheme: Default::default(),
})
}
sender.do_command(commands);
}
}
}
EntityCreatorMode::CreateReplacement => {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(first) = graph_selection.nodes().first() {
sender.do_command(ReplaceNodeCommand {
handle: *first,
node,
});
}
}
}
}
}
}
pub fn handle_ui_message_with_ui_scene(
&mut self,
ui_scene: &mut UiScene,
engine: &mut Engine,
message: &UiMessage,
editor_selection: &Selection,
sender: &MessageSender,
) {
let ui = engine.user_interfaces.first();
if let Some(VariantResult::Handle(ui_node_handle)) = self
.handle_ui_message_internal::<UiNode, UserInterface>(
&engine.widget_constructors,
message,
ui,
&mut ui_scene.ui,
)
{
let sub_graph = ui_scene.ui.take_reserve_sub_graph(ui_node_handle);
let parent = if let Some(selection) = editor_selection.as_ui() {
selection.widgets.first().cloned().unwrap_or_default()
} else {
Handle::NONE
};
sender.do_command(AddWidgetCommand::new(sub_graph, parent, true));
}
}
}
+17 -129
View File
@@ -18,20 +18,19 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::world::create::EntityCreatorMode;
use crate::{
command::{Command, CommandGroup},
fyrox::{
asset::untyped::UntypedResource,
core::{
algebra::{Vector2, Vector3},
algebra::Vector2,
pool::Handle,
reflect::Reflect,
uuid::{uuid, Uuid},
},
engine::SerializationContext,
graph::SceneGraph,
gui::{
constructor::WidgetConstructorContainer,
file_browser::{FileSelector, FileSelectorMessage, FileType},
menu::{
self, ContextMenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent,
@@ -42,18 +41,15 @@ use crate::{
stack_panel::StackPanelBuilder,
widget::{WidgetBuilder, WidgetMessage},
window::{WindowAlignment, WindowMessage},
BuildContext, RcUiNodeHandle, UiNode, UserInterface,
BuildContext, RcUiNodeHandle, UiNode,
},
},
make_save_file_selector,
menu::{create::CreateEntityMenu, create_menu_item, create_menu_item_shortcut},
menu::{create_menu_item, create_menu_item_shortcut},
message::MessageSender,
scene::{
commands::{
graph::{
AddNodeCommand, LinkNodesCommand, MoveNodeCommand, ReplaceNodeCommand,
SetGraphRootCommand, SetNodeTransformCommand,
},
graph::{SetGraphRootCommand, SetNodeTransformCommand},
make_delete_selection_command, RevertSceneNodePropertyCommand,
},
controller::SceneController,
@@ -71,11 +67,8 @@ pub struct SceneNodeContextMenu {
delete_selection: Handle<MenuItem>,
copy_selection: Handle<MenuItem>,
create_child_menu: Handle<MenuItem>,
create_child_entity_menu: CreateEntityMenu,
create_parent_menu: Handle<MenuItem>,
create_parent_entity_menu: CreateEntityMenu,
replace_with_menu: Handle<MenuItem>,
replace_with_entity_menu: CreateEntityMenu,
placement_target: Handle<UiNode>,
save_as_prefab: Handle<MenuItem>,
save_as_prefab_dialog: Handle<FileSelector>,
@@ -89,26 +82,6 @@ impl WorldViewerItemContextMenu for SceneNodeContextMenu {
fn menu(&self) -> RcUiNodeHandle {
self.menu.clone()
}
fn on_plugin_added(
&mut self,
serialization_context: &SerializationContext,
widget_constructors_container: &WidgetConstructorContainer,
ui: &mut UserInterface,
) {
for (handle, menu) in [
(self.create_child_menu, &mut self.create_child_entity_menu),
(self.create_parent_menu, &mut self.create_parent_entity_menu),
(self.replace_with_menu, &mut self.replace_with_entity_menu),
] {
*menu = CreateEntityMenu::new(
serialization_context,
widget_constructors_container,
&mut ui.build_ctx(),
);
ui.send(handle, MenuItemMessage::Items(menu.root_items.clone()));
}
}
}
fn resource_path_of_first_selected_node(
@@ -145,11 +118,7 @@ impl SceneNodeContextMenu {
pub const RESET_INHERITABLE: Uuid = uuid!("95c6437f-dc23-4ec1-9490-d35f0864f027");
pub const SAVE_AS_PREFAB_FILE_SELECTOR: Uuid = uuid!("5d438037-a4be-4d70-a830-138185e1a049");
pub fn new(
serialization_context: &SerializationContext,
widget_constructors_container: &WidgetConstructorContainer,
ctx: &mut BuildContext,
) -> Self {
pub fn new(ctx: &mut BuildContext) -> Self {
let delete_selection;
let copy_selection;
let save_as_prefab;
@@ -161,13 +130,6 @@ impl SceneNodeContextMenu {
let create_child;
let replace_with;
let create_child_entity_menu =
CreateEntityMenu::new(serialization_context, widget_constructors_container, ctx);
let create_parent_entity_menu =
CreateEntityMenu::new(serialization_context, widget_constructors_container, ctx);
let replace_with_entity_menu =
CreateEntityMenu::new(serialization_context, widget_constructors_container, ctx);
let menu = ContextMenuBuilder::new(
PopupBuilder::new(WidgetBuilder::new().with_visibility(false))
.with_content(
@@ -177,8 +139,7 @@ impl SceneNodeContextMenu {
create_child = MenuItemBuilder::new(
WidgetBuilder::new().with_min_size(Vector2::new(120.0, 22.0)),
)
.with_content(MenuItemContent::text("Create Child Node"))
.with_items(create_child_entity_menu.root_items.clone())
.with_content(MenuItemContent::text("Create Child Node..."))
.build(ctx);
create_child
})
@@ -221,8 +182,7 @@ impl SceneNodeContextMenu {
.with_id(Self::CREATE_PARENT)
.with_min_size(Vector2::new(120.0, 22.0)),
)
.with_content(MenuItemContent::text("Create Parent Node"))
.with_items(create_parent_entity_menu.root_items.clone())
.with_content(MenuItemContent::text("Create Parent Node..."))
.build(ctx);
create_parent
})
@@ -232,8 +192,7 @@ impl SceneNodeContextMenu {
.with_id(Self::REPLACE_WITH)
.with_min_size(Vector2::new(120.0, 22.0)),
)
.with_content(MenuItemContent::text("Replace With Node"))
.with_items(replace_with_entity_menu.root_items.clone())
.with_content(MenuItemContent::text("Replace With Node..."))
.build(ctx);
replace_with
})
@@ -290,19 +249,16 @@ impl SceneNodeContextMenu {
}
Self {
create_child_entity_menu,
menu,
delete_selection,
copy_selection,
placement_target: Default::default(),
save_as_prefab,
save_as_prefab_dialog: Default::default(),
replace_with_entity_menu,
paste,
make_root,
open_asset,
reset_inheritable_properties,
create_parent_entity_menu,
create_child_menu: create_child,
create_parent_menu: create_parent,
replace_with_menu: replace_with,
@@ -318,82 +274,6 @@ impl SceneNodeContextMenu {
sender: &MessageSender,
settings: &Settings,
) {
if let Some(node) = self.create_child_entity_menu.handle_ui_message(
message,
sender,
controller,
editor_selection,
engine,
) {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(parent) = graph_selection.nodes().first() {
sender.do_command(AddNodeCommand::new(node, *parent, true));
}
}
} else if let Some(node) = self.create_parent_entity_menu.handle_ui_message(
message,
sender,
controller,
editor_selection,
engine,
) {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(first) = graph_selection.nodes().first() {
if let Some(game_scene) = controller.downcast_ref::<GameScene>() {
let scene = &engine.scenes[game_scene.scene];
let position = game_scene
.camera_controller
.placement_position(&scene.graph, *first);
let first_ref = &scene.graph[*first];
let parent = if first_ref.parent().is_some() {
first_ref.parent()
} else {
game_scene.scene_content_root
};
let new_parent_handle = scene.graph.generate_free_handles(1)[0];
let mut commands = CommandGroup::from(vec![
Command::new(AddNodeCommand::new(node, parent, true)),
Command::new(LinkNodesCommand::new(*first, new_parent_handle)),
]);
if parent == game_scene.scene_content_root {
commands.push(MoveNodeCommand::new(
new_parent_handle,
Vector3::default(),
position,
));
}
if *first == game_scene.scene_content_root {
commands.push(SetGraphRootCommand {
root: new_parent_handle,
link_scheme: Default::default(),
})
}
sender.do_command(commands);
}
}
}
} else if let Some(replacement) = self.replace_with_entity_menu.handle_ui_message(
message,
sender,
controller,
editor_selection,
engine,
) {
if let Some(graph_selection) = editor_selection.as_graph() {
if let Some(first) = graph_selection.nodes().first() {
sender.do_command(ReplaceNodeCommand {
handle: *first,
node: replacement,
});
}
}
}
if let Some(game_scene) = controller.downcast_mut::<GameScene>() {
if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if message.destination() == self.delete_selection {
@@ -503,6 +383,14 @@ impl SceneNodeContextMenu {
}
sender.do_command(CommandGroup::from(commands));
}
} else if message.destination() == self.create_child_menu {
sender.send(Message::OpenEntityCreator(EntityCreatorMode::CreateChild));
} else if message.destination() == self.create_parent_menu {
sender.send(Message::OpenEntityCreator(EntityCreatorMode::CreateParent));
} else if message.destination() == self.replace_with_menu {
sender.send(Message::OpenEntityCreator(
EntityCreatorMode::CreateReplacement,
));
}
} else if let Some(PopupMessage::Placement(Placement::Cursor(target))) = message.data()
{
+1 -8
View File
@@ -57,9 +57,7 @@ use crate::{
};
use fyrox::core::color::Color;
use fyrox::core::pool::HandlesVecExtension;
use fyrox::engine::SerializationContext;
use fyrox::gui::button::Button;
use fyrox::gui::constructor::WidgetConstructorContainer;
use fyrox::gui::scroll_viewer::ScrollViewer;
use fyrox::gui::searchbar::SearchBar;
use fyrox::gui::text_box::EmptyTextPlaceholder;
@@ -78,6 +76,7 @@ use std::{
rc::Rc,
};
pub mod create;
pub mod graph;
pub mod item;
pub mod menu;
@@ -139,12 +138,6 @@ pub trait WorldViewerDataProvider {
pub trait WorldViewerItemContextMenu {
fn menu(&self) -> RcUiNodeHandle;
fn on_plugin_added(
&mut self,
serialization_context: &SerializationContext,
widget_constructors_container: &WidgetConstructorContainer,
ui: &mut UserInterface,
);
}
pub struct WorldViewer {