From a57b322bc7ad32308fd7fd0d47a533e4ba94fd36 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 19 Apr 2026 00:03:25 +0800 Subject: [PATCH] feat(new_editor): wire project, inspector, and viewport runtime --- engine/include/XCEngine/UI/Types.h | 5 + new_editor/CMakeLists.txt | 1 + .../app/Bootstrap/ApplicationBootstrap.cpp | 4 + .../app/Bootstrap/ApplicationRunLoop.cpp | 6 +- .../app/Composition/EditorEditCommandRoute.h | 20 - .../Composition/EditorShellAssetBuilder.cpp | 8 +- .../app/Composition/EditorShellAssetBuilder.h | 4 +- .../EditorShellAssetBuilderInternal.h | 6 +- .../Composition/EditorShellAssetCommands.cpp | 18 +- .../EditorShellAssetDefinition.cpp | 45 +- .../Composition/EditorShellAssetLayout.cpp | 121 +- .../app/Composition/EditorShellAssetMenu.cpp | 25 +- .../EditorShellRuntimeViewport.cpp | 6 +- .../app/Composition/WorkspaceEventSync.cpp | 90 +- .../app/Features/Console/ConsolePanel.cpp | 2 +- .../app/Features/Console/ConsolePanel.h | 2 +- .../app/Features/Hierarchy/HierarchyModel.cpp | 72 +- .../app/Features/Hierarchy/HierarchyModel.h | 12 +- .../Hierarchy/HierarchyPanelInternal.h | 2 +- .../AudioListenerInspectorComponentEditor.h | 159 ++ .../AudioSourceInspectorComponentEditor.h | 353 ++++ .../BoxColliderInspectorComponentEditor.h | 68 + .../CameraInspectorComponentEditor.h | 329 ++++ .../CapsuleColliderInspectorComponentEditor.h | 118 ++ .../ColliderInspectorComponentEditorUtils.h | 133 ++ .../Components/IInspectorComponentEditor.h | 50 + .../InspectorComponentEditorRegistry.cpp | 64 + .../InspectorComponentEditorRegistry.h | 29 + .../InspectorComponentEditorUtils.h | 216 +++ .../LightInspectorComponentEditor.h | 327 ++++ .../MeshFilterInspectorComponentEditor.h | 69 + .../MeshRendererInspectorComponentEditor.h | 183 ++ .../RigidbodyInspectorComponentEditor.h | 178 ++ .../SphereColliderInspectorComponentEditor.h | 70 + .../TransformInspectorComponentEditor.cpp | 141 ++ .../TransformInspectorComponentEditor.h | 25 + .../VolumeRendererInspectorComponentEditor.h | 135 ++ .../app/Features/Inspector/InspectorPanel.cpp | 504 ++++-- .../app/Features/Inspector/InspectorPanel.h | 59 +- .../Inspector/InspectorPresentationModel.cpp | 237 +++ .../Inspector/InspectorPresentationModel.h | 36 + .../Features/Inspector/InspectorSubject.cpp | 74 + .../app/Features/Inspector/InspectorSubject.h | 59 + .../Features/Project/ProjectBrowserModel.cpp | 572 ++++++ .../Features/Project/ProjectBrowserModel.h | 50 + .../Project/ProjectBrowserModelAssets.cpp | 6 + .../Project/ProjectBrowserModelInternal.cpp | 274 +++ .../Project/ProjectBrowserModelInternal.h | 37 + .../app/Features/Project/ProjectPanel.cpp | 1606 ++++++++++++++++- .../app/Features/Project/ProjectPanel.h | 117 +- .../Features/Project/ProjectPanelInternal.h | 3 +- .../app/Features/Shared/GridItemDragDrop.h | 157 ++ .../app/Features/Shared/TreeItemDragDrop.h | 324 ++++ new_editor/app/Internal/StringEncoding.h | 32 + new_editor/app/Platform/Win32/EditorWindow.h | 10 + .../Win32/EditorWindowBorderlessResize.cpp | 10 +- .../app/Platform/Win32/EditorWindowFrame.cpp | 316 ++++ .../Win32/EditorWindowFrameRuntime.cpp | 211 --- .../Win32/EditorWindowInitialization.cpp | 125 -- .../app/Platform/Win32/EditorWindowInput.cpp | 179 +- .../Win32/EditorWindowInputInternal.cpp | 103 -- .../Win32/EditorWindowInputInternal.h | 16 - .../Platform/Win32/EditorWindowLifecycle.cpp | 8 +- .../Platform/Win32/EditorWindowMetrics.cpp | 95 - .../Win32/EditorWindowPlatformInternal.cpp | 35 - .../Win32/EditorWindowPointerCapture.h | 42 + .../Win32/EditorWindowResizeLifecycle.cpp | 73 - .../Win32/EditorWindowRuntimeInternal.cpp | 24 - .../app/Platform/Win32/EditorWindowState.h | 3 + .../Win32/EditorWindowTitleBarDragRestore.cpp | 97 - .../Win32/EditorWindowTitleBarInteraction.cpp | 98 +- .../app/Platform/Win32/InputModifierTracker.h | 84 +- .../Platform/Win32/WindowManager/Creation.cpp | 107 -- .../Win32/WindowManager/CrossWindowDrop.cpp | 93 - .../WindowManager/CrossWindowDropInternal.cpp | 109 -- .../WindowManager/CrossWindowDropInternal.h | 26 - .../Platform/Win32/WindowManager/Detach.cpp | 47 - .../Win32/WindowManager/Lifecycle.cpp | 4 +- .../Platform/Win32/WindowManager/Lookup.cpp | 57 - .../Platform/Win32/WindowManager/TabDrag.cpp | 11 +- .../Win32/WindowManager/WindowSet.cpp | 55 - .../Win32/WindowManager/WindowSync.cpp | 4 +- .../Win32/WindowMessageDispatcher.cpp | 107 +- .../app/Project/EditorProjectRuntime.cpp | 454 +++++ new_editor/app/Project/EditorProjectRuntime.h | 112 ++ .../Viewport/Passes/SceneViewportGridPass.cpp | 642 +++++++ .../Viewport/Passes/SceneViewportGridPass.h | 38 + .../SceneViewportSelectionOutlinePass.cpp | 746 ++++++++ .../SceneViewportSelectionOutlinePass.h | 52 + .../Viewport/RenderTargetManager/Cleanup.cpp | 56 - .../Viewport/RenderTargetManager/Surfaces.cpp | 63 - .../Viewport/SceneViewportPassSpecs.h | 36 + .../SceneViewportRenderPassBundle.cpp | 32 + .../Viewport/SceneViewportRenderPassBundle.h | 24 + .../Viewport/SceneViewportRenderPlan.h | 167 ++ .../Viewport/SceneViewportResourcePaths.h | 49 + .../Viewport/ViewportHostService.cpp | 390 ++++ .../Rendering/Viewport/ViewportHostService.h | 39 + .../Viewport/ViewportHostServiceFrame.cpp | 139 -- .../Viewport/ViewportHostServiceLifecycle.cpp | 71 - .../Viewport/ViewportObjectIdPicker.h | 156 ++ ...esources.cpp => ViewportRenderTargets.cpp} | 105 ++ new_editor/app/State/EditorContext.h | 9 +- new_editor/app/State/EditorContextStatus.cpp | 2 +- .../XCEditor/App/EditorEditCommandRoute.h | 35 + .../XCEditor/App}/EditorHostCommandBridge.h | 14 +- .../include/XCEditor/App/EditorPanelIds.h | 25 + .../XCEditor/App}/EditorSession.h | 3 +- .../XCEditor/Fields/UIEditorPropertyGrid.h | 41 + .../Fields/UIEditorPropertyGridInteraction.h | 31 + .../Panels/UIEditorPanelContentHost.h | 11 +- .../XCEditor/Panels/UIEditorPanelRegistry.h | 5 +- .../XCEditor/Shell/UIEditorShellAsset.h | 2 +- .../Shell/UIEditorShellCapturePolicy.h | 15 + .../Viewport/UIEditorViewportInputBridge.h | 19 + .../XCEditor/Viewport/UIEditorViewportSlot.h | 6 +- .../Workspace/UIEditorWorkspaceModel.h | 2 +- .../Workspace/UIEditorWorkspaceValidation.h | 1 + .../App}/EditorHostCommandBridge.cpp | 49 +- .../{app/State => src/App}/EditorSession.cpp | 16 +- .../Fields/PropertyGridInteractionAsset.cpp | 300 +++ .../Fields/PropertyGridInteractionHelpers.cpp | 7 + .../Fields/PropertyGridInteractionInternal.h | 44 + .../Fields/PropertyGridInteractionVector.cpp | 627 +++++++ new_editor/src/Fields/PropertyGridInternal.h | 17 + .../src/Fields/PropertyGridRendering.cpp | 132 +- .../src/Fields/UIEditorPropertyGrid.cpp | 3 + .../UIEditorPropertyGridInteraction.cpp | 89 +- .../src/Panels/UIEditorPanelContentHost.cpp | 36 +- .../src/Panels/UIEditorPanelRegistry.cpp | 2 +- new_editor/src/Shell/UIEditorShellAsset.cpp | 21 +- .../src/Shell/UIEditorShellCapturePolicy.cpp | 50 + .../Viewport/UIEditorViewportInputBridge.cpp | 124 +- .../src/Viewport/UIEditorViewportShell.cpp | 23 +- .../src/Viewport/UIEditorViewportSlot.cpp | 4 +- .../Workspace/UIEditorWorkspaceCompose.cpp | 35 +- .../src/Workspace/UIEditorWorkspaceModel.cpp | 16 +- .../src/Workspace/WorkspaceModelInternal.h | 1 + .../Workspace/WorkspaceModelValidation.cpp | 3 +- tests/NewEditor/CMakeLists.txt | 30 - .../NewEditor/test_sandbox_frame_builder.cpp | 54 - .../test_structured_editor_shell.cpp | 88 - .../shared/src/InputModifierTracker.h | 84 +- .../Core/unit/test_input_modifier_tracker.cpp | 23 +- tests/UI/Editor/CMakeLists.txt | 11 +- tests/UI/Editor/unit/CMakeLists.txt | 18 +- .../unit/test_editor_host_command_bridge.cpp | 105 +- .../unit/test_editor_project_runtime.cpp | 172 ++ .../test_editor_shell_asset_validation.cpp | 22 +- .../unit/test_editor_window_input_routing.cpp | 125 ++ .../unit/test_hierarchy_scene_binding.cpp | 204 +++ .../unit/test_input_modifier_tracker.cpp | 23 +- .../unit/test_inspector_presentation.cpp | 316 ++++ .../unit/test_project_browser_model.cpp | 262 +++ tests/UI/Editor/unit/test_project_panel.cpp | 245 +++ .../unit/test_scene_viewport_render_plan.cpp | 246 +++ .../unit/test_structured_editor_shell.cpp | 6 +- .../test_ui_editor_panel_content_host.cpp | 32 +- .../unit/test_ui_editor_panel_registry.cpp | 4 +- ...st_ui_editor_property_grid_interaction.cpp | 204 +++ .../test_ui_editor_viewport_input_bridge.cpp | 194 +- .../unit/test_ui_editor_viewport_shell.cpp | 44 + .../unit/test_ui_editor_viewport_slot.cpp | 2 + .../test_ui_editor_workspace_interaction.cpp | 39 + .../unit/test_ui_editor_workspace_model.cpp | 23 + .../unit/test_viewport_object_id_picker.cpp | 97 + .../test_scene_viewport_overlay_renderer.cpp | 10 +- .../test_viewport_render_flow_utils.cpp | 70 +- 168 files changed, 14829 insertions(+), 2507 deletions(-) delete mode 100644 new_editor/app/Composition/EditorEditCommandRoute.h create mode 100644 new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h create mode 100644 new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp create mode 100644 new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.h create mode 100644 new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h create mode 100644 new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp create mode 100644 new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h create mode 100644 new_editor/app/Features/Inspector/InspectorPresentationModel.cpp create mode 100644 new_editor/app/Features/Inspector/InspectorPresentationModel.h create mode 100644 new_editor/app/Features/Inspector/InspectorSubject.cpp create mode 100644 new_editor/app/Features/Inspector/InspectorSubject.h create mode 100644 new_editor/app/Features/Shared/GridItemDragDrop.h create mode 100644 new_editor/app/Features/Shared/TreeItemDragDrop.h delete mode 100644 new_editor/app/Platform/Win32/EditorWindowFrameRuntime.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowInitialization.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowInputInternal.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowInputInternal.h delete mode 100644 new_editor/app/Platform/Win32/EditorWindowMetrics.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowPlatformInternal.cpp create mode 100644 new_editor/app/Platform/Win32/EditorWindowPointerCapture.h delete mode 100644 new_editor/app/Platform/Win32/EditorWindowResizeLifecycle.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.cpp delete mode 100644 new_editor/app/Platform/Win32/EditorWindowTitleBarDragRestore.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/Creation.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/CrossWindowDrop.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.h delete mode 100644 new_editor/app/Platform/Win32/WindowManager/Detach.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/Lookup.cpp delete mode 100644 new_editor/app/Platform/Win32/WindowManager/WindowSet.cpp create mode 100644 new_editor/app/Project/EditorProjectRuntime.cpp create mode 100644 new_editor/app/Project/EditorProjectRuntime.h create mode 100644 new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp create mode 100644 new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.h create mode 100644 new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp create mode 100644 new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h delete mode 100644 new_editor/app/Rendering/Viewport/RenderTargetManager/Cleanup.cpp delete mode 100644 new_editor/app/Rendering/Viewport/RenderTargetManager/Surfaces.cpp create mode 100644 new_editor/app/Rendering/Viewport/SceneViewportPassSpecs.h create mode 100644 new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp create mode 100644 new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h create mode 100644 new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h create mode 100644 new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h create mode 100644 new_editor/app/Rendering/Viewport/ViewportHostService.cpp delete mode 100644 new_editor/app/Rendering/Viewport/ViewportHostServiceFrame.cpp delete mode 100644 new_editor/app/Rendering/Viewport/ViewportHostServiceLifecycle.cpp create mode 100644 new_editor/app/Rendering/Viewport/ViewportObjectIdPicker.h rename new_editor/app/Rendering/Viewport/{RenderTargetManager/Resources.cpp => ViewportRenderTargets.cpp} (58%) create mode 100644 new_editor/include/XCEditor/App/EditorEditCommandRoute.h rename new_editor/{app/Composition => include/XCEditor/App}/EditorHostCommandBridge.h (80%) create mode 100644 new_editor/include/XCEditor/App/EditorPanelIds.h rename new_editor/{app/State => include/XCEditor/App}/EditorSession.h (97%) create mode 100644 new_editor/include/XCEditor/Shell/UIEditorShellCapturePolicy.h rename new_editor/{app/Composition => src/App}/EditorHostCommandBridge.cpp (79%) rename new_editor/{app/State => src/App}/EditorSession.cpp (86%) create mode 100644 new_editor/src/Fields/PropertyGridInteractionAsset.cpp create mode 100644 new_editor/src/Fields/PropertyGridInteractionVector.cpp create mode 100644 new_editor/src/Shell/UIEditorShellCapturePolicy.cpp delete mode 100644 tests/NewEditor/CMakeLists.txt delete mode 100644 tests/NewEditor/test_sandbox_frame_builder.cpp delete mode 100644 tests/NewEditor/test_structured_editor_shell.cpp create mode 100644 tests/UI/Editor/unit/test_editor_project_runtime.cpp create mode 100644 tests/UI/Editor/unit/test_editor_window_input_routing.cpp create mode 100644 tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp create mode 100644 tests/UI/Editor/unit/test_inspector_presentation.cpp create mode 100644 tests/UI/Editor/unit/test_project_browser_model.cpp create mode 100644 tests/UI/Editor/unit/test_project_panel.cpp create mode 100644 tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp create mode 100644 tests/UI/Editor/unit/test_viewport_object_id_picker.cpp diff --git a/engine/include/XCEngine/UI/Types.h b/engine/include/XCEngine/UI/Types.h index c41bfceb..c3ad9c41 100644 --- a/engine/include/XCEngine/UI/Types.h +++ b/engine/include/XCEngine/UI/Types.h @@ -88,6 +88,11 @@ struct UIInputModifiers { bool control = false; bool alt = false; bool super = false; + bool leftMouse = false; + bool rightMouse = false; + bool middleMouse = false; + bool x1Mouse = false; + bool x2Mouse = false; }; struct UIInputEvent { diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 681961c4..d532b397 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -49,6 +49,7 @@ set(XCUI_EDITOR_FIELD_SOURCES src/Fields/UIEditorObjectFieldInteraction.cpp src/Fields/UIEditorPropertyGrid.cpp src/Fields/UIEditorPropertyGridInteraction.cpp + src/Fields/PropertyGridInteractionAsset.cpp src/Fields/PropertyGridInteractionColor.cpp src/Fields/PropertyGridInteractionEdit.cpp src/Fields/PropertyGridInteractionHelpers.cpp diff --git a/new_editor/app/Bootstrap/ApplicationBootstrap.cpp b/new_editor/app/Bootstrap/ApplicationBootstrap.cpp index cd94de95..af7412af 100644 --- a/new_editor/app/Bootstrap/ApplicationBootstrap.cpp +++ b/new_editor/app/Bootstrap/ApplicationBootstrap.cpp @@ -3,6 +3,8 @@ #include +#include + #include "State/EditorContext.h" #include "Platform/Win32/EditorWindow.h" #include "Platform/Win32/EditorWindowManager.h" @@ -110,6 +112,8 @@ void Application::Shutdown() { m_windowManager.reset(); } + ::XCEngine::Resources::ResourceManager::Get().Shutdown(); + if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); m_windowClassAtom = 0; diff --git a/new_editor/app/Bootstrap/ApplicationRunLoop.cpp b/new_editor/app/Bootstrap/ApplicationRunLoop.cpp index 41496dd9..87a1162e 100644 --- a/new_editor/app/Bootstrap/ApplicationRunLoop.cpp +++ b/new_editor/app/Bootstrap/ApplicationRunLoop.cpp @@ -32,9 +32,12 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { return 1; } + constexpr int kMaxMessagesPerTick = 64; MSG message = {}; while (true) { - while (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + int processedMessageCount = 0; + while (processedMessageCount < kMaxMessagesPerTick && + PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { if (message.message == WM_QUIT) { Shutdown(); return static_cast(message.wParam); @@ -42,6 +45,7 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { TranslateMessage(&message); DispatchMessageW(&message); + ++processedMessageCount; } if (m_windowManager != nullptr) { diff --git a/new_editor/app/Composition/EditorEditCommandRoute.h b/new_editor/app/Composition/EditorEditCommandRoute.h deleted file mode 100644 index 6c0741b2..00000000 --- a/new_editor/app/Composition/EditorEditCommandRoute.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -#include - -namespace XCEngine::UI::Editor::App { - -class EditorEditCommandRoute { -public: - virtual ~EditorEditCommandRoute() = default; - - virtual UIEditorHostCommandEvaluationResult EvaluateEditCommand( - std::string_view commandId) const = 0; - - virtual UIEditorHostCommandDispatchResult DispatchEditCommand( - std::string_view commandId) = 0; -}; - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellAssetBuilder.cpp b/new_editor/app/Composition/EditorShellAssetBuilder.cpp index 4d554bbe..f610c90b 100644 --- a/new_editor/app/Composition/EditorShellAssetBuilder.cpp +++ b/new_editor/app/Composition/EditorShellAssetBuilder.cpp @@ -6,21 +6,21 @@ namespace XCEngine::UI::Editor::App { using namespace CompositionInternal; -EditorShellAsset BuildEditorShellAsset(const std::filesystem::path& repoRoot) { +EditorShellAsset BuildEditorApplicationShellAsset(const std::filesystem::path& repoRoot) { EditorShellAsset asset = {}; asset.screenId = "editor.shell"; asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal(); asset.panelRegistry = BuildEditorPanelRegistry(); - asset.workspace = BuildEditorWorkspaceModel(); + asset.workspace = BuildEditorWorkspaceModel(asset.panelRegistry); asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace); - asset.shellDefinition = BuildBaseEditorShellDefinition(); + asset.shellDefinition = BuildBaseEditorShellDefinition(asset.panelRegistry); asset.shortcutAsset.commandRegistry = BuildEditorCommandRegistry(); asset.shortcutAsset.bindings = BuildEditorShortcutBindings(); return asset; } -UIEditorShellInteractionDefinition BuildEditorShellInteractionDefinition( +UIEditorShellInteractionDefinition BuildEditorApplicationShellInteractionDefinition( const EditorShellAsset& asset, const UIEditorWorkspaceController& controller, std::string_view statusText, diff --git a/new_editor/app/Composition/EditorShellAssetBuilder.h b/new_editor/app/Composition/EditorShellAssetBuilder.h index 681771f3..27eb71ec 100644 --- a/new_editor/app/Composition/EditorShellAssetBuilder.h +++ b/new_editor/app/Composition/EditorShellAssetBuilder.h @@ -10,9 +10,9 @@ namespace XCEngine::UI::Editor::App { -EditorShellAsset BuildEditorShellAsset(const std::filesystem::path& repoRoot); +EditorShellAsset BuildEditorApplicationShellAsset(const std::filesystem::path& repoRoot); -UIEditorShellInteractionDefinition BuildEditorShellInteractionDefinition( +UIEditorShellInteractionDefinition BuildEditorApplicationShellInteractionDefinition( const EditorShellAsset& asset, const UIEditorWorkspaceController& controller, std::string_view statusText, diff --git a/new_editor/app/Composition/EditorShellAssetBuilderInternal.h b/new_editor/app/Composition/EditorShellAssetBuilderInternal.h index 7b8ba5ed..98d564ba 100644 --- a/new_editor/app/Composition/EditorShellAssetBuilderInternal.h +++ b/new_editor/app/Composition/EditorShellAssetBuilderInternal.h @@ -11,11 +11,13 @@ namespace XCEngine::UI::Editor::App::CompositionInternal { UIEditorPanelRegistry BuildEditorPanelRegistry(); -UIEditorWorkspaceModel BuildEditorWorkspaceModel(); +UIEditorWorkspaceModel BuildEditorWorkspaceModel( + const UIEditorPanelRegistry& panelRegistry); UIEditorCommandRegistry BuildEditorCommandRegistry(); UIEditorMenuModel BuildEditorMenuModel(); std::vector<::XCEngine::UI::UIShortcutBinding> BuildEditorShortcutBindings(); -UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition(); +UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( + const UIEditorPanelRegistry& panelRegistry); std::string ResolveEditorPanelTitle( const UIEditorPanelRegistry& registry, std::string_view panelId); diff --git a/new_editor/app/Composition/EditorShellAssetCommands.cpp b/new_editor/app/Composition/EditorShellAssetCommands.cpp index d6bcb007..4d242ae8 100644 --- a/new_editor/app/Composition/EditorShellAssetCommands.cpp +++ b/new_editor/app/Composition/EditorShellAssetCommands.cpp @@ -1,5 +1,6 @@ #include "EditorShellAssetBuilderInternal.h" +#include #include #include @@ -81,6 +82,10 @@ UIEditorCommandRegistry BuildEditorCommandRegistry() { BuildHostCommand("edit.duplicate", "Duplicate"), BuildHostCommand("edit.delete", "Delete"), BuildHostCommand("edit.rename", "Rename"), + BuildHostCommand("assets.create_folder", "Create Folder"), + BuildHostCommand("assets.create_material", "Create Material"), + BuildHostCommand("assets.copy_path", "Copy Path"), + BuildHostCommand("assets.show_in_explorer", "Show in Explorer"), BuildHostCommand("assets.reimport_selected", "Reimport Selected Asset"), BuildHostCommand("assets.reimport_all", "Reimport All Assets"), BuildHostCommand("assets.clear_library", "Clear Library"), @@ -97,32 +102,32 @@ UIEditorCommandRegistry BuildEditorCommandRegistry() { "view.activate_hierarchy", "Hierarchy", UIEditorWorkspaceCommandKind::ActivatePanel, - "hierarchy"), + std::string(kHierarchyPanelId)), BuildWorkspaceCommand( "view.activate_scene", "Scene", UIEditorWorkspaceCommandKind::ActivatePanel, - "scene"), + std::string(kScenePanelId)), BuildWorkspaceCommand( "view.activate_game", "Game", UIEditorWorkspaceCommandKind::ActivatePanel, - "game"), + std::string(kGamePanelId)), BuildWorkspaceCommand( "view.activate_inspector", "Inspector", UIEditorWorkspaceCommandKind::ActivatePanel, - "inspector"), + std::string(kInspectorPanelId)), BuildWorkspaceCommand( "view.activate_console", "Console", UIEditorWorkspaceCommandKind::ActivatePanel, - "console"), + std::string(kConsolePanelId)), BuildWorkspaceCommand( "view.activate_project", "Project", UIEditorWorkspaceCommandKind::ActivatePanel, - "project") + std::string(kProjectPanelId)) }; return registry; } @@ -141,6 +146,7 @@ std::vector BuildEditorShortcutBindings() { BuildBinding("edit.duplicate", static_cast(KeyCode::D), true), BuildBinding("edit.delete", static_cast(KeyCode::Delete)), BuildBinding("edit.rename", static_cast(KeyCode::F2)), + BuildBinding("assets.create_folder", static_cast(KeyCode::N), true, true), BuildBinding("run.play", static_cast(KeyCode::F5)), BuildBinding("run.pause", static_cast(KeyCode::F6)), BuildBinding("run.step", static_cast(KeyCode::F7)), diff --git a/new_editor/app/Composition/EditorShellAssetDefinition.cpp b/new_editor/app/Composition/EditorShellAssetDefinition.cpp index 0b0e4f58..06a6831e 100644 --- a/new_editor/app/Composition/EditorShellAssetDefinition.cpp +++ b/new_editor/app/Composition/EditorShellAssetDefinition.cpp @@ -16,31 +16,34 @@ UIEditorShellToolbarButton BuildToolbarButton( return button; } -UIEditorWorkspacePanelPresentationModel BuildHostedContentPresentation( - std::string panelId) { +UIEditorWorkspacePanelPresentationModel BuildViewportPresentation( + const UIEditorPanelDescriptor& descriptor) { UIEditorWorkspacePanelPresentationModel presentation = {}; - presentation.panelId = std::move(panelId); - presentation.kind = UIEditorPanelPresentationKind::HostedContent; + presentation.panelId = descriptor.panelId; + presentation.kind = descriptor.presentationKind; + presentation.viewportShellModel.spec = descriptor.viewportShellSpec; + if (presentation.viewportShellModel.spec.chrome.title.empty()) { + presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; + } return presentation; } -UIEditorWorkspacePanelPresentationModel BuildViewportPresentation( - std::string panelId, - std::string title) { +UIEditorWorkspacePanelPresentationModel BuildPanelPresentation( + const UIEditorPanelDescriptor& descriptor) { + if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + return BuildViewportPresentation(descriptor); + } + UIEditorWorkspacePanelPresentationModel presentation = {}; - presentation.panelId = std::move(panelId); - presentation.kind = UIEditorPanelPresentationKind::ViewportShell; - presentation.viewportShellModel.spec.chrome.title = std::move(title); - presentation.viewportShellModel.spec.chrome.subtitle = {}; - presentation.viewportShellModel.spec.chrome.showTopBar = false; - presentation.viewportShellModel.spec.chrome.showBottomBar = false; - presentation.viewportShellModel.frame.statusText.clear(); + presentation.panelId = descriptor.panelId; + presentation.kind = descriptor.presentationKind; return presentation; } } // namespace -UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition() { +UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition( + const UIEditorPanelRegistry& panelRegistry) { UIEditorShellInteractionDefinition definition = {}; definition.menuModel = BuildEditorMenuModel(); definition.toolbarButtons = { @@ -49,14 +52,10 @@ UIEditorShellInteractionDefinition BuildBaseEditorShellDefinition() { BuildToolbarButton("run.step", UIEditorShellToolbarGlyph::Step) }; definition.statusSegments = {}; - definition.workspacePresentations = { - BuildHostedContentPresentation("hierarchy"), - BuildViewportPresentation("scene", "Scene"), - BuildViewportPresentation("game", "Game"), - BuildHostedContentPresentation("inspector"), - BuildHostedContentPresentation("console"), - BuildHostedContentPresentation("project") - }; + definition.workspacePresentations.reserve(panelRegistry.panels.size()); + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + definition.workspacePresentations.push_back(BuildPanelPresentation(descriptor)); + } return definition; } diff --git a/new_editor/app/Composition/EditorShellAssetLayout.cpp b/new_editor/app/Composition/EditorShellAssetLayout.cpp index 748dc8c7..10764f36 100644 --- a/new_editor/app/Composition/EditorShellAssetLayout.cpp +++ b/new_editor/app/Composition/EditorShellAssetLayout.cpp @@ -1,21 +1,90 @@ #include "EditorShellAssetBuilderInternal.h" +#include + namespace XCEngine::UI::Editor::App::CompositionInternal { +namespace { + +UIEditorPanelDescriptor BuildHostedContentPanelDescriptor( + std::string_view panelId, + std::string_view title, + bool placeholder, + bool canHide, + bool canClose) { + UIEditorPanelDescriptor descriptor = {}; + descriptor.panelId = std::string(panelId); + descriptor.defaultTitle = std::string(title); + descriptor.presentationKind = UIEditorPanelPresentationKind::HostedContent; + descriptor.placeholder = placeholder; + descriptor.canHide = canHide; + descriptor.canClose = canClose; + return descriptor; +} + +UIEditorPanelDescriptor BuildViewportPanelDescriptor( + std::string_view panelId, + std::string_view title, + bool canHide, + bool canClose, + bool showTopBar, + bool showBottomBar) { + UIEditorPanelDescriptor descriptor = {}; + descriptor.panelId = std::string(panelId); + descriptor.defaultTitle = std::string(title); + descriptor.presentationKind = UIEditorPanelPresentationKind::ViewportShell; + descriptor.placeholder = false; + descriptor.canHide = canHide; + descriptor.canClose = canClose; + descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; + descriptor.viewportShellSpec.chrome.showTopBar = showTopBar; + descriptor.viewportShellSpec.chrome.showBottomBar = showBottomBar; + return descriptor; +} + +const UIEditorPanelDescriptor& RequirePanelDescriptor( + const UIEditorPanelRegistry& registry, + std::string_view panelId) { + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(registry, panelId); + descriptor != nullptr) { + return *descriptor; + } + + static const UIEditorPanelDescriptor fallback = {}; + return fallback; +} + +} // namespace + UIEditorPanelRegistry BuildEditorPanelRegistry() { UIEditorPanelRegistry registry = {}; registry.panels = { - { "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::HostedContent, true, false, false }, - { "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, false, false }, - { "game", "Game", UIEditorPanelPresentationKind::ViewportShell, false, false, false }, - { "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, true, false, false }, - { "console", "Console", UIEditorPanelPresentationKind::HostedContent, true, false, false }, - { "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false } + BuildHostedContentPanelDescriptor(kHierarchyPanelId, kHierarchyPanelTitle, true, false, false), + BuildViewportPanelDescriptor(kScenePanelId, kScenePanelTitle, false, false, false, false), + BuildViewportPanelDescriptor(kGamePanelId, kGamePanelTitle, false, false, false, false), + BuildHostedContentPanelDescriptor(kInspectorPanelId, kInspectorPanelTitle, true, false, false), + BuildHostedContentPanelDescriptor(kConsolePanelId, kConsolePanelTitle, true, false, false), + BuildHostedContentPanelDescriptor(kProjectPanelId, kProjectPanelTitle, false, false, false) }; return registry; } -UIEditorWorkspaceModel BuildEditorWorkspaceModel() { +UIEditorWorkspaceModel BuildEditorWorkspaceModel( + const UIEditorPanelRegistry& panelRegistry) { + const UIEditorPanelDescriptor& hierarchy = + RequirePanelDescriptor(panelRegistry, kHierarchyPanelId); + const UIEditorPanelDescriptor& scene = + RequirePanelDescriptor(panelRegistry, kScenePanelId); + const UIEditorPanelDescriptor& game = + RequirePanelDescriptor(panelRegistry, kGamePanelId); + const UIEditorPanelDescriptor& inspector = + RequirePanelDescriptor(panelRegistry, kInspectorPanelId); + const UIEditorPanelDescriptor& console = + RequirePanelDescriptor(panelRegistry, kConsolePanelId); + const UIEditorPanelDescriptor& project = + RequirePanelDescriptor(panelRegistry, kProjectPanelId); + UIEditorWorkspaceModel workspace = {}; workspace.root = BuildUIEditorWorkspaceSplit( "workspace-root", @@ -31,45 +100,45 @@ UIEditorWorkspaceModel BuildEditorWorkspaceModel() { 0.19047619f, BuildUIEditorWorkspaceSingleTabStack( "hierarchy-panel", - "hierarchy", - "Hierarchy", - true), + std::string(kHierarchyPanelId), + hierarchy.defaultTitle, + hierarchy.placeholder), BuildUIEditorWorkspaceTabStack( "center-tabs", { BuildUIEditorWorkspacePanel( "scene-panel", - "scene", - "Scene", - false), + std::string(kScenePanelId), + scene.defaultTitle, + scene.placeholder), BuildUIEditorWorkspacePanel( "game-panel", - "game", - "Game", - false) + std::string(kGamePanelId), + game.defaultTitle, + game.placeholder) }, 0u)), BuildUIEditorWorkspaceSingleTabStack( "inspector-panel", - "inspector", - "Inspector", - true)), + std::string(kInspectorPanelId), + inspector.defaultTitle, + inspector.placeholder)), BuildUIEditorWorkspaceTabStack( "bottom-tabs", { BuildUIEditorWorkspacePanel( "console-panel", - "console", - "Console", - true), + std::string(kConsolePanelId), + console.defaultTitle, + console.placeholder), BuildUIEditorWorkspacePanel( "project-panel", - "project", - "Project", - false) + std::string(kProjectPanelId), + project.defaultTitle, + project.placeholder) }, 1u)); - workspace.activePanelId = "scene"; + workspace.activePanelId = std::string(kScenePanelId); return workspace; } diff --git a/new_editor/app/Composition/EditorShellAssetMenu.cpp b/new_editor/app/Composition/EditorShellAssetMenu.cpp index 2ad70902..ced4d838 100644 --- a/new_editor/app/Composition/EditorShellAssetMenu.cpp +++ b/new_editor/app/Composition/EditorShellAssetMenu.cpp @@ -1,5 +1,7 @@ #include "EditorShellAssetBuilderInternal.h" +#include + #include namespace XCEngine::UI::Editor::App::CompositionInternal { @@ -79,6 +81,17 @@ UIEditorMenuModel BuildEditorMenuModel() { assetsMenu.menuId = "assets"; assetsMenu.label = "Assets"; assetsMenu.items = { + BuildSubmenuItem( + "assets-create", + "Create", + { + BuildCommandItem("assets-create-folder", "Folder", "assets.create_folder"), + BuildCommandItem("assets-create-material", "Material", "assets.create_material") + }), + BuildSeparatorItem("assets-separator-create"), + BuildCommandItem("assets-show-in-explorer", "Show in Explorer", "assets.show_in_explorer"), + BuildCommandItem("assets-copy-path", "Copy Path", "assets.copy_path"), + BuildSeparatorItem("assets-separator-utility"), BuildCommandItem("assets-reimport-selected", "Reimport Selected Asset", "assets.reimport_selected"), BuildCommandItem("assets-reimport-all", "Reimport All Assets", "assets.reimport_all"), BuildSeparatorItem("assets-separator-clear"), @@ -103,27 +116,27 @@ UIEditorMenuModel BuildEditorMenuModel() { UIEditorMenuCheckedStateBinding hierarchyActive = { UIEditorMenuCheckedStateSource::PanelActive, - "hierarchy" + std::string(kHierarchyPanelId) }; UIEditorMenuCheckedStateBinding sceneActive = { UIEditorMenuCheckedStateSource::PanelActive, - "scene" + std::string(kScenePanelId) }; UIEditorMenuCheckedStateBinding gameActive = { UIEditorMenuCheckedStateSource::PanelActive, - "game" + std::string(kGamePanelId) }; UIEditorMenuCheckedStateBinding inspectorActive = { UIEditorMenuCheckedStateSource::PanelActive, - "inspector" + std::string(kInspectorPanelId) }; UIEditorMenuCheckedStateBinding consoleActive = { UIEditorMenuCheckedStateSource::PanelActive, - "console" + std::string(kConsolePanelId) }; UIEditorMenuCheckedStateBinding projectActive = { UIEditorMenuCheckedStateSource::PanelActive, - "project" + std::string(kProjectPanelId) }; UIEditorMenuDescriptor viewMenu = {}; diff --git a/new_editor/app/Composition/EditorShellRuntimeViewport.cpp b/new_editor/app/Composition/EditorShellRuntimeViewport.cpp index 3df05932..90a6bda9 100644 --- a/new_editor/app/Composition/EditorShellRuntimeViewport.cpp +++ b/new_editor/app/Composition/EditorShellRuntimeViewport.cpp @@ -1,15 +1,17 @@ #include "Composition/EditorShellRuntimeInternal.h" +#include + namespace XCEngine::UI::Editor::App::Internal { namespace { bool IsViewportPanel(std::string_view panelId) { - return panelId == "scene" || panelId == "game"; + return IsEditorViewportPanelId(panelId); } ViewportKind ResolveViewportKind(std::string_view panelId) { - return panelId == "game" + return panelId == kGamePanelId ? ViewportKind::Game : ViewportKind::Scene; } diff --git a/new_editor/app/Composition/WorkspaceEventSync.cpp b/new_editor/app/Composition/WorkspaceEventSync.cpp index a191a583..28a30547 100644 --- a/new_editor/app/Composition/WorkspaceEventSync.cpp +++ b/new_editor/app/Composition/WorkspaceEventSync.cpp @@ -14,6 +14,26 @@ namespace XCEngine::UI::Editor::App { namespace { +std::string_view DescribeProjectItemKind(ProjectBrowserModel::ItemKind kind) { + switch (kind) { + case ProjectBrowserModel::ItemKind::Folder: + return "Folder"; + case ProjectBrowserModel::ItemKind::Scene: + return "Scene"; + case ProjectBrowserModel::ItemKind::Model: + return "Model"; + case ProjectBrowserModel::ItemKind::Material: + return "Material"; + case ProjectBrowserModel::ItemKind::Texture: + return "Texture"; + case ProjectBrowserModel::ItemKind::Script: + return "Script"; + case ProjectBrowserModel::ItemKind::File: + default: + return "File"; + } +} + std::string DescribeProjectPanelEvent(const ProjectPanel::Event& event) { std::ostringstream stream = {}; switch (event.kind) { @@ -58,6 +78,12 @@ std::string DescribeProjectPanelEvent(const ProjectPanel::Event& event) { case ProjectPanel::EventSource::GridSecondary: stream << "GridSecondary"; break; + case ProjectPanel::EventSource::GridDrag: + stream << "GridDrag"; + break; + case ProjectPanel::EventSource::Command: + stream << "Command"; + break; case ProjectPanel::EventSource::Background: stream << "Background"; break; @@ -73,6 +99,9 @@ std::string DescribeProjectPanelEvent(const ProjectPanel::Event& event) { if (!event.displayName.empty()) { stream << " label=" << event.displayName; } + if (!event.itemId.empty()) { + stream << " kind=" << DescribeProjectItemKind(event.itemKind); + } return stream.str(); } @@ -109,75 +138,26 @@ std::string DescribeHierarchyPanelEvent(const HierarchyPanel::Event& event) { return stream.str(); } -void ApplyHierarchySelection( - EditorContext& context, - const HierarchyPanel::Event& event) { - if (event.kind != HierarchyPanel::EventKind::SelectionChanged) { - return; - } - - const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime(); - if (!sceneRuntime.HasSceneSelection()) { - if (context.GetSession().selection.kind == EditorSelectionKind::HierarchyNode) { - context.ClearSelection(); - } - return; - } - - EditorSelectionState selection = {}; - selection.kind = EditorSelectionKind::HierarchyNode; - selection.itemId = sceneRuntime.GetSelectedItemId(); - selection.displayName = sceneRuntime.GetSelectedDisplayName().empty() - ? selection.itemId - : sceneRuntime.GetSelectedDisplayName(); - context.SetSelection(std::move(selection)); -} - -void ApplyProjectSelection( - EditorContext& context, - const ProjectPanel::Event& event) { - switch (event.kind) { - case ProjectPanel::EventKind::AssetSelected: { - EditorSelectionState selection = {}; - selection.kind = EditorSelectionKind::ProjectItem; - selection.itemId = event.itemId; - selection.displayName = event.displayName.empty() ? event.itemId : event.displayName; - selection.absolutePath = event.absolutePath; - selection.directory = event.directory; - context.SetSelection(std::move(selection)); - return; - } - case ProjectPanel::EventKind::AssetSelectionCleared: - case ProjectPanel::EventKind::FolderNavigated: - if (context.GetSession().selection.kind == EditorSelectionKind::ProjectItem) { - context.ClearSelection(); - } - return; - case ProjectPanel::EventKind::AssetOpened: - case ProjectPanel::EventKind::RenameRequested: - case ProjectPanel::EventKind::ContextMenuRequested: - case ProjectPanel::EventKind::None: - default: - return; - } -} - } // namespace std::vector SyncWorkspaceEvents( EditorContext& context, const EditorShellRuntime& runtime) { std::vector entries = {}; + context.SyncSessionFromProjectRuntime(); + if (const std::optional scenePath = + context.GetProjectRuntime().ConsumePendingSceneOpenPath(); + scenePath.has_value()) { + context.GetSceneRuntime().OpenSceneAsset(scenePath.value()); + } for (const HierarchyPanel::Event& event : runtime.GetHierarchyPanelEvents()) { - ApplyHierarchySelection(context, event); const std::string message = DescribeHierarchyPanelEvent(event); context.SetStatus("Hierarchy", message); entries.push_back(WorkspaceTraceEntry{ std::string(kHierarchyPanelId), std::move(message) }); } for (const ProjectPanel::Event& event : runtime.GetProjectPanelEvents()) { - ApplyProjectSelection(context, event); const std::string message = DescribeProjectPanelEvent(event); context.SetStatus("Project", message); entries.push_back(WorkspaceTraceEntry{ std::string(kProjectPanelId), std::move(message) }); diff --git a/new_editor/app/Features/Console/ConsolePanel.cpp b/new_editor/app/Features/Console/ConsolePanel.cpp index edde8f55..9d50fc60 100644 --- a/new_editor/app/Features/Console/ConsolePanel.cpp +++ b/new_editor/app/Features/Console/ConsolePanel.cpp @@ -1,5 +1,6 @@ #include "ConsolePanel.h" +#include #include #include @@ -14,7 +15,6 @@ using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; -constexpr std::string_view kConsolePanelId = "console"; constexpr float kPadding = 8.0f; constexpr float kLineHeight = 18.0f; constexpr float kFontSize = 11.0f; diff --git a/new_editor/app/Features/Console/ConsolePanel.h b/new_editor/app/Features/Console/ConsolePanel.h index f8216370..b017d4ab 100644 --- a/new_editor/app/Features/Console/ConsolePanel.h +++ b/new_editor/app/Features/Console/ConsolePanel.h @@ -1,6 +1,6 @@ #pragma once -#include "State/EditorSession.h" +#include #include diff --git a/new_editor/app/Features/Hierarchy/HierarchyModel.cpp b/new_editor/app/Features/Hierarchy/HierarchyModel.cpp index 3357c5a3..c7c5ff70 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyModel.cpp +++ b/new_editor/app/Features/Hierarchy/HierarchyModel.cpp @@ -1,5 +1,10 @@ #include "HierarchyModel.h" +#include "Scene/EditorSceneBridge.h" + +#include +#include + #include #include #include @@ -136,35 +141,46 @@ void BuildTreeItemsRecursive( } } +HierarchyNode BuildSceneNodeRecursive( + const ::XCEngine::Components::GameObject& gameObject) { + HierarchyNode node = {}; + node.nodeId = MakeEditorGameObjectItemId(gameObject.GetID()); + node.label = gameObject.GetName().empty() + ? std::string("GameObject") + : gameObject.GetName(); + node.children.reserve(gameObject.GetChildCount()); + for (std::size_t childIndex = 0u; + childIndex < gameObject.GetChildCount(); + ++childIndex) { + const auto* child = gameObject.GetChild(childIndex); + if (child == nullptr) { + continue; + } + + node.children.push_back(BuildSceneNodeRecursive(*child)); + } + + return node; +} + } // namespace -HierarchyModel HierarchyModel::BuildDefault() { +HierarchyModel HierarchyModel::BuildFromScene( + const ::XCEngine::Components::Scene* scene) { HierarchyModel model = {}; - model.m_roots = { - HierarchyNode{ "main_camera", "Main Camera", {} }, - HierarchyNode{ "directional_light", "Directional Light", {} }, - HierarchyNode{ - "player", - "Player", - { - HierarchyNode{ "camera_pivot", "Camera Pivot", {} }, - HierarchyNode{ "player_mesh", "Mesh", {} } - } }, - HierarchyNode{ - "environment", - "Environment", - { - HierarchyNode{ "ground", "Ground", {} }, - HierarchyNode{ - "props", - "Props", - { - HierarchyNode{ "crate_01", "Crate_01", {} }, - HierarchyNode{ "barrel_01", "Barrel_01", {} } - } } - } } - }; - model.m_nextGeneratedNodeId = 1u; + if (scene == nullptr) { + return model; + } + + const auto roots = scene->GetRootGameObjects(); + model.m_roots.reserve(roots.size()); + for (const auto* root : roots) { + if (root == nullptr) { + continue; + } + + model.m_roots.push_back(BuildSceneNodeRecursive(*root)); + } return model; } @@ -172,6 +188,10 @@ bool HierarchyModel::Empty() const { return m_roots.empty(); } +bool HierarchyModel::HasSameTree(const HierarchyModel& other) const { + return m_roots == other.m_roots; +} + bool HierarchyModel::ContainsNode(std::string_view nodeId) const { return FindNode(nodeId) != nullptr; } diff --git a/new_editor/app/Features/Hierarchy/HierarchyModel.h b/new_editor/app/Features/Hierarchy/HierarchyModel.h index daf871df..d8b6afa5 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyModel.h +++ b/new_editor/app/Features/Hierarchy/HierarchyModel.h @@ -4,24 +4,34 @@ #include +#include #include #include #include #include +namespace XCEngine::Components { + +class Scene; + +} // namespace XCEngine::Components + namespace XCEngine::UI::Editor::App { struct HierarchyNode { std::string nodeId = {}; std::string label = {}; std::vector children = {}; + + bool operator==(const HierarchyNode& other) const = default; }; class HierarchyModel { public: - static HierarchyModel BuildDefault(); + static HierarchyModel BuildFromScene(const ::XCEngine::Components::Scene* scene); bool Empty() const; + bool HasSameTree(const HierarchyModel& other) const; bool ContainsNode(std::string_view nodeId) const; const HierarchyNode* FindNode(std::string_view nodeId) const; HierarchyNode* FindNode(std::string_view nodeId); diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h b/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h index ee565c32..7de5e63b 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h +++ b/new_editor/app/Features/Hierarchy/HierarchyPanelInternal.h @@ -4,6 +4,7 @@ #include "Rendering/Assets/BuiltInIcons.h" +#include #include #include @@ -20,7 +21,6 @@ using Widgets::UIEditorTreeViewHitTarget; using Widgets::UIEditorTreeViewHitTargetKind; using Widgets::UIEditorTreeViewInvalidIndex; -inline constexpr std::string_view kHierarchyPanelId = "hierarchy"; inline constexpr float kDragThreshold = 4.0f; inline constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); diff --git a/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h new file mode 100644 index 00000000..11235c7c --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/AudioListenerInspectorComponentEditor.h @@ -0,0 +1,159 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class AudioListenerInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "AudioListener"; + } + + std::string_view GetDisplayName() const override { + return "Audio Listener"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* listener = + dynamic_cast(context.component); + if (listener == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "master_volume"), + "Master Volume", + listener->GetMasterVolume(), + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "mute"), + "Mute", + listener->IsMute())); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "doppler_level"), + "Doppler Level", + listener->GetDopplerLevel(), + 0.1, + 0.0, + 5.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "speed_of_sound"), + "Speed Of Sound", + listener->GetSpeedOfSound(), + 1.0, + 1.0, + 100000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "reverb_level"), + "Reverb Level", + listener->GetReverbLevel(), + 0.05, + 0.0, + 1.0)); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Bool && + field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "mute")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* listener = + dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); + if (listener == nullptr) { + return false; + } + listener->SetMute(field.boolValue); + return true; + }); + } + + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "master_volume")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* listener = + dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); + if (listener == nullptr) { + return false; + } + listener->SetMasterVolume( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "doppler_level")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* listener = + dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); + if (listener == nullptr) { + return false; + } + listener->SetDopplerLevel( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "speed_of_sound")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* listener = + dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); + if (listener == nullptr) { + return false; + } + listener->SetSpeedOfSound( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "reverb_level")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* listener = + dynamic_cast<::XCEngine::Components::AudioListenerComponent*>(&component); + if (listener == nullptr) { + return false; + } + listener->SetReverbLevel( + static_cast(field.numberValue.value)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h new file mode 100644 index 00000000..dc8201ee --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/AudioSourceInspectorComponentEditor.h @@ -0,0 +1,353 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class AudioSourceInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "AudioSource"; + } + + std::string_view GetDisplayName() const override { + return "Audio Source"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* source = + dynamic_cast(context.component); + if (source == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId(context.componentId, "clip"), + "Clip", + source->GetClipPath(), + "None")); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "volume"), + "Volume", + source->GetVolume(), + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "pitch"), + "Pitch", + source->GetPitch(), + 0.05, + 0.0, + 3.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "pan"), + "Pan", + source->GetPan(), + 0.05, + -1.0, + 1.0)); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "looping"), + "Looping", + source->IsLooping())); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "spatialize"), + "Spatialize", + source->IsSpatialize())); + + if (source->IsSpatialize()) { + const ::XCEngine::Audio::Audio3DParams params = source->Get3DParams(); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "min_distance"), + "Min Distance", + params.minDistance, + 0.1, + 0.0, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "max_distance"), + "Max Distance", + params.maxDistance, + 0.1, + 0.0, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "pan_level"), + "Pan Level", + params.panLevel, + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "spread"), + "Spread", + params.spread, + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "reverb_send"), + "Reverb Send", + params.reverbZoneMix, + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "doppler_level"), + "Doppler Level", + params.dopplerLevel, + 0.1, + 0.0, + 10.0)); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "use_hrtf"), + "Use HRTF", + source->IsHRTFEnabled())); + + if (source->IsHRTFEnabled()) { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "hrtf_crossfeed"), + "HRTF Crossfeed", + source->GetHRTFCrossFeed(), + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "hrtf_quality"), + "HRTF Quality", + source->GetHRTFQuality(), + 1.0, + 1.0, + 3.0, + true)); + } + } + + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + auto mutate3DParams = + [&sceneRuntime, &context]( + const std::function& mutator) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&mutator](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + ::XCEngine::Audio::Audio3DParams params = source->Get3DParams(); + mutator(params); + source->Set3DParams(params); + return true; + }); + }; + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "clip") && + field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + + if (field.assetValue.assetId.empty()) { + source->ClearClip(); + } else { + source->SetClipPath(field.assetValue.assetId); + } + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "looping")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + source->SetLooping(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "spatialize")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + source->SetSpatialize(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "use_hrtf")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + source->SetHRTFEnabled(field.boolValue); + return true; + }); + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "volume")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + source->SetVolume(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "pitch")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + source->SetPitch(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "pan")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + source->SetPan(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "min_distance")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.minDistance = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "max_distance")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.maxDistance = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "pan_level")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.panLevel = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "spread")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.spread = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "reverb_send")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.reverbZoneMix = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "doppler_level")) { + return mutate3DParams([&field](::XCEngine::Audio::Audio3DParams& params) { + params.dopplerLevel = static_cast(field.numberValue.value); + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "hrtf_crossfeed")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + source->SetHRTFCrossFeed( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "hrtf_quality")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* source = + dynamic_cast<::XCEngine::Components::AudioSourceComponent*>(&component); + if (source == nullptr) { + return false; + } + source->SetHRTFQuality( + static_cast<::XCEngine::Audio::uint32>(field.numberValue.value)); + return true; + }); + } + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h new file mode 100644 index 00000000..fd143abc --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/BoxColliderInspectorComponentEditor.h @@ -0,0 +1,68 @@ +#pragma once + +#include "Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class BoxColliderInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "BoxCollider"; + } + + std::string_view GetDisplayName() const override { + return "Box Collider"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* collider = + dynamic_cast(context.component); + if (collider == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + AppendColliderBaseFields(context.componentId, *collider, section.fields); + section.fields.push_back(BuildInspectorVector3Field( + BuildInspectorComponentFieldId(context.componentId, "size"), + "Size", + collider->GetSize(), + 0.1)); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { + return true; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "size")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::BoxColliderComponent*>(&component); + if (collider == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { + return false; + } + collider->SetSize(ToMathVector3(field)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h new file mode 100644 index 00000000..a4c4c310 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/CameraInspectorComponentEditor.h @@ -0,0 +1,329 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class CameraInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "Camera"; + } + + std::string_view GetDisplayName() const override { + return "Camera"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* camera = + dynamic_cast(context.component); + if (camera == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorEnumField( + BuildInspectorComponentFieldId(context.componentId, "projection"), + "Projection", + { "Perspective", "Orthographic" }, + static_cast(camera->GetProjectionType()))); + if (camera->GetProjectionType() == + ::XCEngine::Components::CameraProjectionType::Perspective) { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "field_of_view"), + "Field Of View", + camera->GetFieldOfView(), + 0.5, + 1.0, + 179.0)); + } else { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "orthographic_size"), + "Orthographic Size", + camera->GetOrthographicSize(), + 0.1, + 0.001, + 1000000.0)); + } + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "near_clip"), + "Near Clip", + camera->GetNearClipPlane(), + 0.01, + 0.001, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "far_clip"), + "Far Clip", + camera->GetFarClipPlane(), + 0.1, + 0.001, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "depth"), + "Depth", + camera->GetDepth(), + 0.1, + -1000000.0, + 1000000.0)); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "primary"), + "Primary", + camera->IsPrimary())); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "skybox"), + "Skybox", + camera->IsSkyboxEnabled())); + if (camera->IsSkyboxEnabled()) { + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId(context.componentId, "skybox_material"), + "Skybox Material", + camera->GetSkyboxMaterialPath(), + "None")); + if (camera->GetSkyboxMaterialPath().empty()) { + section.fields.push_back(BuildInspectorColorField( + BuildInspectorComponentFieldId(context.componentId, "skybox_top"), + "Skybox Top", + camera->GetSkyboxTopColor())); + section.fields.push_back(BuildInspectorColorField( + BuildInspectorComponentFieldId(context.componentId, "skybox_horizon"), + "Skybox Horizon", + camera->GetSkyboxHorizonColor())); + section.fields.push_back(BuildInspectorColorField( + BuildInspectorComponentFieldId(context.componentId, "skybox_bottom"), + "Skybox Bottom", + camera->GetSkyboxBottomColor())); + } + } + section.fields.push_back(BuildInspectorColorField( + BuildInspectorComponentFieldId(context.componentId, "clear_color"), + "Clear Color", + camera->GetClearColor())); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "projection")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + camera->SetProjectionType( + static_cast<::XCEngine::Components::CameraProjectionType>( + field.enumValue.selectedIndex)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "primary")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + camera->SetPrimary(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "skybox")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + camera->SetSkyboxEnabled(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "skybox_material") && + field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetSkyboxMaterialPath(field.assetValue.assetId); + return true; + }); + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "field_of_view")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetFieldOfView( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "orthographic_size")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetOrthographicSize( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "near_clip")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetNearClipPlane( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "far_clip")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetFarClipPlane( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "depth")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetDepth(static_cast(field.numberValue.value)); + return true; + }); + } + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Color) { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "clear_color")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetClearColor(ToMathColor(field)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "skybox_top")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetSkyboxTopColor(ToMathColor(field)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "skybox_horizon")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetSkyboxHorizonColor(ToMathColor(field)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "skybox_bottom")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* camera = + dynamic_cast<::XCEngine::Components::CameraComponent*>(&component); + if (camera == nullptr) { + return false; + } + camera->SetSkyboxBottomColor(ToMathColor(field)); + return true; + }); + } + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h new file mode 100644 index 00000000..613f94b5 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h @@ -0,0 +1,118 @@ +#pragma once + +#include "Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class CapsuleColliderInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "CapsuleCollider"; + } + + std::string_view GetDisplayName() const override { + return "Capsule Collider"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* collider = + dynamic_cast(context.component); + if (collider == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + AppendColliderBaseFields(context.componentId, *collider, section.fields); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "radius"), + "Radius", + collider->GetRadius(), + 0.05, + 0.0001, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "height"), + "Height", + collider->GetHeight(), + 0.05, + 0.0001, + 1000000.0)); + section.fields.push_back(BuildInspectorEnumField( + BuildInspectorComponentFieldId(context.componentId, "axis"), + "Axis", + { "X", "Y", "Z" }, + static_cast(collider->GetAxis()))); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { + return true; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "axis")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); + if (collider == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + collider->SetAxis( + static_cast<::XCEngine::Components::ColliderAxis>( + field.enumValue.selectedIndex)); + return true; + }); + } + + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "radius")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); + if (collider == nullptr) { + return false; + } + collider->SetRadius(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "height")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::CapsuleColliderComponent*>(&component); + if (collider == nullptr) { + return false; + } + collider->SetHeight(static_cast(field.numberValue.value)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h b/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h new file mode 100644 index 00000000..9b90ba39 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h @@ -0,0 +1,133 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +inline void AppendColliderBaseFields( + std::string_view componentId, + const ::XCEngine::Components::ColliderComponent& collider, + std::vector& fields) { + fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(componentId, "is_trigger"), + "Is Trigger", + collider.IsTrigger())); + fields.push_back(BuildInspectorVector3Field( + BuildInspectorComponentFieldId(componentId, "center"), + "Center", + collider.GetCenter(), + 0.1)); + fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(componentId, "static_friction"), + "Static Friction", + collider.GetStaticFriction(), + 0.05, + 0.0, + 1000.0)); + fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(componentId, "dynamic_friction"), + "Dynamic Friction", + collider.GetDynamicFriction(), + 0.05, + 0.0, + 1000.0)); + fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(componentId, "restitution"), + "Restitution", + collider.GetRestitution(), + 0.05, + 0.0, + 1.0)); +} + +inline bool ApplyColliderBaseFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "is_trigger")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); + if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + collider->SetTrigger(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "center")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); + if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Vector3) { + return false; + } + collider->SetCenter(ToMathVector3(field)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "static_friction")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); + if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + collider->SetStaticFriction( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "dynamic_friction")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); + if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + collider->SetDynamicFriction( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "restitution")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::ColliderComponent*>(&component); + if (collider == nullptr || field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + collider->SetRestitution( + static_cast(field.numberValue.value)); + return true; + }); + } + + return false; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h new file mode 100644 index 00000000..04843f7a --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/IInspectorComponentEditor.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Scene/EditorSceneRuntime.h" + +#include + +#include +#include + +namespace XCEngine::Components { + +class Component; +class GameObject; + +} // namespace XCEngine::Components + +namespace XCEngine::UI::Editor::App { + +struct InspectorComponentEditorContext { + const ::XCEngine::Components::GameObject* gameObject = nullptr; + const ::XCEngine::Components::Component* component = nullptr; + std::string componentId = {}; + std::string typeName = {}; + std::string displayName = {}; + bool removable = false; +}; + +class IInspectorComponentEditor { +public: + virtual ~IInspectorComponentEditor() = default; + + virtual std::string_view GetComponentTypeName() const = 0; + virtual std::string_view GetDisplayName() const = 0; + + virtual void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const = 0; + + virtual bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const = 0; + + virtual bool CanRemove( + const InspectorComponentEditorContext& context) const { + return context.removable; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp new file mode 100644 index 00000000..7da63917 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp @@ -0,0 +1,64 @@ +#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" + +#include "Features/Inspector/Components/AudioListenerInspectorComponentEditor.h" +#include "Features/Inspector/Components/AudioSourceInspectorComponentEditor.h" +#include "Features/Inspector/Components/BoxColliderInspectorComponentEditor.h" +#include "Features/Inspector/Components/CameraInspectorComponentEditor.h" +#include "Features/Inspector/Components/CapsuleColliderInspectorComponentEditor.h" +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/LightInspectorComponentEditor.h" +#include "Features/Inspector/Components/MeshFilterInspectorComponentEditor.h" +#include "Features/Inspector/Components/MeshRendererInspectorComponentEditor.h" +#include "Features/Inspector/Components/RigidbodyInspectorComponentEditor.h" +#include "Features/Inspector/Components/SphereColliderInspectorComponentEditor.h" +#include "Features/Inspector/Components/TransformInspectorComponentEditor.h" +#include "Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h" + +namespace XCEngine::UI::Editor::App { + +const InspectorComponentEditorRegistry& InspectorComponentEditorRegistry::Get() { + static const InspectorComponentEditorRegistry kRegistry = {}; + return kRegistry; +} + +InspectorComponentEditorRegistry::InspectorComponentEditorRegistry() { + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); +} + +void InspectorComponentEditorRegistry::RegisterEditor( + std::unique_ptr editor) { + if (editor == nullptr) { + return; + } + + const std::string typeName(editor->GetComponentTypeName()); + const IInspectorComponentEditor* editorPtr = editor.get(); + m_editorsByType.insert_or_assign(typeName, editorPtr); + m_editors.push_back(std::move(editor)); +} + +const IInspectorComponentEditor* InspectorComponentEditorRegistry::FindEditor( + std::string_view componentTypeName) const { + const auto it = m_editorsByType.find(std::string(componentTypeName)); + return it != m_editorsByType.end() + ? it->second + : nullptr; +} + +const std::vector>& +InspectorComponentEditorRegistry::GetEditors() const { + return m_editors; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.h b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.h new file mode 100644 index 00000000..cc6cbc22 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorRegistry.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class IInspectorComponentEditor; + +class InspectorComponentEditorRegistry { +public: + static const InspectorComponentEditorRegistry& Get(); + + const IInspectorComponentEditor* FindEditor( + std::string_view componentTypeName) const; + const std::vector>& GetEditors() const; + +private: + InspectorComponentEditorRegistry(); + void RegisterEditor(std::unique_ptr editor); + + std::unordered_map m_editorsByType = {}; + std::vector> m_editors = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h new file mode 100644 index 00000000..3be6f2e3 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/InspectorComponentEditorUtils.h @@ -0,0 +1,216 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +inline std::string BuildInspectorComponentSectionId( + std::string_view componentId, + std::string_view sectionSuffix = {}) { + std::string sectionId = "component."; + sectionId += componentId; + if (!sectionSuffix.empty()) { + sectionId.push_back('.'); + sectionId += sectionSuffix; + } + return sectionId; +} + +inline std::string BuildInspectorComponentFieldId( + std::string_view componentId, + std::string_view fieldName) { + std::string fieldId = "component."; + fieldId += componentId; + fieldId.push_back('.'); + fieldId += fieldName; + return fieldId; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorReadOnlyTextField( + std::string fieldId, + std::string label, + std::string value) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Text; + field.valueText = std::move(value); + field.readOnly = true; + return field; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorTextField( + std::string fieldId, + std::string label, + std::string value, + bool readOnly = false) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Text; + field.valueText = std::move(value); + field.readOnly = readOnly; + return field; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorBoolField( + std::string fieldId, + std::string label, + bool value) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Bool; + field.boolValue = value; + return field; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorNumberField( + std::string fieldId, + std::string label, + double value, + double step, + double minValue, + double maxValue, + bool integerMode = false) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Number; + field.numberValue.value = value; + field.numberValue.step = step; + field.numberValue.minValue = minValue; + field.numberValue.maxValue = maxValue; + field.numberValue.integerMode = integerMode; + return field; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorEnumField( + std::string fieldId, + std::string label, + std::vector options, + std::size_t selectedIndex) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Enum; + field.enumValue.options = std::move(options); + field.enumValue.selectedIndex = selectedIndex; + return field; +} + +inline std::string ResolveInspectorAssetDisplayName(std::string_view assetId) { + const std::size_t separatorIndex = assetId.find_last_of("/\\"); + return separatorIndex == std::string_view::npos + ? std::string(assetId) + : std::string(assetId.substr(separatorIndex + 1u)); +} + +inline std::string ResolveInspectorAssetStatusText(std::string_view assetId) { + const std::size_t separatorIndex = assetId.find_last_of("/\\"); + const std::size_t dotIndex = assetId.find_last_of('.'); + if (dotIndex == std::string_view::npos || + (separatorIndex != std::string_view::npos && dotIndex <= separatorIndex + 1u)) { + return {}; + } + + std::string extension = std::string(assetId.substr(dotIndex + 1u)); + for (char& character : extension) { + character = static_cast(std::toupper(static_cast(character))); + } + return extension; +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorAssetField( + std::string fieldId, + std::string label, + std::string assetId, + std::string emptyText = "None", + bool showPickerButton = false) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Asset; + field.assetValue.displayName = ResolveInspectorAssetDisplayName(assetId); + field.assetValue.statusText = ResolveInspectorAssetStatusText(assetId); + field.assetValue.assetId = std::move(assetId); + field.assetValue.emptyText = std::move(emptyText); + field.assetValue.showPickerButton = showPickerButton; + field.assetValue.showStatusBadge = !field.assetValue.statusText.empty(); + return field; +} + +inline ::XCEngine::UI::UIColor ToUIEditorColor(const ::XCEngine::Math::Color& value) { + return ::XCEngine::UI::UIColor(value.r, value.g, value.b, value.a); +} + +inline ::XCEngine::Math::Color ToMathColor( + const Widgets::UIEditorPropertyGridField& field) { + return ::XCEngine::Math::Color( + field.colorValue.value.r, + field.colorValue.value.g, + field.colorValue.value.b, + field.colorValue.value.a); +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorColorField( + std::string fieldId, + std::string label, + const ::XCEngine::Math::Color& value, + bool showAlpha = true) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Color; + field.colorValue.value = ToUIEditorColor(value); + field.colorValue.showAlpha = showAlpha; + return field; +} + +inline std::array ToInspectorVector3Values( + const ::XCEngine::Math::Vector3& value) { + return { + static_cast(value.x), + static_cast(value.y), + static_cast(value.z) + }; +} + +inline ::XCEngine::Math::Vector3 ToMathVector3( + const Widgets::UIEditorPropertyGridField& field) { + return ::XCEngine::Math::Vector3( + static_cast(field.vector3Value.values[0]), + static_cast(field.vector3Value.values[1]), + static_cast(field.vector3Value.values[2])); +} + +inline Widgets::UIEditorPropertyGridField BuildInspectorVector3Field( + std::string fieldId, + std::string label, + const ::XCEngine::Math::Vector3& value, + double step, + bool integerMode = false, + double minValue = -1000000.0, + double maxValue = 1000000.0) { + Widgets::UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = Widgets::UIEditorPropertyGridFieldKind::Vector3; + field.vector3Value.values = ToInspectorVector3Values(value); + field.vector3Value.step = step; + field.vector3Value.minValue = minValue; + field.vector3Value.maxValue = maxValue; + field.vector3Value.integerMode = integerMode; + return field; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h new file mode 100644 index 00000000..3dc87e54 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/LightInspectorComponentEditor.h @@ -0,0 +1,327 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class LightInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "Light"; + } + + std::string_view GetDisplayName() const override { + return "Light"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* light = + dynamic_cast(context.component); + if (light == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorEnumField( + BuildInspectorComponentFieldId(context.componentId, "type"), + "Type", + { "Directional", "Point", "Spot" }, + static_cast(light->GetLightType()))); + section.fields.push_back(BuildInspectorColorField( + BuildInspectorComponentFieldId(context.componentId, "color"), + "Color", + light->GetColor())); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "intensity"), + "Intensity", + light->GetIntensity(), + 0.1, + 0.0, + 1000000.0)); + + if (light->GetLightType() != ::XCEngine::Components::LightType::Directional) { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "range"), + "Range", + light->GetRange(), + 0.1, + 0.001, + 1000000.0)); + } + + if (light->GetLightType() == ::XCEngine::Components::LightType::Spot) { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "spot_angle"), + "Spot Angle", + light->GetSpotAngle(), + 0.5, + 1.0, + 179.0)); + } + + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + "Cast Shadows", + light->GetCastsShadows())); + + if (light->GetLightType() == ::XCEngine::Components::LightType::Directional && + light->GetCastsShadows()) { + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "override_shadow_params"), + "Override Shadow Params", + light->GetOverridesDirectionalShadowSettings())); + + if (light->GetOverridesDirectionalShadowSettings()) { + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "receiver_depth_bias"), + "Receiver Depth Bias", + light->GetDirectionalShadowReceiverDepthBias(), + 0.0001, + 0.0, + 1000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "normal_bias_scale"), + "Normal Bias Scale", + light->GetDirectionalShadowNormalBiasScale(), + 0.05, + 0.0, + 1000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "shadow_strength"), + "Shadow Strength", + light->GetDirectionalShadowStrength(), + 0.05, + 0.0, + 1.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "depth_bias_factor"), + "Depth Bias Factor", + light->GetDirectionalShadowDepthBiasFactor(), + 0.1, + 0.0, + 1000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "depth_bias_units"), + "Depth Bias Units", + light->GetDirectionalShadowDepthBiasUnits(), + 1.0, + 0.0, + 1000000.0, + true)); + } + } + + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "type")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + light->SetLightType( + static_cast<::XCEngine::Components::LightType>( + field.enumValue.selectedIndex)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + light->SetCastsShadows(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "override_shadow_params")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + light->SetOverridesDirectionalShadowSettings(field.boolValue); + return true; + }); + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Color && + field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "color")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetColor(ToMathColor(field)); + return true; + }); + } + + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "intensity")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetIntensity(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "range")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetRange(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "spot_angle")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetSpotAngle(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "receiver_depth_bias")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetDirectionalShadowReceiverDepthBias( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "normal_bias_scale")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetDirectionalShadowNormalBiasScale( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "shadow_strength")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetDirectionalShadowStrength( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "depth_bias_factor")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetDirectionalShadowDepthBiasFactor( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "depth_bias_units")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* light = + dynamic_cast<::XCEngine::Components::LightComponent*>(&component); + if (light == nullptr) { + return false; + } + light->SetDirectionalShadowDepthBiasUnits( + static_cast(field.numberValue.value)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h new file mode 100644 index 00000000..85940849 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/MeshFilterInspectorComponentEditor.h @@ -0,0 +1,69 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class MeshFilterInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "MeshFilter"; + } + + std::string_view GetDisplayName() const override { + return "Mesh Filter"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* meshFilter = + dynamic_cast(context.component); + if (meshFilter == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId(context.componentId, "mesh"), + "Mesh", + meshFilter->GetMeshPath(), + "None")); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "mesh") && + field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* meshFilter = + dynamic_cast<::XCEngine::Components::MeshFilterComponent*>(&component); + if (meshFilter == nullptr) { + return false; + } + + if (field.assetValue.assetId.empty()) { + meshFilter->ClearMesh(); + } else { + meshFilter->SetMeshPath(field.assetValue.assetId); + } + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h new file mode 100644 index 00000000..04f5d3d7 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/MeshRendererInspectorComponentEditor.h @@ -0,0 +1,183 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class MeshRendererInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "MeshRenderer"; + } + + std::string_view GetDisplayName() const override { + return "Mesh Renderer"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* meshRenderer = + dynamic_cast(context.component); + if (meshRenderer == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + "Cast Shadows", + meshRenderer->GetCastShadows())); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "receive_shadows"), + "Receive Shadows", + meshRenderer->GetReceiveShadows())); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "render_layer"), + "Render Layer", + meshRenderer->GetRenderLayer(), + 1.0, + 0.0, + 4294967295.0, + true)); + + const std::size_t slotCount = GetVisibleMaterialSlotCount(context.gameObject, *meshRenderer); + for (std::size_t slotIndex = 0u; slotIndex < slotCount; ++slotIndex) { + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId( + context.componentId, + "material_" + std::to_string(slotIndex)), + "Material " + std::to_string(slotIndex), + meshRenderer->GetMaterialPath(slotIndex), + "None")); + } + + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* meshRenderer = + dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); + if (meshRenderer == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + meshRenderer->SetCastShadows(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "receive_shadows")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* meshRenderer = + dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); + if (meshRenderer == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + meshRenderer->SetReceiveShadows(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "render_layer") && + field.kind == Widgets::UIEditorPropertyGridFieldKind::Number) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* meshRenderer = + dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); + if (meshRenderer == nullptr) { + return false; + } + meshRenderer->SetRenderLayer( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { + const std::string fieldPrefix = + BuildInspectorComponentFieldId(context.componentId, "material_"); + if (field.fieldId.rfind(fieldPrefix, 0u) == 0u) { + const std::string slotText = + field.fieldId.substr(fieldPrefix.size()); + std::size_t slotIndex = 0u; + try { + slotIndex = static_cast(std::stoull(slotText)); + } catch (...) { + return false; + } + + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field, slotIndex](::XCEngine::Components::Component& component) { + auto* meshRenderer = + dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(&component); + if (meshRenderer == nullptr) { + return false; + } + meshRenderer->SetMaterialPath(slotIndex, field.assetValue.assetId); + return true; + }); + } + } + + return false; + } + +private: + static std::size_t GetVisibleMaterialSlotCount( + const ::XCEngine::Components::GameObject* gameObject, + const ::XCEngine::Components::MeshRendererComponent& meshRenderer) { + std::size_t slotCount = + (std::max)(static_cast(1u), meshRenderer.GetMaterialCount()); + if (gameObject == nullptr) { + return slotCount; + } + + const auto* meshFilter = + gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>(); + if (meshFilter == nullptr) { + return slotCount; + } + + ::XCEngine::Resources::Mesh* mesh = meshFilter->GetMesh(); + if (mesh == nullptr || !mesh->IsValid()) { + return slotCount; + } + + slotCount = + (std::max)(slotCount, static_cast(mesh->GetMaterials().Size())); + const auto& sections = mesh->GetSections(); + for (std::size_t sectionIndex = 0u; sectionIndex < sections.Size(); ++sectionIndex) { + slotCount = (std::max)( + slotCount, + static_cast(sections[sectionIndex].materialID) + 1u); + } + return slotCount; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h new file mode 100644 index 00000000..b6f59a30 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/RigidbodyInspectorComponentEditor.h @@ -0,0 +1,178 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class RigidbodyInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "Rigidbody"; + } + + std::string_view GetDisplayName() const override { + return "Rigidbody"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* rigidbody = + dynamic_cast(context.component); + if (rigidbody == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorEnumField( + BuildInspectorComponentFieldId(context.componentId, "body_type"), + "Body Type", + { "Static", "Dynamic", "Kinematic" }, + static_cast(rigidbody->GetBodyType()))); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "mass"), + "Mass", + rigidbody->GetMass(), + 0.1, + 0.0001, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "linear_damping"), + "Linear Damping", + rigidbody->GetLinearDamping(), + 0.05, + 0.0, + 1000000.0)); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "angular_damping"), + "Angular Damping", + rigidbody->GetAngularDamping(), + 0.05, + 0.0, + 1000000.0)); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "use_gravity"), + "Use Gravity", + rigidbody->GetUseGravity())); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "enable_ccd"), + "Enable CCD", + rigidbody->GetEnableCCD())); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "body_type")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Enum) { + return false; + } + rigidbody->SetBodyType( + static_cast<::XCEngine::Physics::PhysicsBodyType>( + field.enumValue.selectedIndex)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "use_gravity")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + rigidbody->SetUseGravity(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "enable_ccd")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + rigidbody->SetEnableCCD(field.boolValue); + return true; + }); + } + + if (field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "mass")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr) { + return false; + } + rigidbody->SetMass(static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "linear_damping")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr) { + return false; + } + rigidbody->SetLinearDamping( + static_cast(field.numberValue.value)); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "angular_damping")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* rigidbody = + dynamic_cast<::XCEngine::Components::RigidbodyComponent*>(&component); + if (rigidbody == nullptr) { + return false; + } + rigidbody->SetAngularDamping( + static_cast(field.numberValue.value)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h new file mode 100644 index 00000000..82eba96d --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/SphereColliderInspectorComponentEditor.h @@ -0,0 +1,70 @@ +#pragma once + +#include "Features/Inspector/Components/ColliderInspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class SphereColliderInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "SphereCollider"; + } + + std::string_view GetDisplayName() const override { + return "Sphere Collider"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* collider = + dynamic_cast(context.component); + if (collider == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + AppendColliderBaseFields(context.componentId, *collider, section.fields); + section.fields.push_back(BuildInspectorNumberField( + BuildInspectorComponentFieldId(context.componentId, "radius"), + "Radius", + collider->GetRadius(), + 0.05, + 0.0001, + 1000000.0)); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (ApplyColliderBaseFieldValue(sceneRuntime, context, field)) { + return true; + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "radius")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* collider = + dynamic_cast<::XCEngine::Components::SphereColliderComponent*>(&component); + if (collider == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Number) { + return false; + } + collider->SetRadius(static_cast(field.numberValue.value)); + return true; + }); + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp new file mode 100644 index 00000000..a1aca63b --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp @@ -0,0 +1,141 @@ +#include "Features/Inspector/Components/TransformInspectorComponentEditor.h" + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::TransformComponent; +using ::XCEngine::Math::Vector3; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridSection; + +std::string BuildTransformFieldId( + std::string_view componentId, + std::string_view fieldName) { + std::string fieldId = "component."; + fieldId += componentId; + fieldId += '.'; + fieldId += fieldName; + return fieldId; +} + +std::string BuildTransformSectionId(std::string_view componentId) { + std::string sectionId = "component."; + sectionId += componentId; + return sectionId; +} + +std::array ToPropertyValues(const Vector3& value) { + return { + static_cast(value.x), + static_cast(value.y), + static_cast(value.z) + }; +} + +Vector3 ToVector3(const UIEditorPropertyGridField& field) { + return Vector3( + static_cast(field.vector3Value.values[0]), + static_cast(field.vector3Value.values[1]), + static_cast(field.vector3Value.values[2])); +} + +UIEditorPropertyGridField BuildTransformVectorField( + std::string fieldId, + std::string label, + const Vector3& value, + double step) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Vector3; + field.vector3Value.values = ToPropertyValues(value); + field.vector3Value.step = step; + field.vector3Value.minValue = -1000000.0; + field.vector3Value.maxValue = 1000000.0; + field.vector3Value.integerMode = false; + return field; +} + +} // namespace + +std::string_view TransformInspectorComponentEditor::GetComponentTypeName() const { + return "Transform"; +} + +std::string_view TransformInspectorComponentEditor::GetDisplayName() const { + return "Transform"; +} + +void TransformInspectorComponentEditor::BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const { + const auto* transform = + dynamic_cast(context.component); + if (transform == nullptr) { + return; + } + + UIEditorPropertyGridSection section = {}; + section.sectionId = BuildTransformSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildTransformVectorField( + BuildTransformFieldId(context.componentId, "position"), + "Position", + transform->GetLocalPosition(), + 0.1)); + section.fields.push_back(BuildTransformVectorField( + BuildTransformFieldId(context.componentId, "rotation"), + "Rotation", + transform->GetLocalEulerAngles(), + 1.0)); + section.fields.push_back(BuildTransformVectorField( + BuildTransformFieldId(context.componentId, "scale"), + "Scale", + transform->GetLocalScale(), + 0.1)); + outSections.push_back(std::move(section)); +} + +bool TransformInspectorComponentEditor::ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const UIEditorPropertyGridField& field) const { + if (field.kind != UIEditorPropertyGridFieldKind::Vector3) { + return false; + } + + if (field.fieldId == + BuildTransformFieldId(context.componentId, "position")) { + return sceneRuntime.SetSelectedTransformLocalPosition( + context.componentId, + ToVector3(field)); + } + + if (field.fieldId == + BuildTransformFieldId(context.componentId, "rotation")) { + return sceneRuntime.SetSelectedTransformLocalEulerAngles( + context.componentId, + ToVector3(field)); + } + + if (field.fieldId == + BuildTransformFieldId(context.componentId, "scale")) { + return sceneRuntime.SetSelectedTransformLocalScale( + context.componentId, + ToVector3(field)); + } + + return false; +} + +bool TransformInspectorComponentEditor::CanRemove( + const InspectorComponentEditorContext& context) const { + (void)context; + return false; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h new file mode 100644 index 00000000..39faa660 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/TransformInspectorComponentEditor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" + +namespace XCEngine::UI::Editor::App { + +class TransformInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override; + std::string_view GetDisplayName() const override; + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override; + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override; + + bool CanRemove( + const InspectorComponentEditorContext& context) const override; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h b/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h new file mode 100644 index 00000000..da53b2e2 --- /dev/null +++ b/new_editor/app/Features/Inspector/Components/VolumeRendererInspectorComponentEditor.h @@ -0,0 +1,135 @@ +#pragma once + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorUtils.h" + +#include + +namespace XCEngine::UI::Editor::App { + +class VolumeRendererInspectorComponentEditor final : public IInspectorComponentEditor { +public: + std::string_view GetComponentTypeName() const override { + return "VolumeRenderer"; + } + + std::string_view GetDisplayName() const override { + return "Volume Renderer"; + } + + void BuildSections( + const InspectorComponentEditorContext& context, + std::vector& outSections) const override { + const auto* volumeRenderer = + dynamic_cast(context.component); + if (volumeRenderer == nullptr) { + return; + } + + Widgets::UIEditorPropertyGridSection section = {}; + section.sectionId = BuildInspectorComponentSectionId(context.componentId); + section.title = std::string(GetDisplayName()); + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId(context.componentId, "volume_field"), + "Volume Field", + volumeRenderer->GetVolumeFieldPath(), + "None")); + section.fields.push_back(BuildInspectorAssetField( + BuildInspectorComponentFieldId(context.componentId, "material"), + "Material", + volumeRenderer->GetMaterialPath(), + "None")); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "cast_shadows"), + "Cast Shadows", + volumeRenderer->GetCastShadows())); + section.fields.push_back(BuildInspectorBoolField( + BuildInspectorComponentFieldId(context.componentId, "receive_shadows"), + "Receive Shadows", + volumeRenderer->GetReceiveShadows())); + outSections.push_back(std::move(section)); + } + + bool ApplyFieldValue( + EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorContext& context, + const Widgets::UIEditorPropertyGridField& field) const override { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "cast_shadows")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* volumeRenderer = + dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); + if (volumeRenderer == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + volumeRenderer->SetCastShadows(field.boolValue); + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "receive_shadows")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* volumeRenderer = + dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); + if (volumeRenderer == nullptr || + field.kind != Widgets::UIEditorPropertyGridFieldKind::Bool) { + return false; + } + volumeRenderer->SetReceiveShadows(field.boolValue); + return true; + }); + } + + if (field.kind == Widgets::UIEditorPropertyGridFieldKind::Asset) { + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "volume_field")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* volumeRenderer = + dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); + if (volumeRenderer == nullptr) { + return false; + } + + if (field.assetValue.assetId.empty()) { + volumeRenderer->ClearVolumeField(); + } else { + volumeRenderer->SetVolumeFieldPath(field.assetValue.assetId); + } + return true; + }); + } + + if (field.fieldId == + BuildInspectorComponentFieldId(context.componentId, "material")) { + return sceneRuntime.ApplySelectedComponentMutation( + context.componentId, + [&field](::XCEngine::Components::Component& component) { + auto* volumeRenderer = + dynamic_cast<::XCEngine::Components::VolumeRendererComponent*>(&component); + if (volumeRenderer == nullptr) { + return false; + } + + if (field.assetValue.assetId.empty()) { + volumeRenderer->ClearMaterial(); + } else { + volumeRenderer->SetMaterialPath(field.assetValue.assetId); + } + return true; + }); + } + } + + return false; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorPanel.cpp b/new_editor/app/Features/Inspector/InspectorPanel.cpp index 1cae469e..4bc67b9d 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.cpp +++ b/new_editor/app/Features/Inspector/InspectorPanel.cpp @@ -1,11 +1,15 @@ #include "InspectorPanel.h" -#include "Scene/EditorSceneRuntime.h" - #include +#include #include +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" +#include "Scene/EditorSceneRuntime.h" + #include +#include namespace XCEngine::UI::Editor::App { @@ -13,35 +17,94 @@ namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; constexpr float kPanelPadding = 10.0f; -constexpr float kHeaderHeight = 22.0f; -constexpr float kSectionGap = 10.0f; -constexpr float kRowHeight = 21.0f; -constexpr float kLabelWidth = 88.0f; +constexpr float kTitleHeight = 18.0f; +constexpr float kSubtitleHeight = 16.0f; +constexpr float kHeaderGap = 10.0f; constexpr float kTitleFontSize = 13.0f; constexpr float kSubtitleFontSize = 11.0f; -constexpr float kSectionTitleFontSize = 11.0f; -constexpr float kRowFontSize = 11.0f; - -constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); -constexpr UIColor kSectionHeaderColor(0.11f, 0.11f, 0.11f, 1.0f); -constexpr UIColor kSectionBodyColor(0.10f, 0.10f, 0.10f, 1.0f); constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f); constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 1.0f); -constexpr UIColor kLabelColor(0.720f, 0.720f, 0.720f, 1.0f); -constexpr UIColor kValueColor(0.900f, 0.900f, 0.900f, 1.0f); +constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); + +bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} float ResolveTextTop(float rectY, float rectHeight, float fontSize) { const float lineHeight = fontSize * 1.6f; return rectY + std::floor((rectHeight - lineHeight) * 0.5f); } -std::string PathToUtf8String(const std::filesystem::path& path) { - const std::u8string value = path.u8string(); - return std::string(value.begin(), value.end()); +std::vector FilterInspectorInputEvents( + const UIRect& bounds, + const std::vector& inputEvents, + bool allowInteraction, + bool panelActive) { + if (!allowInteraction && !panelActive) { + return {}; + } + + std::vector filteredEvents = {}; + filteredEvents.reserve(inputEvents.size()); + for (const UIInputEvent& event : inputEvents) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + if (allowInteraction && ContainsPoint(bounds, event.position)) { + filteredEvents.push_back(event); + } + break; + case UIInputEventType::PointerLeave: + filteredEvents.push_back(event); + break; + case UIInputEventType::FocusGained: + case UIInputEventType::FocusLost: + if (panelActive) { + filteredEvents.push_back(event); + } + break; + case UIInputEventType::KeyDown: + case UIInputEventType::KeyUp: + case UIInputEventType::Character: + if (panelActive) { + filteredEvents.push_back(event); + } + break; + default: + break; + } + } + + return filteredEvents; +} + +UIEditorHostCommandEvaluationResult BuildEvaluationResult( + bool executable, + std::string message) { + UIEditorHostCommandEvaluationResult result = {}; + result.executable = executable; + result.message = std::move(message); + return result; +} + +UIEditorHostCommandDispatchResult BuildDispatchResult( + bool commandExecuted, + std::string message) { + UIEditorHostCommandDispatchResult result = {}; + result.commandExecuted = commandExecuted; + result.message = std::move(message); + return result; } } // namespace @@ -57,96 +120,228 @@ const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPa return nullptr; } -void InspectorPanel::BuildPresentation( - const EditorSession& session, - const EditorSceneRuntime& sceneRuntime) { - m_sections.clear(); - m_title.clear(); - m_subtitle.clear(); - m_hasSelection = false; +void InspectorPanel::ResetPanelState() { + m_visible = false; + m_bounds = {}; + m_subject = {}; + m_subjectKey.clear(); + m_presentation = {}; + m_gridFrame = {}; + m_knownSectionIds.clear(); + ResetInteractionState(); +} - if (session.selection.kind == EditorSelectionKind::ProjectItem) { - m_hasSelection = true; - m_title = session.selection.displayName.empty() - ? (session.selection.directory ? std::string("Folder") : std::string("Asset")) - : session.selection.displayName; - m_subtitle = session.selection.directory ? "Folder" : "Asset"; +void InspectorPanel::ResetInteractionState() { + m_fieldSelection.ClearSelection(); + m_sectionExpansion.Clear(); + m_propertyEditModel = {}; + m_interactionState = {}; + m_gridFrame = {}; +} - Section identity = {}; - identity.title = "Identity"; - identity.rows = { - { "Type", session.selection.directory ? std::string("Folder") : std::string("Asset") }, - { "Name", m_title }, - { "Id", session.selection.itemId } - }; - m_sections.push_back(std::move(identity)); - - Section location = {}; - location.title = "Location"; - location.rows = { - { "Path", PathToUtf8String(session.selection.absolutePath) } - }; - m_sections.push_back(std::move(location)); - return; +void InspectorPanel::SyncExpansionState(bool subjectChanged) { + if (subjectChanged) { + m_sectionExpansion.Clear(); + m_knownSectionIds.clear(); } - if (session.selection.kind == EditorSelectionKind::HierarchyNode && - sceneRuntime.HasSceneSelection()) { - const auto* selectedGameObject = sceneRuntime.GetSelectedGameObject(); - m_hasSelection = true; - m_title = sceneRuntime.GetSelectedDisplayName().empty() - ? std::string("GameObject") - : sceneRuntime.GetSelectedDisplayName(); - m_subtitle = "GameObject"; - - Section identity = {}; - identity.title = "Identity"; - identity.rows = { - { "Type", "GameObject" }, - { "Name", m_title }, - { "Id", sceneRuntime.GetSelectedItemId() } - }; - m_sections.push_back(std::move(identity)); - - if (selectedGameObject != nullptr) { - Section hierarchy = {}; - hierarchy.title = "Hierarchy"; - hierarchy.rows = { - { "Children", std::to_string(selectedGameObject->GetChildCount()) }, - { "Parent", selectedGameObject->GetParent() != nullptr - ? (selectedGameObject->GetParent()->GetName().empty() - ? std::string("GameObject") - : selectedGameObject->GetParent()->GetName()) - : std::string("Scene Root") } - }; - m_sections.push_back(std::move(hierarchy)); + std::unordered_set currentSectionIds = {}; + for (const Widgets::UIEditorPropertyGridSection& section : + m_presentation.sections) { + currentSectionIds.insert(section.sectionId); + if (m_knownSectionIds.find(section.sectionId) == m_knownSectionIds.end()) { + m_sectionExpansion.Expand(section.sectionId); } - return; } - m_title = "Nothing selected"; - m_subtitle = "Select a hierarchy item or project asset."; + m_knownSectionIds = std::move(currentSectionIds); +} + +void InspectorPanel::SyncSelectionState() { + if (m_fieldSelection.HasSelection() && + !Widgets::FindUIEditorPropertyGridFieldLocation( + m_presentation.sections, + m_fieldSelection.GetSelectedId()) + .IsValid()) { + m_fieldSelection.ClearSelection(); + } + + if (m_propertyEditModel.HasActiveEdit() && + !Widgets::FindUIEditorPropertyGridFieldLocation( + m_presentation.sections, + m_propertyEditModel.GetActiveFieldId()) + .IsValid()) { + m_propertyEditModel.CancelEdit(); + m_interactionState.textInputState = {}; + } + + if (!m_interactionState.propertyGridState.popupFieldId.empty() && + !Widgets::FindUIEditorPropertyGridFieldLocation( + m_presentation.sections, + m_interactionState.propertyGridState.popupFieldId) + .IsValid()) { + m_interactionState.propertyGridState.popupFieldId.clear(); + } +} + +std::string InspectorPanel::BuildSubjectKey() const { + switch (m_subject.kind) { + case InspectorSubjectKind::ProjectAsset: + return std::string("project:") + m_subject.projectAsset.selection.itemId; + case InspectorSubjectKind::SceneObject: + return std::string("scene:") + m_subject.sceneObject.itemId; + case InspectorSubjectKind::None: + default: + return "none"; + } +} + +UIRect InspectorPanel::BuildGridBounds() const { + const float x = m_bounds.x + kPanelPadding; + const float width = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); + const float y = + m_bounds.y + kPanelPadding + kTitleHeight + kSubtitleHeight + kHeaderGap; + const float height = + (std::max)(m_bounds.y + m_bounds.height - kPanelPadding - y, 0.0f); + return UIRect(x, y, width, height); +} + +const InspectorPresentationComponentBinding* InspectorPanel::FindSelectedComponentBinding() const { + if (!m_fieldSelection.HasSelection()) { + return nullptr; + } + + const std::string& fieldId = m_fieldSelection.GetSelectedId(); + for (const InspectorPresentationComponentBinding& binding : + m_presentation.componentBindings) { + for (const std::string& ownedFieldId : binding.fieldIds) { + if (ownedFieldId == fieldId) { + return &binding; + } + } + } + + return nullptr; +} + +bool InspectorPanel::ApplyChangedField(std::string_view fieldId) { + if (m_sceneRuntime == nullptr || + m_subject.kind != InspectorSubjectKind::SceneObject) { + return false; + } + + const auto location = + Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= m_presentation.sections.size() || + location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) { + return false; + } + + const Widgets::UIEditorPropertyGridField& field = + m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; + const InspectorPresentationComponentBinding* binding = nullptr; + for (const InspectorPresentationComponentBinding& candidate : + m_presentation.componentBindings) { + if (std::find( + candidate.fieldIds.begin(), + candidate.fieldIds.end(), + field.fieldId) != candidate.fieldIds.end()) { + binding = &candidate; + break; + } + } + if (binding == nullptr) { + return false; + } + + const IInspectorComponentEditor* editor = + InspectorComponentEditorRegistry::Get().FindEditor(binding->typeName); + if (editor == nullptr) { + return false; + } + + InspectorComponentEditorContext context = {}; + context.gameObject = m_subject.sceneObject.gameObject; + context.componentId = binding->componentId; + context.typeName = binding->typeName; + context.displayName = binding->displayName; + context.removable = binding->removable; + for (const EditorSceneComponentDescriptor& descriptor : + m_sceneRuntime->GetSelectedComponents()) { + if (descriptor.componentId == binding->componentId) { + context.component = descriptor.component; + break; + } + } + + return editor->ApplyFieldValue(*m_sceneRuntime, context, field); } void InspectorPanel::Update( const EditorSession& session, - const EditorSceneRuntime& sceneRuntime, - const UIEditorPanelContentHostFrame& contentHostFrame) { + EditorSceneRuntime& sceneRuntime, + const UIEditorPanelContentHostFrame& contentHostFrame, + const std::vector& inputEvents, + bool allowInteraction, + bool panelActive) { const UIEditorPanelContentHostPanelState* panelState = FindMountedInspectorPanel(contentHostFrame); if (panelState == nullptr) { - m_visible = false; - m_bounds = {}; - m_sections.clear(); - m_title.clear(); - m_subtitle.clear(); - m_hasSelection = false; + ResetPanelState(); return; } m_visible = true; m_bounds = panelState->bounds; - BuildPresentation(session, sceneRuntime); + m_sceneRuntime = &sceneRuntime; + m_subject = BuildInspectorSubject(session, sceneRuntime); + + const std::string nextSubjectKey = BuildSubjectKey(); + const bool subjectChanged = m_subjectKey != nextSubjectKey; + m_subjectKey = nextSubjectKey; + if (subjectChanged) { + ResetInteractionState(); + } + + m_presentation = BuildInspectorPresentationModel( + m_subject, + sceneRuntime, + InspectorComponentEditorRegistry::Get()); + SyncExpansionState(subjectChanged); + SyncSelectionState(); + + if (m_presentation.sections.empty()) { + m_gridFrame = {}; + return; + } + + const std::vector filteredEvents = + FilterInspectorInputEvents( + m_bounds, + inputEvents, + allowInteraction, + panelActive); + m_gridFrame = UpdateUIEditorPropertyGridInteraction( + m_interactionState, + m_fieldSelection, + m_sectionExpansion, + m_propertyEditModel, + BuildGridBounds(), + m_presentation.sections, + filteredEvents, + ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); + + if (m_gridFrame.result.fieldValueChanged && + !m_gridFrame.result.changedFieldId.empty() && + !ApplyChangedField(m_gridFrame.result.changedFieldId)) { + m_presentation = BuildInspectorPresentationModel( + m_subject, + sceneRuntime, + InspectorComponentEditorRegistry::Get()); + SyncExpansionState(false); + SyncSelectionState(); + } } void InspectorPanel::Append(UIDrawList& drawList) const { @@ -154,88 +349,105 @@ void InspectorPanel::Append(UIDrawList& drawList) const { return; } - const UIColor borderColor = ResolveUIEditorDockHostPalette().splitterColor; drawList.AddFilledRect(m_bounds, kSurfaceColor); const float contentX = m_bounds.x + kPanelPadding; const float contentWidth = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); float nextY = m_bounds.y + kPanelPadding; - const UIRect titleRect(contentX, nextY, contentWidth, 18.0f); + const UIRect titleRect(contentX, nextY, contentWidth, kTitleHeight); drawList.AddText( UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)), - m_title, + m_presentation.title, kTitleColor, kTitleFontSize); nextY += titleRect.height; - const UIRect subtitleRect(contentX, nextY, contentWidth, 16.0f); + const UIRect subtitleRect(contentX, nextY, contentWidth, kSubtitleHeight); drawList.AddText( - UIPoint(subtitleRect.x, ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), - m_subtitle, + UIPoint( + subtitleRect.x, + ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), + m_presentation.subtitle, kSubtitleColor, kSubtitleFontSize); - nextY += subtitleRect.height + kSectionGap; - if (!m_hasSelection) { + if (m_presentation.sections.empty()) { return; } - for (const Section& section : m_sections) { - const float bodyHeight = static_cast(section.rows.size()) * kRowHeight; - const UIRect headerRect(contentX, nextY, contentWidth, kHeaderHeight); - const UIRect bodyRect(contentX, headerRect.y + headerRect.height, contentWidth, bodyHeight); + Widgets::AppendUIEditorPropertyGrid( + drawList, + BuildGridBounds(), + m_presentation.sections, + m_fieldSelection, + m_sectionExpansion, + m_propertyEditModel, + m_interactionState.propertyGridState, + ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(), + ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); +} - drawList.AddFilledRect(headerRect, kSectionHeaderColor); - drawList.AddFilledRect(bodyRect, kSectionBodyColor); - drawList.AddRectOutline( - UIRect(headerRect.x, headerRect.y, headerRect.width, headerRect.height + bodyRect.height), - borderColor, - 1.0f, - 0.0f); - drawList.AddText( - UIPoint(headerRect.x + 8.0f, ResolveTextTop(headerRect.y, headerRect.height, kSectionTitleFontSize)), - section.title, - kTitleColor, - kSectionTitleFontSize); +UIEditorHostCommandEvaluationResult InspectorPanel::EvaluateEditCommand( + std::string_view commandId) const { + const InspectorPresentationComponentBinding* binding = + FindSelectedComponentBinding(); + if (binding == nullptr) { + return BuildEvaluationResult( + false, + "Select an inspector component field first."); + } - float rowY = bodyRect.y; - for (std::size_t rowIndex = 0u; rowIndex < section.rows.size(); ++rowIndex) { - const SectionRow& row = section.rows[rowIndex]; - const UIRect rowRect(contentX, rowY, contentWidth, kRowHeight); - const UIRect labelRect(rowRect.x + 8.0f, rowRect.y, kLabelWidth, rowRect.height); - const UIRect valueRect( - labelRect.x + labelRect.width + 8.0f, - rowRect.y, - (std::max)(rowRect.width - (labelRect.width + 24.0f), 0.0f), - rowRect.height); - - if (rowIndex > 0u) { - drawList.AddFilledRect( - UIRect(rowRect.x, rowRect.y, rowRect.width, 1.0f), - borderColor); - } - - drawList.AddText( - UIPoint(labelRect.x, ResolveTextTop(labelRect.y, labelRect.height, kRowFontSize)), - row.label, - kLabelColor, - kRowFontSize); - - drawList.PushClipRect(valueRect); - drawList.AddText( - UIPoint(valueRect.x, ResolveTextTop(valueRect.y, valueRect.height, kRowFontSize)), - row.value, - kValueColor, - kRowFontSize); - drawList.PopClipRect(); - - rowY += kRowHeight; + if (commandId == "edit.delete") { + if (!binding->removable || m_sceneRuntime == nullptr || + !m_sceneRuntime->CanRemoveSelectedComponent(binding->componentId)) { + return BuildEvaluationResult( + false, + "'" + binding->displayName + "' cannot be removed."); } - nextY = bodyRect.y + bodyRect.height + kSectionGap; + return BuildEvaluationResult( + true, + "Remove inspector component '" + binding->displayName + "'."); } + + return BuildEvaluationResult( + false, + "Inspector does not expose this edit command."); +} + +UIEditorHostCommandDispatchResult InspectorPanel::DispatchEditCommand( + std::string_view commandId) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateEditCommand(commandId); + if (!evaluation.executable) { + return BuildDispatchResult(false, evaluation.message); + } + + const InspectorPresentationComponentBinding* binding = + FindSelectedComponentBinding(); + if (binding == nullptr || m_sceneRuntime == nullptr) { + return BuildDispatchResult( + false, + "Inspector component route is unavailable."); + } + + if (commandId == "edit.delete") { + if (!m_sceneRuntime->RemoveSelectedComponent(binding->componentId)) { + return BuildDispatchResult( + false, + "Failed to remove inspector component."); + } + + ResetInteractionState(); + return BuildDispatchResult( + true, + "Removed inspector component '" + binding->displayName + "'."); + } + + return BuildDispatchResult( + false, + "Inspector does not expose this edit command."); } } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/Features/Inspector/InspectorPanel.h b/new_editor/app/Features/Inspector/InspectorPanel.h index db51fe61..1c2e7f61 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.h +++ b/new_editor/app/Features/Inspector/InspectorPanel.h @@ -1,50 +1,65 @@ #pragma once -#include +#include "Features/Inspector/InspectorPresentationModel.h" +#include "Features/Inspector/InspectorSubject.h" +#include +#include #include #include +#include +#include +#include #include +#include #include namespace XCEngine::UI::Editor::App { class EditorSceneRuntime; -class InspectorPanel { +class InspectorPanel final : public EditorEditCommandRoute { public: void Update( const EditorSession& session, - const EditorSceneRuntime& sceneRuntime, - const UIEditorPanelContentHostFrame& contentHostFrame); + EditorSceneRuntime& sceneRuntime, + const UIEditorPanelContentHostFrame& contentHostFrame, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + bool allowInteraction, + bool panelActive); void Append(::XCEngine::UI::UIDrawList& drawList) const; + UIEditorHostCommandEvaluationResult EvaluateEditCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchEditCommand( + std::string_view commandId) override; + private: - struct SectionRow { - std::string label = {}; - std::string value = {}; - }; - - struct Section { - std::string title = {}; - std::vector rows = {}; - }; - const UIEditorPanelContentHostPanelState* FindMountedInspectorPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const; - void BuildPresentation( - const EditorSession& session, - const EditorSceneRuntime& sceneRuntime); + void ResetPanelState(); + void ResetInteractionState(); + void SyncExpansionState(bool subjectChanged); + void SyncSelectionState(); + std::string BuildSubjectKey() const; + ::XCEngine::UI::UIRect BuildGridBounds() const; + const InspectorPresentationComponentBinding* FindSelectedComponentBinding() const; + bool ApplyChangedField(std::string_view fieldId); + EditorSceneRuntime* m_sceneRuntime = nullptr; bool m_visible = false; - bool m_hasSelection = false; ::XCEngine::UI::UIRect m_bounds = {}; - std::string m_title = {}; - std::string m_subtitle = {}; - std::vector
m_sections = {}; + InspectorSubject m_subject = {}; + std::string m_subjectKey = {}; + InspectorPresentationModel m_presentation = {}; + ::XCEngine::UI::Widgets::UISelectionModel m_fieldSelection = {}; + ::XCEngine::UI::Widgets::UIExpansionModel m_sectionExpansion = {}; + ::XCEngine::UI::Widgets::UIPropertyEditModel m_propertyEditModel = {}; + UIEditorPropertyGridInteractionState m_interactionState = {}; + UIEditorPropertyGridInteractionFrame m_gridFrame = {}; + std::unordered_set m_knownSectionIds = {}; }; } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp b/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp new file mode 100644 index 00000000..cb9d8852 --- /dev/null +++ b/new_editor/app/Features/Inspector/InspectorPresentationModel.cpp @@ -0,0 +1,237 @@ +#include "Features/Inspector/InspectorPresentationModel.h" + +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" +#include "Scene/EditorSceneRuntime.h" + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::GameObject; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridSection; + +std::string PathToUtf8String(const std::filesystem::path& path) { + const std::u8string value = path.u8string(); + return std::string(value.begin(), value.end()); +} + +std::string ResolveSceneObjectDisplayName( + const InspectorSceneObjectSubject& sceneObject) { + return sceneObject.displayName.empty() + ? std::string("GameObject") + : sceneObject.displayName; +} + +std::string ResolveProjectSelectionTitle( + const EditorSelectionState& selection) { + if (!selection.displayName.empty()) { + return selection.displayName; + } + + return selection.directory + ? std::string("Folder") + : std::string("Asset"); +} + +UIEditorPropertyGridField BuildReadOnlyTextField( + std::string fieldId, + std::string label, + std::string value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(fieldId); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Text; + field.valueText = std::move(value); + field.readOnly = true; + return field; +} + +void AppendReadOnlySection( + InspectorPresentationModel& model, + std::string sectionId, + std::string title, + std::vector fields) { + UIEditorPropertyGridSection section = {}; + section.sectionId = std::move(sectionId); + section.title = std::move(title); + section.fields = std::move(fields); + model.sections.push_back(std::move(section)); +} + +void AppendFallbackComponentSection( + InspectorPresentationModel& model, + const EditorSceneComponentDescriptor& descriptor) { + std::vector fields = {}; + fields.push_back(BuildReadOnlyTextField( + "component." + descriptor.componentId + ".type", + "Type", + descriptor.typeName)); + fields.push_back(BuildReadOnlyTextField( + "component." + descriptor.componentId + ".status", + "Status", + "Inspector not implemented")); + + AppendReadOnlySection( + model, + "component." + descriptor.componentId, + descriptor.typeName, + std::move(fields)); +} + +void AppendComponentPresentation( + InspectorPresentationModel& model, + const EditorSceneComponentDescriptor& descriptor, + const GameObject* gameObject, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + InspectorPresentationComponentBinding binding = {}; + binding.componentId = descriptor.componentId; + binding.typeName = descriptor.typeName; + binding.removable = descriptor.removable; + + InspectorComponentEditorContext context = {}; + context.gameObject = gameObject; + context.component = descriptor.component; + context.componentId = descriptor.componentId; + context.typeName = descriptor.typeName; + context.removable = descriptor.removable; + + const IInspectorComponentEditor* editor = + componentEditorRegistry.FindEditor(descriptor.typeName); + if (editor != nullptr) { + context.displayName = std::string(editor->GetDisplayName()); + binding.displayName = context.displayName; + + const std::size_t sectionCountBefore = model.sections.size(); + editor->BuildSections(context, model.sections); + for (std::size_t sectionIndex = sectionCountBefore; + sectionIndex < model.sections.size(); + ++sectionIndex) { + for (const UIEditorPropertyGridField& field : + model.sections[sectionIndex].fields) { + binding.fieldIds.push_back(field.fieldId); + } + } + } else { + context.displayName = descriptor.typeName; + binding.displayName = descriptor.typeName; + AppendFallbackComponentSection(model, descriptor); + if (!model.sections.empty()) { + for (const UIEditorPropertyGridField& field : + model.sections.back().fields) { + binding.fieldIds.push_back(field.fieldId); + } + } + } + + model.componentBindings.push_back(std::move(binding)); +} + +} // namespace + +InspectorPresentationModel BuildInspectorPresentationModel( + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry) { + InspectorPresentationModel model = {}; + model.hasSelection = subject.HasSelection(); + + switch (subject.kind) { + case InspectorSubjectKind::ProjectAsset: { + const EditorSelectionState& selection = subject.projectAsset.selection; + const std::string title = ResolveProjectSelectionTitle(selection); + const std::string typeLabel = + selection.directory ? std::string("Folder") : std::string("Asset"); + + model.title = title; + model.subtitle = typeLabel; + + AppendReadOnlySection( + model, + "project.identity", + "Identity", + { + BuildReadOnlyTextField("project.identity.type", "Type", typeLabel), + BuildReadOnlyTextField("project.identity.name", "Name", title), + BuildReadOnlyTextField("project.identity.id", "Id", selection.itemId) + }); + AppendReadOnlySection( + model, + "project.location", + "Location", + { + BuildReadOnlyTextField( + "project.location.path", + "Path", + PathToUtf8String(selection.absolutePath)) + }); + return model; + } + + case InspectorSubjectKind::SceneObject: { + const InspectorSceneObjectSubject& sceneObject = subject.sceneObject; + const std::string title = ResolveSceneObjectDisplayName(sceneObject); + + model.title = title; + model.subtitle = "GameObject"; + + AppendReadOnlySection( + model, + "scene.identity", + "Identity", + { + BuildReadOnlyTextField("scene.identity.type", "Type", "GameObject"), + BuildReadOnlyTextField("scene.identity.name", "Name", title), + BuildReadOnlyTextField("scene.identity.id", "Id", sceneObject.itemId) + }); + + if (sceneObject.gameObject != nullptr) { + const GameObject* parent = sceneObject.gameObject->GetParent(); + const std::string parentName = + parent != nullptr + ? (parent->GetName().empty() + ? std::string("GameObject") + : parent->GetName()) + : std::string("Scene Root"); + AppendReadOnlySection( + model, + "scene.hierarchy", + "Hierarchy", + { + BuildReadOnlyTextField( + "scene.hierarchy.children", + "Children", + std::to_string(sceneObject.gameObject->GetChildCount())), + BuildReadOnlyTextField( + "scene.hierarchy.parent", + "Parent", + parentName) + }); + } + + for (const EditorSceneComponentDescriptor& descriptor : + sceneRuntime.GetSelectedComponents()) { + AppendComponentPresentation( + model, + descriptor, + sceneObject.gameObject, + componentEditorRegistry); + } + return model; + } + + case InspectorSubjectKind::None: + default: + model.title = "Nothing selected"; + model.subtitle = "Select a hierarchy item or project asset."; + return model; + } +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorPresentationModel.h b/new_editor/app/Features/Inspector/InspectorPresentationModel.h new file mode 100644 index 00000000..9befb2a0 --- /dev/null +++ b/new_editor/app/Features/Inspector/InspectorPresentationModel.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Features/Inspector/InspectorSubject.h" + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorSceneRuntime; +class InspectorComponentEditorRegistry; + +struct InspectorPresentationComponentBinding { + std::string componentId = {}; + std::string typeName = {}; + std::string displayName = {}; + bool removable = false; + std::vector fieldIds = {}; +}; + +struct InspectorPresentationModel { + bool hasSelection = false; + std::string title = {}; + std::string subtitle = {}; + std::vector sections = {}; + std::vector componentBindings = {}; +}; + +InspectorPresentationModel BuildInspectorPresentationModel( + const InspectorSubject& subject, + const EditorSceneRuntime& sceneRuntime, + const InspectorComponentEditorRegistry& componentEditorRegistry); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorSubject.cpp b/new_editor/app/Features/Inspector/InspectorSubject.cpp new file mode 100644 index 00000000..03933c84 --- /dev/null +++ b/new_editor/app/Features/Inspector/InspectorSubject.cpp @@ -0,0 +1,74 @@ +#include "Features/Inspector/InspectorSubject.h" + +#include "Scene/EditorSceneRuntime.h" + +namespace XCEngine::UI::Editor::App { + +InspectorSelectionSource ResolveInspectorSelectionSource( + const EditorSession& session, + const EditorSceneRuntime& sceneRuntime) { + const bool hasProjectSelection = + session.selection.kind == EditorSelectionKind::ProjectItem; + const bool hasSceneSelection = sceneRuntime.HasSceneSelection(); + const std::uint64_t projectStamp = session.selection.stamp; + const std::uint64_t sceneStamp = sceneRuntime.GetSelectionStamp(); + + if (projectStamp > sceneStamp) { + return hasProjectSelection + ? InspectorSelectionSource::Project + : InspectorSelectionSource::None; + } + + if (sceneStamp > projectStamp) { + return hasSceneSelection + ? InspectorSelectionSource::Scene + : InspectorSelectionSource::None; + } + + if (hasSceneSelection) { + return InspectorSelectionSource::Scene; + } + + if (hasProjectSelection) { + return InspectorSelectionSource::Project; + } + + return InspectorSelectionSource::None; +} + +InspectorSubject BuildInspectorSubject( + const EditorSession& session, + const EditorSceneRuntime& sceneRuntime) { + InspectorSubject subject = {}; + subject.source = ResolveInspectorSelectionSource(session, sceneRuntime); + + switch (subject.source) { + case InspectorSelectionSource::Project: + subject.kind = InspectorSubjectKind::ProjectAsset; + subject.projectAsset.selection = session.selection; + break; + + case InspectorSelectionSource::Scene: + if (const auto* gameObject = sceneRuntime.GetSelectedGameObject(); + gameObject != nullptr) { + subject.kind = InspectorSubjectKind::SceneObject; + subject.sceneObject.gameObject = gameObject; + subject.sceneObject.itemId = sceneRuntime.GetSelectedItemId(); + subject.sceneObject.displayName = + sceneRuntime.GetSelectedDisplayName(); + } + break; + + case InspectorSelectionSource::None: + default: + break; + } + + if (!subject.HasSelection()) { + subject.source = InspectorSelectionSource::None; + } + + return subject; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Inspector/InspectorSubject.h b/new_editor/app/Features/Inspector/InspectorSubject.h new file mode 100644 index 00000000..703f20a3 --- /dev/null +++ b/new_editor/app/Features/Inspector/InspectorSubject.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include + +namespace XCEngine::Components { + +class GameObject; + +} // namespace XCEngine::Components + +namespace XCEngine::UI::Editor::App { + +class EditorSceneRuntime; + +enum class InspectorSelectionSource : std::uint8_t { + None = 0, + Project, + Scene +}; + +enum class InspectorSubjectKind : std::uint8_t { + None = 0, + ProjectAsset, + SceneObject +}; + +struct InspectorProjectAssetSubject { + EditorSelectionState selection = {}; +}; + +struct InspectorSceneObjectSubject { + const ::XCEngine::Components::GameObject* gameObject = nullptr; + std::string itemId = {}; + std::string displayName = {}; +}; + +struct InspectorSubject { + InspectorSubjectKind kind = InspectorSubjectKind::None; + InspectorSelectionSource source = InspectorSelectionSource::None; + InspectorProjectAssetSubject projectAsset = {}; + InspectorSceneObjectSubject sceneObject = {}; + + constexpr bool HasSelection() const { + return kind != InspectorSubjectKind::None; + } +}; + +InspectorSelectionSource ResolveInspectorSelectionSource( + const EditorSession& session, + const EditorSceneRuntime& sceneRuntime); + +InspectorSubject BuildInspectorSubject( + const EditorSession& session, + const EditorSceneRuntime& sceneRuntime); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Project/ProjectBrowserModel.cpp b/new_editor/app/Features/Project/ProjectBrowserModel.cpp index 5545ce1e..585f270a 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModel.cpp +++ b/new_editor/app/Features/Project/ProjectBrowserModel.cpp @@ -1,12 +1,88 @@ #include "ProjectBrowserModel.h" #include "ProjectBrowserModelInternal.h" +#include "Internal/StringEncoding.h" + +#include + namespace XCEngine::UI::Editor::App { using namespace ProjectBrowserModelInternal; +namespace { + +bool IsSameOrDescendantFolderId( + std::string_view candidateId, + std::string_view ancestorId) { + if (candidateId == ancestorId) { + return true; + } + + if (candidateId.size() <= ancestorId.size() || + candidateId.substr(0u, ancestorId.size()) != ancestorId) { + return false; + } + + return candidateId[ancestorId.size()] == '/'; +} + +std::string RemapMovedFolderId( + std::string_view itemId, + std::string_view sourceFolderId, + std::string_view destinationFolderId) { + if (!IsSameOrDescendantFolderId(itemId, sourceFolderId)) { + return std::string(itemId); + } + + if (itemId == sourceFolderId) { + return std::string(destinationFolderId); + } + + std::string remapped = std::string(destinationFolderId); + remapped += std::string(itemId.substr(sourceFolderId.size())); + return remapped; +} + +bool IsSameOrDescendantItemId( + std::string_view candidateId, + std::string_view ancestorId) { + if (candidateId == ancestorId) { + return true; + } + + if (candidateId.size() <= ancestorId.size() || + candidateId.substr(0u, ancestorId.size()) != ancestorId) { + return false; + } + + return candidateId[ancestorId.size()] == '/'; +} + +std::string RemapMovedItemId( + std::string_view itemId, + std::string_view sourceItemId, + std::string_view destinationItemId) { + if (!IsSameOrDescendantItemId(itemId, sourceItemId)) { + return std::string(itemId); + } + + if (itemId == sourceItemId) { + return std::string(destinationItemId); + } + + std::string remapped = std::string(destinationItemId); + remapped += std::string(itemId.substr(sourceItemId.size())); + return remapped; +} + +} // namespace + void ProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) { m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal(); + std::error_code errorCode = {}; + if (!std::filesystem::exists(m_assetsRootPath, errorCode)) { + std::filesystem::create_directories(m_assetsRootPath / "Scenes", errorCode); + } Refresh(); } @@ -27,6 +103,12 @@ bool ProjectBrowserModel::Empty() const { return m_treeItems.empty(); } +std::filesystem::path ProjectBrowserModel::GetProjectRootPath() const { + return m_assetsRootPath.empty() + ? std::filesystem::path() + : m_assetsRootPath.parent_path(); +} + const std::filesystem::path& ProjectBrowserModel::GetAssetsRootPath() const { return m_assetsRootPath; } @@ -47,6 +129,10 @@ const std::string& ProjectBrowserModel::GetCurrentFolderId() const { return m_currentFolderId; } +bool ProjectBrowserModel::IsAssetsRoot(std::string_view itemId) const { + return itemId == kAssetsRootId; +} + const ProjectBrowserModel::FolderEntry* ProjectBrowserModel::FindFolderEntry( std::string_view itemId) const { for (const FolderEntry& entry : m_folderEntries) { @@ -69,6 +155,492 @@ const ProjectBrowserModel::AssetEntry* ProjectBrowserModel::FindAssetEntry( return nullptr; } +std::optional ProjectBrowserModel::ResolveItemAbsolutePath( + std::string_view itemId) const { + if (itemId.empty() || m_assetsRootPath.empty()) { + return std::nullopt; + } + + const std::filesystem::path rootPath = m_assetsRootPath.parent_path(); + const std::filesystem::path candidatePath = + (rootPath / std::filesystem::path(std::string(itemId))).lexically_normal(); + if (!IsSameOrDescendantPath(candidatePath, m_assetsRootPath)) { + return std::nullopt; + } + + return candidatePath; +} + +std::string ProjectBrowserModel::BuildProjectRelativePath(std::string_view itemId) const { + const std::optional absolutePath = + ResolveItemAbsolutePath(itemId); + if (!absolutePath.has_value()) { + return {}; + } + + return BuildRelativeProjectPath(absolutePath.value(), GetProjectRootPath()); +} + +bool ProjectBrowserModel::CreateFolder( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdFolderId) { + if (createdFolderId != nullptr) { + createdFolderId->clear(); + } + + const FolderEntry* parentFolder = FindFolderEntry(parentFolderId); + if (parentFolder == nullptr) { + return false; + } + + try { + const std::string trimmedName = TrimAssetName(requestedName); + if (HasInvalidAssetName(trimmedName)) { + return false; + } + + const std::filesystem::path newFolderPath = + MakeUniqueFolderPath(parentFolder->absolutePath, trimmedName); + if (!std::filesystem::create_directory(newFolderPath)) { + return false; + } + + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + if (createdFolderId != nullptr) { + *createdFolderId = BuildRelativeItemId(newFolderPath, m_assetsRootPath); + } + return true; + } catch (...) { + return false; + } +} + +bool ProjectBrowserModel::CreateMaterial( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdItemId) { + if (createdItemId != nullptr) { + createdItemId->clear(); + } + + const FolderEntry* parentFolder = FindFolderEntry(parentFolderId); + if (parentFolder == nullptr) { + return false; + } + + try { + const std::string trimmedName = TrimAssetName(requestedName); + if (HasInvalidAssetName(trimmedName)) { + return false; + } + + std::filesystem::path requestedPath = + App::Internal::Utf8ToWide(trimmedName); + if (!requestedPath.has_extension()) { + requestedPath += L".mat"; + } + + const std::filesystem::path materialPath = + MakeUniqueFilePath(parentFolder->absolutePath, requestedPath); + std::ofstream output(materialPath, std::ios::out | std::ios::trunc); + if (!output.is_open()) { + return false; + } + + output << + "{\n" + " \"renderQueue\": \"geometry\",\n" + " \"renderState\": {\n" + " \"cull\": \"none\",\n" + " \"depthTest\": true,\n" + " \"depthWrite\": true,\n" + " \"depthFunc\": \"less\",\n" + " \"blendEnable\": false,\n" + " \"srcBlend\": \"one\",\n" + " \"dstBlend\": \"zero\",\n" + " \"srcBlendAlpha\": \"one\",\n" + " \"dstBlendAlpha\": \"zero\",\n" + " \"blendOp\": \"add\",\n" + " \"blendOpAlpha\": \"add\",\n" + " \"colorWriteMask\": 15\n" + " }\n" + "}\n"; + output.close(); + if (!output.good()) { + return false; + } + + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + if (createdItemId != nullptr) { + *createdItemId = BuildRelativeItemId(materialPath, m_assetsRootPath); + } + return true; + } catch (...) { + return false; + } +} + +bool ProjectBrowserModel::RenameItem( + std::string_view itemId, + std::string_view newName, + std::string* renamedItemId) { + if (renamedItemId != nullptr) { + renamedItemId->clear(); + } + + if (itemId.empty() || IsAssetsRoot(itemId)) { + return false; + } + + const std::optional sourcePath = + ResolveItemAbsolutePath(itemId); + if (!sourcePath.has_value()) { + return false; + } + + try { + const std::string trimmedName = TrimAssetName(newName); + if (HasInvalidAssetName(trimmedName) || + !std::filesystem::exists(sourcePath.value())) { + return false; + } + + const std::string targetName = + BuildRenamedEntryName(sourcePath.value(), trimmedName); + if (HasInvalidAssetName(targetName)) { + return false; + } + + const std::filesystem::path destinationPath = + (sourcePath->parent_path() / App::Internal::Utf8ToWide(targetName)) + .lexically_normal(); + const std::string destinationItemId = + BuildRelativeItemId(destinationPath, m_assetsRootPath); + + if (!RenamePathCaseAware(sourcePath.value(), destinationPath)) { + return false; + } + MoveMetaSidecarIfPresent(sourcePath.value(), destinationPath); + + if (std::filesystem::is_directory(destinationPath)) { + m_currentFolderId = RemapMovedItemId( + m_currentFolderId, + itemId, + destinationItemId); + } + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + if (renamedItemId != nullptr) { + *renamedItemId = destinationItemId; + } + return true; + } catch (...) { + return false; + } +} + +bool ProjectBrowserModel::DeleteItem(std::string_view itemId) { + if (itemId.empty() || IsAssetsRoot(itemId)) { + return false; + } + + const std::optional sourcePath = + ResolveItemAbsolutePath(itemId); + if (!sourcePath.has_value()) { + return false; + } + + try { + if (!std::filesystem::exists(sourcePath.value())) { + return false; + } + + if (std::filesystem::is_directory(sourcePath.value()) && + IsSameOrDescendantFolderId(m_currentFolderId, itemId)) { + m_currentFolderId = + BuildRelativeItemId(sourcePath->parent_path(), m_assetsRootPath); + } + + std::filesystem::remove_all(sourcePath.value()); + RemoveMetaSidecarIfPresent(sourcePath.value()); + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + return true; + } catch (...) { + return false; + } +} + +bool ProjectBrowserModel::CanMoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId) const { + if (itemId.empty() || targetFolderId.empty() || IsAssetsRoot(itemId)) { + return false; + } + + const std::optional sourcePath = + ResolveItemAbsolutePath(itemId); + const FolderEntry* targetFolder = FindFolderEntry(targetFolderId); + if (!sourcePath.has_value() || targetFolder == nullptr) { + return false; + } + + std::error_code errorCode = {}; + if (!std::filesystem::exists(sourcePath.value(), errorCode) || errorCode) { + return false; + } + + const std::filesystem::path destinationPath = + (targetFolder->absolutePath / sourcePath->filename()).lexically_normal(); + if (destinationPath == sourcePath->lexically_normal()) { + return false; + } + + if (std::filesystem::exists(destinationPath, errorCode) || errorCode) { + return false; + } + + if (std::filesystem::is_directory(sourcePath.value(), errorCode)) { + if (errorCode) { + return false; + } + if (IsSameOrDescendantPath(targetFolder->absolutePath, sourcePath.value())) { + return false; + } + } + + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath.value()); + const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); + const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); + if (errorCode) { + return false; + } + if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { + return false; + } + + return !errorCode; +} + +bool ProjectBrowserModel::MoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId, + std::string* movedItemId) { + if (movedItemId != nullptr) { + movedItemId->clear(); + } + + if (!CanMoveItemToFolder(itemId, targetFolderId)) { + return false; + } + + const std::optional sourcePath = + ResolveItemAbsolutePath(itemId); + const FolderEntry* targetFolder = FindFolderEntry(targetFolderId); + if (!sourcePath.has_value() || targetFolder == nullptr) { + return false; + } + + try { + const std::filesystem::path destinationPath = + (targetFolder->absolutePath / sourcePath->filename()).lexically_normal(); + const std::string destinationItemId = + BuildRelativeItemId(destinationPath, m_assetsRootPath); + if (!MovePathWithOptionalMeta(sourcePath.value(), destinationPath)) { + return false; + } + + if (std::filesystem::is_directory(destinationPath)) { + m_currentFolderId = RemapMovedItemId( + m_currentFolderId, + itemId, + destinationItemId); + } + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + if (movedItemId != nullptr) { + *movedItemId = destinationItemId; + } + return true; + } catch (...) { + return false; + } +} + +std::optional ProjectBrowserModel::GetParentFolderId(std::string_view itemId) const { + const FolderEntry* folderEntry = FindFolderEntry(itemId); + if (folderEntry == nullptr || itemId == kAssetsRootId) { + return std::nullopt; + } + + const std::filesystem::path parentPath = folderEntry->absolutePath.parent_path(); + if (parentPath.empty() || parentPath == folderEntry->absolutePath) { + return std::nullopt; + } + + return BuildRelativeItemId(parentPath, m_assetsRootPath); +} + +bool ProjectBrowserModel::CanReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId) const { + if (sourceFolderId.empty() || + targetParentId.empty() || + sourceFolderId == kAssetsRootId) { + return false; + } + + const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); + const FolderEntry* targetFolder = FindFolderEntry(targetParentId); + if (sourceFolder == nullptr || targetFolder == nullptr) { + return false; + } + + if (IsSameOrDescendantFolderId(targetParentId, sourceFolderId)) { + return false; + } + + const std::optional currentParentId = GetParentFolderId(sourceFolderId); + if (currentParentId.has_value() && currentParentId.value() == targetParentId) { + return false; + } + + const std::filesystem::path destinationPath = + targetFolder->absolutePath / sourceFolder->absolutePath.filename(); + if (destinationPath.lexically_normal() == sourceFolder->absolutePath.lexically_normal()) { + return false; + } + + std::error_code errorCode = {}; + if (std::filesystem::exists(destinationPath, errorCode)) { + return false; + } + if (errorCode) { + return false; + } + + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath); + const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); + const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); + if (errorCode) { + return false; + } + if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { + return false; + } + + return !errorCode; +} + +bool ProjectBrowserModel::ReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId, + std::string* movedFolderId) { + if (!CanReparentFolder(sourceFolderId, targetParentId)) { + return false; + } + + const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); + const FolderEntry* targetFolder = FindFolderEntry(targetParentId); + if (sourceFolder == nullptr || targetFolder == nullptr) { + return false; + } + + const std::filesystem::path destinationPath = + (targetFolder->absolutePath / sourceFolder->absolutePath.filename()).lexically_normal(); + const std::string destinationFolderId = + BuildRelativeItemId(destinationPath, m_assetsRootPath); + + if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) { + return false; + } + + m_currentFolderId = RemapMovedFolderId( + m_currentFolderId, + sourceFolderId, + destinationFolderId); + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + + if (movedFolderId != nullptr) { + *movedFolderId = destinationFolderId; + } + return true; +} + +bool ProjectBrowserModel::MoveFolderToRoot( + std::string_view sourceFolderId, + std::string* movedFolderId) { + if (sourceFolderId.empty() || sourceFolderId == kAssetsRootId) { + return false; + } + + const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId); + if (sourceFolder == nullptr) { + return false; + } + + const std::optional currentParentId = GetParentFolderId(sourceFolderId); + if (!currentParentId.has_value() || currentParentId.value() == kAssetsRootId) { + return false; + } + + const std::filesystem::path destinationPath = + (m_assetsRootPath / sourceFolder->absolutePath.filename()).lexically_normal(); + if (destinationPath == sourceFolder->absolutePath.lexically_normal()) { + return false; + } + + std::error_code errorCode = {}; + if (std::filesystem::exists(destinationPath, errorCode)) { + return false; + } + if (errorCode) { + return false; + } + + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath); + const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); + const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode); + if (errorCode) { + return false; + } + if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) { + return false; + } + if (errorCode) { + return false; + } + + const std::string destinationFolderId = + BuildRelativeItemId(destinationPath, m_assetsRootPath); + if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) { + return false; + } + + m_currentFolderId = RemapMovedFolderId( + m_currentFolderId, + sourceFolderId, + destinationFolderId); + RefreshFolderTree(); + EnsureValidCurrentFolder(); + RefreshAssetList(); + + if (movedFolderId != nullptr) { + *movedFolderId = destinationFolderId; + } + return true; +} + bool ProjectBrowserModel::NavigateToFolder(std::string_view itemId) { if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) { return false; diff --git a/new_editor/app/Features/Project/ProjectBrowserModel.h b/new_editor/app/Features/Project/ProjectBrowserModel.h index 3833f598..eec22796 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModel.h +++ b/new_editor/app/Features/Project/ProjectBrowserModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,16 @@ namespace XCEngine::UI::Editor::App { class ProjectBrowserModel { public: + enum class ItemKind : std::uint8_t { + Folder = 0, + Scene, + Model, + Material, + Texture, + Script, + File + }; + struct BreadcrumbSegment { std::string label = {}; std::string targetFolderId = {}; @@ -30,7 +41,12 @@ public: std::string itemId = {}; std::filesystem::path absolutePath = {}; std::string displayName = {}; + std::string nameWithExtension = {}; + std::string extensionLower = {}; + ItemKind kind = ItemKind::File; bool directory = false; + bool canOpen = false; + bool canPreview = false; }; void Initialize(const std::filesystem::path& repoRoot); @@ -38,15 +54,49 @@ public: void Refresh(); bool Empty() const; + std::filesystem::path GetProjectRootPath() const; const std::filesystem::path& GetAssetsRootPath() const; const std::vector& GetFolderEntries() const; const std::vector& GetTreeItems() const; const std::vector& GetAssetEntries() const; const std::string& GetCurrentFolderId() const; + bool IsAssetsRoot(std::string_view itemId) const; const FolderEntry* FindFolderEntry(std::string_view itemId) const; const AssetEntry* FindAssetEntry(std::string_view itemId) const; + std::optional ResolveItemAbsolutePath( + std::string_view itemId) const; + std::string BuildProjectRelativePath(std::string_view itemId) const; bool NavigateToFolder(std::string_view itemId); + bool CreateFolder( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdFolderId = nullptr); + bool CreateMaterial( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdItemId = nullptr); + bool RenameItem( + std::string_view itemId, + std::string_view newName, + std::string* renamedItemId = nullptr); + bool DeleteItem(std::string_view itemId); + bool CanMoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId) const; + bool MoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId, + std::string* movedItemId = nullptr); + std::optional GetParentFolderId(std::string_view itemId) const; + bool CanReparentFolder(std::string_view sourceFolderId, std::string_view targetParentId) const; + bool ReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId, + std::string* movedFolderId = nullptr); + bool MoveFolderToRoot( + std::string_view sourceFolderId, + std::string* movedFolderId = nullptr); std::vector BuildBreadcrumbSegments() const; std::vector CollectCurrentFolderAncestorIds() const; std::vector BuildAncestorFolderIds(std::string_view itemId) const; diff --git a/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp b/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp index cde48021..0797b422 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp +++ b/new_editor/app/Features/Project/ProjectBrowserModelAssets.cpp @@ -48,8 +48,14 @@ void ProjectBrowserModel::RefreshAssetList() { AssetEntry assetEntry = {}; assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath); assetEntry.absolutePath = entry.path(); + assetEntry.nameWithExtension = + BuildAssetNameWithExtension(entry.path(), entry.is_directory()); assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory()); + assetEntry.extensionLower = ToLowerCopy(PathToUtf8String(entry.path().extension())); + assetEntry.kind = ResolveItemKind(entry.path(), entry.is_directory()); assetEntry.directory = entry.is_directory(); + assetEntry.canOpen = CanOpenItemKind(assetEntry.kind); + assetEntry.canPreview = CanPreviewItemKind(assetEntry.kind); m_assetEntries.push_back(std::move(assetEntry)); } } diff --git a/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp b/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp index 4fda0fd5..655a4b0b 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp +++ b/new_editor/app/Features/Project/ProjectBrowserModelInternal.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal { @@ -38,6 +40,20 @@ std::string BuildRelativeItemId( return normalized.empty() ? std::string(kAssetsRootId) : normalized; } +std::string BuildRelativeProjectPath( + const std::filesystem::path& path, + const std::filesystem::path& projectRoot) { + if (projectRoot.empty() || path.empty()) { + return {}; + } + + const std::filesystem::path relative = + std::filesystem::relative(path, projectRoot); + const std::string normalized = + NormalizePathSeparators(PathToUtf8String(relative.lexically_normal())); + return normalized; +} + std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) { if (directory) { return PathToUtf8String(path.filename()); @@ -52,6 +68,12 @@ std::string BuildAssetDisplayName(const std::filesystem::path& path, bool direct return filename.substr(0u, extensionOffset); } +std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory) { + return directory + ? PathToUtf8String(path.filename()) + : PathToUtf8String(path.filename()); +} + bool IsMetaFile(const std::filesystem::path& path) { return ToLowerCopy(path.extension().string()) == ".meta"; } @@ -93,4 +115,256 @@ std::vector CollectSortedChildDirectories( return paths; } +std::wstring MakePathKey(const std::filesystem::path& path) { + std::wstring key = path.lexically_normal().generic_wstring(); + std::transform(key.begin(), key.end(), key.begin(), ::towlower); + return key; +} + +bool IsSameOrDescendantPath( + const std::filesystem::path& path, + const std::filesystem::path& ancestor) { + const std::wstring pathKey = MakePathKey(path); + std::wstring ancestorKey = MakePathKey(ancestor); + if (pathKey.empty() || ancestorKey.empty()) { + return false; + } + + if (pathKey == ancestorKey) { + return true; + } + + if (ancestorKey.back() != L'/') { + ancestorKey += L'/'; + } + return pathKey.rfind(ancestorKey, 0) == 0; +} + +std::string TrimAssetName(std::string_view name) { + const auto first = + std::find_if_not(name.begin(), name.end(), [](unsigned char character) { + return std::isspace(character) != 0; + }); + if (first == name.end()) { + return {}; + } + + const auto last = + std::find_if_not(name.rbegin(), name.rend(), [](unsigned char character) { + return std::isspace(character) != 0; + }).base(); + return std::string(first, last); +} + +bool HasInvalidAssetName(std::string_view name) { + if (name.empty() || name == "." || name == "..") { + return true; + } + + return name.find_first_of("\\/:*?\"<>|") != std::string_view::npos; +} + +std::filesystem::path MakeUniqueFolderPath( + const std::filesystem::path& parentPath, + std::string_view preferredName) { + std::filesystem::path candidatePath = + parentPath / App::Internal::Utf8ToWide(std::string(preferredName)); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + + for (std::size_t suffix = 1u;; ++suffix) { + candidatePath = + parentPath / App::Internal::Utf8ToWide( + std::string(preferredName) + " " + std::to_string(suffix)); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + } +} + +std::filesystem::path MakeUniqueFilePath( + const std::filesystem::path& parentPath, + const std::filesystem::path& preferredFileName) { + std::filesystem::path candidatePath = parentPath / preferredFileName; + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + + const std::wstring stem = preferredFileName.stem().wstring(); + const std::wstring extension = preferredFileName.extension().wstring(); + for (std::size_t suffix = 1u;; ++suffix) { + candidatePath = + parentPath / + std::filesystem::path( + stem + L" " + std::to_wstring(suffix) + extension); + if (!std::filesystem::exists(candidatePath)) { + return candidatePath; + } + } +} + +std::string BuildRenamedEntryName( + const std::filesystem::path& sourcePath, + std::string_view requestedName) { + if (std::filesystem::is_directory(sourcePath)) { + return std::string(requestedName); + } + + const std::filesystem::path requestedPath = + App::Internal::Utf8ToWide(std::string(requestedName)); + if (requestedPath.has_extension()) { + return PathToUtf8String(requestedPath.filename()); + } + + return std::string(requestedName) + PathToUtf8String(sourcePath.extension()); +} + +std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath) { + return std::filesystem::path(assetPath.native() + std::filesystem::path(L".meta").native()); +} + +void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath) { + std::error_code errorCode = {}; + std::filesystem::remove_all(GetMetaSidecarPath(assetPath), errorCode); +} + +namespace { + +std::filesystem::path MakeCaseOnlyRenameTempPath(const std::filesystem::path& sourcePath) { + const std::filesystem::path parentPath = sourcePath.parent_path(); + const std::wstring sourceStem = sourcePath.filename().wstring(); + + for (std::size_t suffix = 0u;; ++suffix) { + std::wstring tempName = sourceStem + L".xc_tmp_rename"; + if (suffix > 0u) { + tempName += std::to_wstring(suffix); + } + + const std::filesystem::path tempPath = parentPath / tempName; + if (!std::filesystem::exists(tempPath)) { + return tempPath; + } + } +} + +bool MatchesExtension( + std::wstring_view extension, + std::initializer_list candidates) { + for (const std::wstring_view candidate : candidates) { + if (extension == candidate) { + return true; + } + } + + return false; +} + +} // namespace + +bool RenamePathCaseAware( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath) { + if (MakePathKey(sourcePath) != MakePathKey(destPath)) { + if (std::filesystem::exists(destPath)) { + return false; + } + + std::filesystem::rename(sourcePath, destPath); + return true; + } + + if (sourcePath.filename() == destPath.filename()) { + return true; + } + + const std::filesystem::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath); + std::filesystem::rename(sourcePath, tempPath); + std::filesystem::rename(tempPath, destPath); + return true; +} + +void MoveMetaSidecarIfPresent( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath) { + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); + if (!std::filesystem::exists(sourceMetaPath)) { + return; + } + + const std::filesystem::path destMetaPath = GetMetaSidecarPath(destPath); + RenamePathCaseAware(sourceMetaPath, destMetaPath); +} + +bool MovePathWithOptionalMeta( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destinationPath) { + std::error_code errorCode = {}; + const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath); + const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath); + const bool moveMeta = std::filesystem::exists(sourceMetaPath, errorCode); + if (errorCode) { + return false; + } + + std::filesystem::rename(sourcePath, destinationPath, errorCode); + if (errorCode) { + return false; + } + + if (!moveMeta) { + return true; + } + + std::filesystem::rename(sourceMetaPath, destinationMetaPath, errorCode); + if (!errorCode) { + return true; + } + + std::error_code rollbackError = {}; + std::filesystem::rename(destinationPath, sourcePath, rollbackError); + return false; +} + +ProjectBrowserModel::ItemKind ResolveItemKind( + const std::filesystem::path& path, + bool directory) { + if (directory) { + return ProjectBrowserModel::ItemKind::Folder; + } + + std::wstring extension = path.extension().wstring(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); + + if (MatchesExtension(extension, { L".xc", L".unity", L".scene" })) { + return ProjectBrowserModel::ItemKind::Scene; + } + if (MatchesExtension(extension, { L".fbx", L".obj", L".gltf", L".glb" })) { + return ProjectBrowserModel::ItemKind::Model; + } + if (extension == L".mat") { + return ProjectBrowserModel::ItemKind::Material; + } + if (MatchesExtension(extension, { + L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif", + L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm", + L".pnm", L".dds", L".ktx", L".ktx2", L".webp" })) { + return ProjectBrowserModel::ItemKind::Texture; + } + if (MatchesExtension(extension, { L".cs", L".cpp", L".c", L".h", L".hpp" })) { + return ProjectBrowserModel::ItemKind::Script; + } + + return ProjectBrowserModel::ItemKind::File; +} + +bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind) { + return kind == ProjectBrowserModel::ItemKind::Folder || + kind == ProjectBrowserModel::ItemKind::Scene; +} + +bool CanPreviewItemKind(ProjectBrowserModel::ItemKind kind) { + return kind == ProjectBrowserModel::ItemKind::Texture; +} + } // namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal diff --git a/new_editor/app/Features/Project/ProjectBrowserModelInternal.h b/new_editor/app/Features/Project/ProjectBrowserModelInternal.h index b78d12b8..31e462db 100644 --- a/new_editor/app/Features/Project/ProjectBrowserModelInternal.h +++ b/new_editor/app/Features/Project/ProjectBrowserModelInternal.h @@ -1,5 +1,7 @@ #pragma once +#include "ProjectBrowserModel.h" + #include #include #include @@ -15,10 +17,45 @@ std::string NormalizePathSeparators(std::string value); std::string BuildRelativeItemId( const std::filesystem::path& path, const std::filesystem::path& assetsRoot); +std::string BuildRelativeProjectPath( + const std::filesystem::path& path, + const std::filesystem::path& projectRoot); std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory); +std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory); bool IsMetaFile(const std::filesystem::path& path); bool HasChildDirectories(const std::filesystem::path& folderPath); std::vector CollectSortedChildDirectories( const std::filesystem::path& folderPath); +std::wstring MakePathKey(const std::filesystem::path& path); +bool IsSameOrDescendantPath( + const std::filesystem::path& path, + const std::filesystem::path& ancestor); +std::string TrimAssetName(std::string_view name); +bool HasInvalidAssetName(std::string_view name); +std::filesystem::path MakeUniqueFolderPath( + const std::filesystem::path& parentPath, + std::string_view preferredName); +std::filesystem::path MakeUniqueFilePath( + const std::filesystem::path& parentPath, + const std::filesystem::path& preferredFileName); +std::string BuildRenamedEntryName( + const std::filesystem::path& sourcePath, + std::string_view requestedName); +std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath); +void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath); +bool RenamePathCaseAware( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath); +void MoveMetaSidecarIfPresent( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destPath); +bool MovePathWithOptionalMeta( + const std::filesystem::path& sourcePath, + const std::filesystem::path& destinationPath); +ProjectBrowserModel::ItemKind ResolveItemKind( + const std::filesystem::path& path, + bool directory); +bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind); +bool CanPreviewItemKind(ProjectBrowserModel::ItemKind kind); } // namespace XCEngine::UI::Editor::App::ProjectBrowserModelInternal diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index 37ac67d2..5cc23045 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -1,12 +1,27 @@ #include "ProjectPanelInternal.h" -#include +#include "Project/EditorProjectRuntime.h" +#include +#include + +#include "Internal/StringEncoding.h" + +#include + +#include +#include + +#include +#include +#include #include namespace XCEngine::UI::Editor::App { using namespace ProjectPanelInternal; +namespace GridDrag = GridItemDragDrop; +namespace TreeDrag = TreeItemDragDrop; namespace { @@ -28,17 +43,179 @@ UIEditorHostCommandDispatchResult BuildDispatchResult( return result; } +bool HasValidBounds(const UIRect& bounds) { + return bounds.width > 0.0f && bounds.height > 0.0f; +} + +bool CopyTextToClipboard(std::string_view text) { + if (text.empty()) { + return false; + } + + const std::wstring wideText = + App::Internal::Utf8ToWide(std::string(text)); + const std::size_t byteCount = + (wideText.size() + 1u) * sizeof(wchar_t); + if (!OpenClipboard(nullptr)) { + return false; + } + + struct ClipboardCloser final { + ~ClipboardCloser() { + CloseClipboard(); + } + } clipboardCloser = {}; + + if (!EmptyClipboard()) { + return false; + } + + HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, byteCount); + if (handle == nullptr) { + return false; + } + + void* locked = GlobalLock(handle); + if (locked == nullptr) { + GlobalFree(handle); + return false; + } + + std::memcpy(locked, wideText.c_str(), byteCount); + GlobalUnlock(handle); + + if (SetClipboardData(CF_UNICODETEXT, handle) == nullptr) { + GlobalFree(handle); + return false; + } + + return true; +} + +bool ShowPathInExplorer( + const std::filesystem::path& path, + bool selectTarget) { + if (path.empty()) { + return false; + } + + namespace fs = std::filesystem; + + std::error_code errorCode = {}; + const fs::path targetPath = path.lexically_normal(); + if (!fs::exists(targetPath, errorCode) || errorCode) { + return false; + } + + HINSTANCE result = nullptr; + if (selectTarget) { + const std::wstring parameters = + L"/select,\"" + targetPath.native() + L"\""; + const std::wstring workingDirectory = + targetPath.parent_path().native(); + result = ShellExecuteW( + nullptr, + L"open", + L"explorer.exe", + parameters.c_str(), + workingDirectory.empty() ? nullptr : workingDirectory.c_str(), + SW_SHOWNORMAL); + } else { + const std::wstring workingDirectory = + targetPath.parent_path().native(); + result = ShellExecuteW( + nullptr, + L"open", + targetPath.c_str(), + nullptr, + workingDirectory.empty() ? nullptr : workingDirectory.c_str(), + SW_SHOWNORMAL); + } + + return reinterpret_cast(result) > 32; +} + +Widgets::UIEditorMenuPopupItem BuildContextMenuCommandItem( + std::string itemId, + std::string label, + bool enabled = true) { + Widgets::UIEditorMenuPopupItem item = {}; + item.itemId = std::move(itemId); + item.kind = UIEditorMenuItemKind::Command; + item.label = std::move(label); + item.enabled = enabled; + return item; +} + +Widgets::UIEditorMenuPopupItem BuildContextMenuSeparatorItem(std::string itemId) { + Widgets::UIEditorMenuPopupItem item = {}; + item.itemId = std::move(itemId); + item.kind = UIEditorMenuItemKind::Separator; + item.enabled = false; + return item; +} + +void AppendContextMenuSeparator( + std::vector& items, + std::string itemId) { + if (items.empty() || items.back().kind == UIEditorMenuItemKind::Separator) { + return; + } + + items.push_back(BuildContextMenuSeparatorItem(std::move(itemId))); +} + } // namespace +EditorProjectRuntime* ProjectPanel::ResolveProjectRuntime() { + return m_projectRuntime != nullptr + ? m_projectRuntime + : m_ownedProjectRuntime.get(); +} + +const EditorProjectRuntime* ProjectPanel::ResolveProjectRuntime() const { + return m_projectRuntime != nullptr + ? m_projectRuntime + : m_ownedProjectRuntime.get(); +} + +bool ProjectPanel::HasProjectRuntime() const { + return ResolveProjectRuntime() != nullptr; +} + +ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() { + return ResolveProjectRuntime()->GetBrowserModel(); +} + +const ProjectPanel::BrowserModel& ProjectPanel::GetBrowserModel() const { + return ResolveProjectRuntime()->GetBrowserModel(); +} + void ProjectPanel::Initialize(const std::filesystem::path& repoRoot) { - m_browserModel.Initialize(repoRoot); + m_ownedProjectRuntime = std::make_unique(); + m_ownedProjectRuntime->Initialize(repoRoot); + if (m_icons != nullptr) { + m_ownedProjectRuntime->SetFolderIcon(ResolveFolderIcon(m_icons)); + } SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); +} + +void ProjectPanel::SetProjectRuntime(EditorProjectRuntime* projectRuntime) { + m_projectRuntime = projectRuntime; + if (m_projectRuntime != nullptr && m_icons != nullptr) { + m_projectRuntime->SetFolderIcon(ResolveFolderIcon(m_icons)); + } + SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); } void ProjectPanel::SetBuiltInIcons(const BuiltInIcons* icons) { m_icons = icons; - m_browserModel.SetFolderIcon(ResolveFolderIcon(m_icons)); - SyncCurrentFolderSelection(); + if (EditorProjectRuntime* runtime = ResolveProjectRuntime(); + runtime != nullptr) { + runtime->SetFolderIcon(ResolveFolderIcon(m_icons)); + } } void ProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) { @@ -46,14 +223,19 @@ void ProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) { } void ProjectPanel::ResetInteractionState() { + m_assetDragState = {}; + m_treeDragState = {}; m_treeInteractionState = {}; m_treeFrame = {}; + m_contextMenu = {}; + ClearRenameState(); m_frameEvents.clear(); m_layout = {}; m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); m_hoveredBreadcrumbIndex = kInvalidLayoutIndex; m_pressedBreadcrumbIndex = kInvalidLayoutIndex; + m_assetDropTargetSurface = DropTargetSurface::None; m_visible = false; m_splitterHovered = false; m_splitterDragging = false; @@ -66,15 +248,21 @@ ProjectPanel::CursorKind ProjectPanel::GetCursorKind() const { } bool ProjectPanel::WantsHostPointerCapture() const { - return m_requestPointerCapture; + return m_requestPointerCapture || + m_assetDragState.requestPointerCapture || + m_treeDragState.requestPointerCapture; } bool ProjectPanel::WantsHostPointerRelease() const { - return m_requestPointerRelease; + return m_requestPointerRelease || + m_assetDragState.requestPointerRelease || + m_treeDragState.requestPointerRelease; } bool ProjectPanel::HasActivePointerCapture() const { - return m_splitterDragging; + return m_splitterDragging || + GridDrag::HasActivePointerCapture(m_assetDragState) || + TreeDrag::HasActivePointerCapture(m_treeDragState); } const std::vector& ProjectPanel::GetFrameEvents() const { @@ -82,42 +270,275 @@ const std::vector& ProjectPanel::GetFrameEvents() const { } const ProjectPanel::FolderEntry* ProjectPanel::FindFolderEntry(std::string_view itemId) const { - return m_browserModel.FindFolderEntry(itemId); + const EditorProjectRuntime* runtime = ResolveProjectRuntime(); + return runtime != nullptr + ? runtime->FindFolderEntry(itemId) + : nullptr; } const ProjectPanel::AssetEntry* ProjectPanel::FindAssetEntry(std::string_view itemId) const { - return m_browserModel.FindAssetEntry(itemId); + const EditorProjectRuntime* runtime = ResolveProjectRuntime(); + return runtime != nullptr + ? runtime->FindAssetEntry(itemId) + : nullptr; } -std::optional ProjectPanel::ResolveEditCommandTarget() const { - if (m_assetSelection.HasSelection()) { - const AssetEntry* asset = FindAssetEntry(m_assetSelection.GetSelectedId()); - if (asset != nullptr) { - EditCommandTarget target = {}; - target.itemId = asset->itemId; - target.absolutePath = asset->absolutePath; - target.displayName = asset->displayName; - target.directory = asset->directory; - return target; +ProjectPanel::AssetCommandTarget ProjectPanel::ResolveAssetCommandTarget( + std::string_view explicitItemId, + bool forceCurrentFolder) const { + if (!HasProjectRuntime()) { + return {}; + } + + return ResolveProjectRuntime()->ResolveAssetCommandTarget( + explicitItemId, + forceCurrentFolder); +} + +const ProjectPanel::AssetEntry* ProjectPanel::GetSelectedAssetEntry() const { + const EditorProjectRuntime* runtime = ResolveProjectRuntime(); + return runtime != nullptr && runtime->HasSelection() + ? runtime->FindAssetEntry(runtime->GetSelection().itemId) + : nullptr; +} + +const ProjectPanel::FolderEntry* ProjectPanel::GetSelectedFolderEntry() const { + if (!HasProjectRuntime()) { + return nullptr; + } + + return FindFolderEntry(GetBrowserModel().GetCurrentFolderId()); +} + +void ProjectPanel::ClearRenameState() { + m_renameState = {}; + m_renameFrame = {}; + m_pendingRenameItemId.clear(); + m_pendingRenameSurface = RenameSurface::None; + m_activeRenameSurface = RenameSurface::None; +} + +void ProjectPanel::QueueRenameSession( + std::string_view itemId, + RenameSurface surface) { + if (itemId.empty() || surface == RenameSurface::None) { + return; + } + + if (surface == RenameSurface::Tree && + FindFolderEntry(itemId) == nullptr) { + return; + } + + if (surface == RenameSurface::Grid && + FindAssetEntry(itemId) == nullptr) { + return; + } + + if (m_renameState.active && + m_renameState.itemId == itemId && + m_activeRenameSurface == surface) { + return; + } + + m_pendingRenameItemId = std::string(itemId); + m_pendingRenameSurface = surface; +} + +UIRect ProjectPanel::BuildRenameBounds( + std::string_view itemId, + RenameSurface surface) const { + if (itemId.empty() || surface == RenameSurface::None) { + return {}; + } + + const Widgets::UIEditorTextFieldMetrics hostedMetrics = + BuildUIEditorPropertyGridTextFieldMetrics( + ResolveUIEditorPropertyGridMetrics(), + ResolveUIEditorTextFieldMetrics()); + + if (surface == RenameSurface::Tree) { + const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId( + m_treeFrame.layout, + GetBrowserModel().GetTreeItems(), + itemId); + if (visibleIndex == Widgets::UIEditorTreeViewInvalidIndex || + visibleIndex >= m_treeFrame.layout.rowRects.size() || + visibleIndex >= m_treeFrame.layout.labelRects.size()) { + return {}; + } + + const UIRect& rowRect = m_treeFrame.layout.rowRects[visibleIndex]; + const UIRect& labelRect = m_treeFrame.layout.labelRects[visibleIndex]; + const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX); + const float right = rowRect.x + rowRect.width - 8.0f; + const float width = (std::max)(120.0f, right - x); + return UIRect(x, rowRect.y, width, rowRect.height); + } + + if (surface == RenameSurface::Grid) { + for (const AssetTileLayout& tile : m_layout.assetTiles) { + if (tile.itemIndex >= GetBrowserModel().GetAssetEntries().size()) { + continue; + } + + const AssetEntry& assetEntry = + GetBrowserModel().GetAssetEntries()[tile.itemIndex]; + if (assetEntry.itemId != itemId) { + continue; + } + + const float x = (std::max)( + tile.tileRect.x + 4.0f, + tile.labelRect.x - hostedMetrics.valueTextInsetX); + const float right = tile.tileRect.x + tile.tileRect.width - 4.0f; + const float width = (std::max)(72.0f, right - x); + return UIRect(x, tile.labelRect.y - 2.0f, width, tile.labelRect.height + 4.0f); } } - if (!m_folderSelection.HasSelection()) { + return {}; +} + +bool ProjectPanel::TryStartQueuedRenameSession() { + if (m_pendingRenameItemId.empty() || + m_pendingRenameSurface == RenameSurface::None) { + return false; + } + + std::string initialText = {}; + if (m_pendingRenameSurface == RenameSurface::Grid) { + const AssetEntry* asset = FindAssetEntry(m_pendingRenameItemId); + if (asset == nullptr) { + m_pendingRenameItemId.clear(); + m_pendingRenameSurface = RenameSurface::None; + return false; + } + initialText = asset->displayName; + } else { + const FolderEntry* folder = FindFolderEntry(m_pendingRenameItemId); + if (folder == nullptr) { + m_pendingRenameItemId.clear(); + m_pendingRenameSurface = RenameSurface::None; + return false; + } + initialText = folder->label; + } + + const UIRect bounds = + BuildRenameBounds(m_pendingRenameItemId, m_pendingRenameSurface); + if (!HasValidBounds(bounds)) { + return false; + } + + const Widgets::UIEditorTextFieldMetrics textFieldMetrics = + BuildUIEditorInlineRenameTextFieldMetrics( + bounds, + BuildUIEditorPropertyGridTextFieldMetrics( + ResolveUIEditorPropertyGridMetrics(), + ResolveUIEditorTextFieldMetrics())); + + UIEditorInlineRenameSessionRequest request = {}; + request.beginSession = true; + request.itemId = m_pendingRenameItemId; + request.initialText = initialText; + request.bounds = bounds; + m_renameFrame = UpdateUIEditorInlineRenameSession( + m_renameState, + request, + {}, + textFieldMetrics); + if (!m_renameFrame.result.sessionStarted) { + return false; + } + + m_activeRenameSurface = m_pendingRenameSurface; + m_pendingRenameItemId.clear(); + m_pendingRenameSurface = RenameSurface::None; + return true; +} + +void ProjectPanel::UpdateRenameSession( + const std::vector& inputEvents) { + if (!m_renameState.active || m_activeRenameSurface == RenameSurface::None) { + return; + } + + const UIRect bounds = BuildRenameBounds(m_renameState.itemId, m_activeRenameSurface); + if (!HasValidBounds(bounds)) { + ClearRenameState(); + return; + } + + const Widgets::UIEditorTextFieldMetrics textFieldMetrics = + BuildUIEditorInlineRenameTextFieldMetrics( + bounds, + BuildUIEditorPropertyGridTextFieldMetrics( + ResolveUIEditorPropertyGridMetrics(), + ResolveUIEditorTextFieldMetrics())); + + UIEditorInlineRenameSessionRequest request = {}; + request.itemId = m_renameState.itemId; + request.initialText = m_renameState.textFieldSpec.value; + request.bounds = bounds; + m_renameFrame = UpdateUIEditorInlineRenameSession( + m_renameState, + request, + inputEvents, + textFieldMetrics); + if (!m_renameFrame.result.sessionCommitted) { + if (m_renameFrame.result.sessionCanceled) { + m_activeRenameSurface = RenameSurface::None; + } + return; + } + + RenameSurface committedSurface = m_activeRenameSurface; + m_activeRenameSurface = RenameSurface::None; + + std::string renamedItemId = {}; + if (m_renameFrame.result.valueChanged && + !ResolveProjectRuntime()->RenameItem( + m_renameFrame.result.itemId, + m_renameFrame.result.valueAfter, + &renamedItemId)) { + return; + } + + if (renamedItemId.empty()) { + renamedItemId = m_renameFrame.result.itemId; + } + + SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId = renamedItemId; + if (committedSurface == RenameSurface::Grid) { + if (FindAssetEntry(renamedItemId) != nullptr) { + EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, FindAssetEntry(renamedItemId)); + } else if (m_assetSelection.HasSelection()) { + m_assetSelection.ClearSelection(); + EmitSelectionClearedEvent(EventSource::GridPrimary); + } + } else if (committedSurface == RenameSurface::Tree) { + m_assetSelection.ClearSelection(); + EmitEvent( + EventKind::FolderNavigated, + EventSource::Tree, + FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + } +} + +std::optional ProjectPanel::ResolveEditCommandTarget( + std::string_view explicitItemId, + bool forceCurrentFolder) const { + if (!HasProjectRuntime()) { return std::nullopt; } - const FolderEntry* folder = FindFolderEntry(m_folderSelection.GetSelectedId()); - if (folder == nullptr) { - return std::nullopt; - } - - EditCommandTarget target = {}; - target.itemId = folder->itemId; - target.absolutePath = folder->absolutePath; - target.displayName = folder->label; - target.directory = true; - target.assetsRoot = folder->absolutePath == m_browserModel.GetAssetsRootPath(); - return target; + return ResolveProjectRuntime()->ResolveEditCommandTarget( + explicitItemId, + forceCurrentFolder); } const UIEditorPanelContentHostPanelState* ProjectPanel::FindMountedProjectPanel( @@ -132,33 +553,362 @@ const UIEditorPanelContentHostPanelState* ProjectPanel::FindMountedProjectPanel( } void ProjectPanel::SyncCurrentFolderSelection() { - const std::string& currentFolderId = m_browserModel.GetCurrentFolderId(); + if (!HasProjectRuntime()) { + m_folderSelection.ClearSelection(); + return; + } + + const std::string& currentFolderId = GetBrowserModel().GetCurrentFolderId(); if (currentFolderId.empty()) { m_folderSelection.ClearSelection(); return; } const std::vector ancestorFolderIds = - m_browserModel.CollectCurrentFolderAncestorIds(); + GetBrowserModel().CollectCurrentFolderAncestorIds(); for (const std::string& ancestorFolderId : ancestorFolderIds) { m_folderExpansion.Expand(ancestorFolderId); } m_folderSelection.SetSelection(currentFolderId); } +void ProjectPanel::SyncAssetSelectionFromRuntime() { + const EditorProjectRuntime* runtime = ResolveProjectRuntime(); + if (runtime == nullptr || !runtime->HasSelection()) { + m_assetSelection.ClearSelection(); + return; + } + + if (FindAssetEntry(runtime->GetSelection().itemId) != nullptr) { + m_assetSelection.SetSelection(runtime->GetSelection().itemId); + return; + } + + m_assetSelection.ClearSelection(); +} + bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) { - if (!m_browserModel.NavigateToFolder(itemId)) { + if (!ResolveProjectRuntime()->NavigateToFolder(itemId)) { return false; } SyncCurrentFolderSelection(); - m_assetSelection.ClearSelection(); + SyncAssetSelectionFromRuntime(); m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); - EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_browserModel.GetCurrentFolderId())); + EmitEvent( + EventKind::FolderNavigated, + source, + FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); return true; } +bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) { + const AssetEntry* asset = FindAssetEntry(itemId); + if (asset == nullptr) { + return false; + } + + if (asset->directory) { + const bool navigated = ResolveProjectRuntime()->OpenItem(asset->itemId); + if (navigated && HasValidBounds(m_layout.bounds)) { + SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); + m_layout = BuildLayout(m_layout.bounds); + m_hoveredAssetItemId.clear(); + EmitEvent( + EventKind::FolderNavigated, + source, + FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + } + return navigated; + } + + if (!ResolveProjectRuntime()->OpenItem(asset->itemId)) { + return false; + } + + EmitEvent(EventKind::AssetOpened, source, asset); + return true; +} + +void ProjectPanel::OpenContextMenu( + const UIPoint& anchorPosition, + std::string_view targetItemId, + bool forceCurrentFolder) { + CloseContextMenu(); + ClearRenameState(); + + m_contextMenu.open = true; + m_contextMenu.forceCurrentFolder = forceCurrentFolder; + m_contextMenu.anchorPosition = anchorPosition; + m_contextMenu.targetItemId = std::string(targetItemId); + RebuildContextMenu(); +} + +void ProjectPanel::CloseContextMenu() { + m_contextMenu = {}; +} + +void ProjectPanel::RebuildContextMenu() { + if (!m_contextMenu.open || !HasValidBounds(m_layout.bounds)) { + CloseContextMenu(); + return; + } + + const AssetCommandTarget assetTarget = + ResolveAssetCommandTarget( + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const std::optional editTarget = + ResolveEditCommandTarget( + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + + std::vector items = {}; + const bool canOpen = + assetTarget.subjectAsset != nullptr && + (assetTarget.subjectAsset->directory || assetTarget.subjectAsset->canOpen); + const bool canCreate = assetTarget.containerFolder != nullptr; + const UIEditorHostCommandEvaluationResult showInExplorerEvaluation = + EvaluateAssetCommand( + "assets.show_in_explorer", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const UIEditorHostCommandEvaluationResult copyPathEvaluation = + EvaluateAssetCommand( + "assets.copy_path", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const UIEditorHostCommandEvaluationResult createFolderEvaluation = + EvaluateAssetCommand( + "assets.create_folder", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const UIEditorHostCommandEvaluationResult createMaterialEvaluation = + EvaluateAssetCommand( + "assets.create_material", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const UIEditorHostCommandEvaluationResult renameEvaluation = + EvaluateEditCommand( + "edit.rename", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + const UIEditorHostCommandEvaluationResult deleteEvaluation = + EvaluateEditCommand( + "edit.delete", + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder); + + if (canOpen) { + items.push_back( + BuildContextMenuCommandItem( + "project.context.open", + "Open")); + } + + if (canCreate) { + AppendContextMenuSeparator(items, "project.context.separator.open_create"); + items.push_back( + BuildContextMenuCommandItem( + "assets.create_folder", + "Create Folder", + createFolderEvaluation.executable)); + items.push_back( + BuildContextMenuCommandItem( + "assets.create_material", + "Create Material", + createMaterialEvaluation.executable)); + } + + if (showInExplorerEvaluation.executable || copyPathEvaluation.executable) { + AppendContextMenuSeparator(items, "project.context.separator.create_util"); + items.push_back( + BuildContextMenuCommandItem( + "assets.show_in_explorer", + "Show in Explorer", + showInExplorerEvaluation.executable)); + items.push_back( + BuildContextMenuCommandItem( + "assets.copy_path", + "Copy Path", + copyPathEvaluation.executable)); + } + + if (editTarget.has_value()) { + AppendContextMenuSeparator(items, "project.context.separator.util_edit"); + items.push_back( + BuildContextMenuCommandItem( + "edit.rename", + "Rename", + renameEvaluation.executable)); + items.push_back( + BuildContextMenuCommandItem( + "edit.delete", + "Delete", + deleteEvaluation.executable)); + } + + if (items.empty()) { + CloseContextMenu(); + return; + } + + m_contextMenu.items = std::move(items); + const Widgets::UIEditorMenuPopupMetrics& popupMetrics = + ResolveUIEditorMenuPopupMetrics(); + const float popupWidth = (std::max)( + 156.0f, + Widgets::ResolveUIEditorMenuPopupDesiredWidth( + m_contextMenu.items, + popupMetrics)); + const float popupHeight = + Widgets::MeasureUIEditorMenuPopupHeight( + m_contextMenu.items, + popupMetrics); + const ::XCEngine::UI::Widgets::UIPopupPlacementResult placement = + ::XCEngine::UI::Widgets::ResolvePopupPlacementRect( + UIRect( + m_contextMenu.anchorPosition.x, + m_contextMenu.anchorPosition.y, + 1.0f, + 1.0f), + UISize(popupWidth, popupHeight), + m_layout.bounds, + ::XCEngine::UI::Widgets::UIPopupPlacement::BottomStart); + m_contextMenu.layout = Widgets::BuildUIEditorMenuPopupLayout( + placement.rect, + m_contextMenu.items, + popupMetrics); + m_contextMenu.widgetState = {}; + m_contextMenu.widgetState.focused = true; +} + +bool ProjectPanel::HandleContextMenuEvent(const UIInputEvent& event) { + if (!m_contextMenu.open) { + return false; + } + + const Widgets::UIEditorMenuPopupHitTarget hitTarget = + Widgets::HitTestUIEditorMenuPopup( + m_contextMenu.layout, + m_contextMenu.items, + event.position); + + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + m_contextMenu.widgetState.hoveredIndex = + hitTarget.kind == Widgets::UIEditorMenuPopupHitTargetKind::Item && + hitTarget.index < m_contextMenu.items.size() && + m_contextMenu.items[hitTarget.index].enabled + ? hitTarget.index + : Widgets::UIEditorMenuPopupInvalidIndex; + return hitTarget.kind != Widgets::UIEditorMenuPopupHitTargetKind::None; + + case UIInputEventType::PointerLeave: + m_contextMenu.widgetState.hoveredIndex = + Widgets::UIEditorMenuPopupInvalidIndex; + return false; + + case UIInputEventType::PointerButtonDown: + if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right) { + if (hitTarget.kind != Widgets::UIEditorMenuPopupHitTargetKind::None) { + return true; + } + + CloseContextMenu(); + return false; + } + + if (event.pointerButton != ::XCEngine::UI::UIPointerButton::Left) { + return hitTarget.kind != Widgets::UIEditorMenuPopupHitTargetKind::None; + } + + if (hitTarget.kind == Widgets::UIEditorMenuPopupHitTargetKind::Item && + hitTarget.index < m_contextMenu.items.size() && + m_contextMenu.items[hitTarget.index].enabled) { + const std::string itemId = m_contextMenu.items[hitTarget.index].itemId; + DispatchContextMenuItem(itemId); + CloseContextMenu(); + return true; + } + + if (hitTarget.kind == Widgets::UIEditorMenuPopupHitTargetKind::PopupSurface) { + return true; + } + + CloseContextMenu(); + return true; + + case UIInputEventType::FocusLost: + CloseContextMenu(); + return false; + + case UIInputEventType::KeyDown: + if (event.keyCode == VK_ESCAPE) { + CloseContextMenu(); + return true; + } + return false; + + default: + return false; + } +} + +bool ProjectPanel::DispatchContextMenuItem(std::string_view itemId) { + if (itemId == "project.context.open") { + return OpenProjectItem( + m_contextMenu.targetItemId, + EventSource::GridSecondary); + } + + if (itemId.rfind("assets.", 0u) == 0u) { + return DispatchAssetCommand( + itemId, + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder) + .commandExecuted; + } + + if (itemId.rfind("edit.", 0u) == 0u) { + return DispatchEditCommand( + itemId, + m_contextMenu.targetItemId, + m_contextMenu.forceCurrentFolder) + .commandExecuted; + } + + return false; +} + +void ProjectPanel::AppendContextMenu(UIDrawList& drawList) const { + if (!m_contextMenu.open || m_contextMenu.items.empty()) { + return; + } + + const Widgets::UIEditorMenuPopupMetrics& popupMetrics = + ResolveUIEditorMenuPopupMetrics(); + const Widgets::UIEditorMenuPopupPalette& popupPalette = + ResolveUIEditorMenuPopupPalette(); + Widgets::AppendUIEditorMenuPopupBackground( + drawList, + m_contextMenu.layout, + m_contextMenu.items, + m_contextMenu.widgetState, + popupPalette, + popupMetrics); + Widgets::AppendUIEditorMenuPopupForeground( + drawList, + m_contextMenu.layout, + m_contextMenu.items, + m_contextMenu.widgetState, + popupPalette, + popupMetrics); +} + void ProjectPanel::EmitEvent( EventKind kind, EventSource source, @@ -173,6 +923,7 @@ void ProjectPanel::EmitEvent( event.itemId = folder->itemId; event.absolutePath = folder->absolutePath; event.displayName = folder->label; + event.itemKind = BrowserModel::ItemKind::Folder; event.directory = true; m_frameEvents.push_back(std::move(event)); } @@ -192,6 +943,7 @@ void ProjectPanel::EmitEvent( event.itemId = asset->itemId; event.absolutePath = asset->absolutePath; event.displayName = asset->displayName; + event.itemKind = asset->kind; event.directory = asset->directory; } m_frameEvents.push_back(std::move(event)); @@ -204,9 +956,241 @@ void ProjectPanel::EmitSelectionClearedEvent(EventSource source) { m_frameEvents.push_back(std::move(event)); } +std::vector ProjectPanel::BuildTreeInteractionInputEvents( + const std::vector& inputEvents, + const UIRect& bounds, + bool allowInteraction, + bool panelActive) const { + const std::vector rawEvents = + FilterProjectPanelInputEvents( + bounds, + inputEvents, + allowInteraction, + panelActive, + HasActivePointerCapture()); + const std::vector treeRawEvents = + FilterTreeInputEvents(rawEvents, m_splitterDragging || m_assetDragState.dragging); + + const Widgets::UIEditorTreeViewLayout layout = + m_treeFrame.layout.bounds.width > 0.0f + ? m_treeFrame.layout + : Widgets::BuildUIEditorTreeViewLayout( + m_layout.treeRect, + GetBrowserModel().GetTreeItems(), + m_folderExpansion, + ResolveUIEditorTreeViewMetrics()); + return TreeDrag::BuildInteractionInputEvents( + m_treeDragState, + layout, + GetBrowserModel().GetTreeItems(), + treeRawEvents); +} + +UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateAssetCommand( + std::string_view commandId) const { + return EvaluateAssetCommand(commandId, {}, false); +} + +UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateAssetCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) const { + const AssetCommandTarget target = + ResolveAssetCommandTarget(explicitItemId, forceCurrentFolder); + + if (commandId == "assets.create_folder") { + if (target.containerFolder == nullptr) { + return BuildEvaluationResult(false, "Project has no active folder."); + } + + return BuildEvaluationResult( + true, + "Create a folder under '" + target.containerFolder->label + "'."); + } + + if (commandId == "assets.create_material") { + if (target.containerFolder == nullptr) { + return BuildEvaluationResult(false, "Project has no active folder."); + } + + return BuildEvaluationResult( + true, + "Create a material under '" + target.containerFolder->label + "'."); + } + + if (commandId == "assets.copy_path") { + if (target.subjectRelativePath.empty()) { + return BuildEvaluationResult(false, "Project has no selected item or current folder path."); + } + + return BuildEvaluationResult( + true, + "Copy project path '" + target.subjectRelativePath + "'."); + } + + if (commandId == "assets.show_in_explorer") { + if (target.subjectItemId.empty() || target.subjectDisplayName.empty()) { + return BuildEvaluationResult(false, "Project has no selected item or current folder."); + } + + return BuildEvaluationResult( + true, + "Reveal '" + target.subjectDisplayName + "' in Explorer."); + } + + return BuildEvaluationResult(false, "Project does not expose this asset command."); +} + +UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( + std::string_view commandId) { + return DispatchAssetCommand(commandId, {}, false); +} + +UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateAssetCommand(commandId, explicitItemId, forceCurrentFolder); + if (!evaluation.executable) { + return BuildDispatchResult(false, evaluation.message); + } + + const AssetCommandTarget target = + ResolveAssetCommandTarget(explicitItemId, forceCurrentFolder); + + const auto finalizeCreatedAsset = + [this](std::string_view createdItemId) { + ClearRenameState(); + SyncCurrentFolderSelection(); + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId = std::string(createdItemId); + m_lastPrimaryClickTimeMs = 0u; + ResolveProjectRuntime()->SetSelection(createdItemId); + SyncAssetSelectionFromRuntime(); + + const AssetEntry* createdAsset = FindAssetEntry(createdItemId); + if (createdAsset == nullptr) { + return false; + } + + EmitEvent(EventKind::AssetSelected, EventSource::Command, createdAsset); + QueueRenameSession(createdItemId, RenameSurface::Grid); + EmitEvent(EventKind::RenameRequested, EventSource::Command, createdAsset); + if (m_visible) { + TryStartQueuedRenameSession(); + } + return true; + }; + + if (commandId == "assets.create_folder") { + if (target.containerFolder == nullptr) { + return BuildDispatchResult(false, "Project has no active folder."); + } + + std::string createdFolderId = {}; + if (!ResolveProjectRuntime()->CreateFolder( + target.containerFolder->itemId, + "New Folder", + &createdFolderId)) { + return BuildDispatchResult(false, "Failed to create a folder in the current Project directory."); + } + + if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { + NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); + if (HasValidBounds(m_layout.bounds)) { + m_layout = BuildLayout(m_layout.bounds); + } + } + + if (finalizeCreatedAsset(createdFolderId)) { + if (const AssetEntry* createdFolder = FindAssetEntry(createdFolderId); + createdFolder != nullptr) { + return BuildDispatchResult( + true, + "Created folder '" + createdFolder->displayName + "'."); + } + } + + return BuildDispatchResult(true, "Created a new folder in the current Project directory."); + } + + if (commandId == "assets.create_material") { + if (target.containerFolder == nullptr) { + return BuildDispatchResult(false, "Project has no active folder."); + } + + std::string createdItemId = {}; + if (!ResolveProjectRuntime()->CreateMaterial( + target.containerFolder->itemId, + "New Material", + &createdItemId)) { + return BuildDispatchResult(false, "Failed to create a material in the current Project directory."); + } + + if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { + NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); + if (HasValidBounds(m_layout.bounds)) { + m_layout = BuildLayout(m_layout.bounds); + } + } + + if (finalizeCreatedAsset(createdItemId)) { + if (const AssetEntry* createdMaterial = FindAssetEntry(createdItemId); + createdMaterial != nullptr) { + return BuildDispatchResult( + true, + "Created material '" + createdMaterial->nameWithExtension + "'."); + } + } + + return BuildDispatchResult(true, "Created a new material in the current Project directory."); + } + + if (commandId == "assets.copy_path") { + if (target.subjectRelativePath.empty()) { + return BuildDispatchResult(false, "Project has no selected item or current folder path."); + } + + if (!CopyTextToClipboard(target.subjectRelativePath)) { + return BuildDispatchResult(false, "Failed to copy the project path to the clipboard."); + } + + return BuildDispatchResult( + true, + "Copied project path '" + target.subjectRelativePath + "'."); + } + + if (commandId == "assets.show_in_explorer") { + if (target.subjectPath.empty()) { + return BuildDispatchResult(false, "Project has no selected item or current folder."); + } + + if (!ShowPathInExplorer(target.subjectPath, target.showInExplorerSelectTarget)) { + return BuildDispatchResult(false, "Failed to reveal the target path in Explorer."); + } + + return BuildDispatchResult( + true, + target.showInExplorerSelectTarget + ? "Revealed '" + target.subjectRelativePath + "' in Explorer." + : "Opened current Project folder in Explorer."); + } + + return BuildDispatchResult(false, "Project does not expose this asset command."); +} + UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateEditCommand( std::string_view commandId) const { - const std::optional target = ResolveEditCommandTarget(); + return EvaluateEditCommand(commandId, {}, false); +} + +UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateEditCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) const { + const std::optional target = + ResolveEditCommandTarget(explicitItemId, forceCurrentFolder); if (!target.has_value()) { return BuildEvaluationResult(false, "Select an asset or folder in Project first."); } @@ -224,8 +1208,8 @@ UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateEditCommand( if (commandId == "edit.delete") { return BuildEvaluationResult( - false, - "Project delete is blocked until asset metadata ownership is wired."); + true, + "Delete project item '" + target->displayName + "'."); } if (commandId == "edit.duplicate") { @@ -247,35 +1231,74 @@ UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateEditCommand( UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand( std::string_view commandId) { - const UIEditorHostCommandEvaluationResult evaluation = EvaluateEditCommand(commandId); + return DispatchEditCommand(commandId, {}, false); +} + +UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateEditCommand(commandId, explicitItemId, forceCurrentFolder); if (!evaluation.executable) { return BuildDispatchResult(false, evaluation.message); } - if (commandId == "edit.rename") { - if (m_assetSelection.HasSelection()) { - if (const AssetEntry* asset = FindAssetEntry(m_assetSelection.GetSelectedId()); - asset != nullptr) { - EmitEvent(EventKind::RenameRequested, EventSource::None, asset); - return BuildDispatchResult( - true, - "Project rename requested for '" + asset->displayName + "'."); - } - } - - if (m_folderSelection.HasSelection()) { - if (const FolderEntry* folder = FindFolderEntry(m_folderSelection.GetSelectedId()); - folder != nullptr) { - EmitEvent(EventKind::RenameRequested, EventSource::None, folder); - return BuildDispatchResult( - true, - "Project rename requested for '" + folder->label + "'."); - } - } - + const std::optional target = + ResolveEditCommandTarget(explicitItemId, forceCurrentFolder); + if (!target.has_value()) { return BuildDispatchResult(false, "Select an asset or folder in Project first."); } + if (commandId == "edit.rename") { + const AssetEntry* renameAsset = + !explicitItemId.empty() ? FindAssetEntry(target->itemId) : GetSelectedAssetEntry(); + const FolderEntry* renameFolder = + !explicitItemId.empty() ? FindFolderEntry(target->itemId) : GetSelectedFolderEntry(); + const RenameSurface surface = + !explicitItemId.empty() && FindAssetEntry(explicitItemId) != nullptr + ? RenameSurface::Grid + : (GetSelectedAssetEntry() != nullptr ? RenameSurface::Grid : RenameSurface::Tree); + QueueRenameSession(target->itemId, surface); + if (surface == RenameSurface::Grid) { + EmitEvent(EventKind::RenameRequested, EventSource::GridPrimary, renameAsset); + } else { + EmitEvent(EventKind::RenameRequested, EventSource::Tree, renameFolder); + } + if (m_visible) { + TryStartQueuedRenameSession(); + } + return BuildDispatchResult( + true, + "Project rename requested for '" + target->displayName + "'."); + } + + if (commandId == "edit.delete") { + const std::string previousCurrentFolderId = GetBrowserModel().GetCurrentFolderId(); + const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection(); + if (!ResolveProjectRuntime()->DeleteItem(target->itemId)) { + return BuildDispatchResult(false, "Failed to delete the selected project item."); + } + + ClearRenameState(); + SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId.clear(); + if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) { + EmitSelectionClearedEvent(EventSource::GridPrimary); + } + if (previousCurrentFolderId != GetBrowserModel().GetCurrentFolderId()) { + EmitEvent( + EventKind::FolderNavigated, + EventSource::Tree, + FindFolderEntry(GetBrowserModel().GetCurrentFolderId())); + } + return BuildDispatchResult( + true, + "Deleted project item '" + target->displayName + "'."); + } + return BuildDispatchResult(false, "Project does not expose this edit command."); } @@ -283,6 +1306,7 @@ void ProjectPanel::ResetTransientFrames() { m_treeFrame = {}; m_frameEvents.clear(); m_layout = {}; + m_assetDropTargetSurface = DropTargetSurface::None; m_hoveredAssetItemId.clear(); m_hoveredBreadcrumbIndex = kInvalidLayoutIndex; m_pressedBreadcrumbIndex = kInvalidLayoutIndex; @@ -298,54 +1322,337 @@ void ProjectPanel::Update( m_requestPointerCapture = false; m_requestPointerRelease = false; m_frameEvents.clear(); + GridDrag::ResetTransientRequests(m_assetDragState); + TreeDrag::ResetTransientRequests(m_treeDragState); const UIEditorPanelContentHostPanelState* panelState = FindMountedProjectPanel(contentHostFrame); if (panelState == nullptr) { - if (m_splitterDragging) { + if (m_splitterDragging || + m_assetDragState.dragging || + m_treeDragState.dragging || + m_renameState.active) { m_requestPointerRelease = true; } m_visible = false; + m_assetDragState = {}; + m_treeDragState = {}; + CloseContextMenu(); + ClearRenameState(); ResetTransientFrames(); return; } - if (m_browserModel.GetTreeItems().empty()) { - m_browserModel.Refresh(); + if (!HasProjectRuntime()) { + m_visible = false; + CloseContextMenu(); + ClearRenameState(); + ResetTransientFrames(); + return; + } + + if (GetBrowserModel().GetTreeItems().empty()) { + ResolveProjectRuntime()->Refresh(); SyncCurrentFolderSelection(); + SyncAssetSelectionFromRuntime(); } m_visible = true; + SyncAssetSelectionFromRuntime(); const std::vector filteredEvents = FilterProjectPanelInputEvents( panelState->bounds, inputEvents, allowInteraction, panelActive, - m_splitterDragging); + HasActivePointerCapture()); m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width); m_layout = BuildLayout(panelState->bounds); + if (m_contextMenu.open) { + RebuildContextMenu(); + } const Widgets::UIEditorTreeViewMetrics treeMetrics = ResolveUIEditorTreeViewMetrics(); + m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( + m_layout.treeRect, + GetBrowserModel().GetTreeItems(), + m_folderExpansion, + treeMetrics); + m_treeFrame.result = {}; + + if ((m_renameState.active || !m_pendingRenameItemId.empty()) && + (m_assetDragState.dragging || m_treeDragState.dragging)) { + m_assetDragState = {}; + m_treeDragState = {}; + } + + if (m_renameState.active || !m_pendingRenameItemId.empty()) { + TryStartQueuedRenameSession(); + UpdateRenameSession(filteredEvents); + return; + } + const std::vector treeEvents = - FilterTreeInputEvents(filteredEvents, m_splitterDragging); + BuildTreeInteractionInputEvents( + inputEvents, + panelState->bounds, + allowInteraction, + panelActive); m_treeFrame = UpdateUIEditorTreeViewInteraction( m_treeInteractionState, m_folderSelection, m_folderExpansion, m_layout.treeRect, - m_browserModel.GetTreeItems(), + GetBrowserModel().GetTreeItems(), treeEvents, treeMetrics); if (m_treeFrame.result.selectionChanged && !m_treeFrame.result.selectedItemId.empty() && - m_treeFrame.result.selectedItemId != m_browserModel.GetCurrentFolderId()) { + m_treeFrame.result.selectedItemId != GetBrowserModel().GetCurrentFolderId()) { + CloseContextMenu(); NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); m_layout = BuildLayout(panelState->bounds); } + if (m_treeFrame.result.renameRequested && + !m_treeFrame.result.renameItemId.empty()) { + QueueRenameSession(m_treeFrame.result.renameItemId, RenameSurface::Tree); + EmitEvent( + EventKind::RenameRequested, + EventSource::Tree, + FindFolderEntry(m_treeFrame.result.renameItemId)); + TryStartQueuedRenameSession(); + return; + } + + struct ProjectTreeDragCallbacks { + ::XCEngine::UI::Widgets::UISelectionModel& folderSelection; + ::XCEngine::UI::Widgets::UIExpansionModel& folderExpansion; + EditorProjectRuntime& projectRuntime; + + bool IsItemSelected(std::string_view itemId) const { + return folderSelection.IsSelected(itemId); + } + + bool SelectDraggedItem(std::string_view itemId) { + return folderSelection.SetSelection(std::string(itemId)); + } + + bool CanDropOnItem( + std::string_view draggedItemId, + std::string_view targetItemId) const { + return projectRuntime.CanReparentFolder(draggedItemId, targetItemId); + } + + bool CanDropToRoot(std::string_view draggedItemId) const { + const std::optional parentId = + projectRuntime.GetParentFolderId(draggedItemId); + return parentId.has_value() && parentId.value() != "Assets"; + } + + bool CommitDropOnItem( + std::string_view draggedItemId, + std::string_view targetItemId) { + std::string movedFolderId = {}; + if (!projectRuntime.ReparentFolder(draggedItemId, targetItemId, &movedFolderId)) { + return false; + } + + folderExpansion.Expand(std::string(targetItemId)); + if (!movedFolderId.empty()) { + folderSelection.SetSelection(movedFolderId); + } + return true; + } + + bool CommitDropToRoot(std::string_view draggedItemId) { + std::string movedFolderId = {}; + if (!projectRuntime.MoveFolderToRoot(draggedItemId, &movedFolderId)) { + return false; + } + + if (!movedFolderId.empty()) { + folderSelection.SetSelection(movedFolderId); + } + return true; + } + } treeDragCallbacks{ m_folderSelection, m_folderExpansion, *ResolveProjectRuntime() }; + const TreeDrag::ProcessResult treeDragResult = + TreeDrag::ProcessInputEvents( + m_treeDragState, + m_treeFrame.layout, + GetBrowserModel().GetTreeItems(), + FilterTreeInputEvents(filteredEvents, m_splitterDragging || m_assetDragState.dragging), + m_layout.treeRect, + treeDragCallbacks); + if (treeDragResult.dropCommitted) { + const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection(); + CloseContextMenu(); + ResolveProjectRuntime()->ClearSelection(); + SyncAssetSelectionFromRuntime(); + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId.clear(); + if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) { + EmitSelectionClearedEvent(EventSource::Tree); + } + SyncCurrentFolderSelection(); + m_layout = BuildLayout(panelState->bounds); + m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( + m_layout.treeRect, + GetBrowserModel().GetTreeItems(), + m_folderExpansion, + treeMetrics); + } + + struct ProjectAssetDragCallbacks { + ::XCEngine::UI::Widgets::UISelectionModel& assetSelection; + ::XCEngine::UI::Widgets::UIExpansionModel& folderExpansion; + EditorProjectRuntime& projectRuntime; + const Layout& layout; + const std::vector& assetEntries; + std::function resolveDropTarget = {}; + DropTargetSurface dropTargetSurface = DropTargetSurface::None; + std::string movedItemId = {}; + + bool IsItemSelected(std::string_view itemId) const { + return assetSelection.IsSelected(itemId); + } + + bool SelectDraggedItem(std::string_view itemId) { + return assetSelection.SetSelection(std::string(itemId)); + } + + std::string ResolveDraggableItem(const UIPoint& point) const { + for (const AssetTileLayout& tile : layout.assetTiles) { + if (tile.itemIndex >= assetEntries.size()) { + continue; + } + if (ContainsPoint(tile.tileRect, point)) { + return assetEntries[tile.itemIndex].itemId; + } + } + + return {}; + } + + std::string ResolveDropTargetItem( + std::string_view draggedItemId, + const UIPoint& point) { + dropTargetSurface = DropTargetSurface::None; + std::string targetItemId = resolveDropTarget(point, &dropTargetSurface); + if (targetItemId.empty() || targetItemId == draggedItemId) { + dropTargetSurface = DropTargetSurface::None; + return {}; + } + + return targetItemId; + } + + bool CanDropOnItem( + std::string_view draggedItemId, + std::string_view targetItemId) const { + return projectRuntime.CanMoveItemToFolder(draggedItemId, targetItemId); + } + + bool CommitDropOnItem( + std::string_view draggedItemId, + std::string_view targetItemId) { + movedItemId.clear(); + if (!projectRuntime.MoveItemToFolder(draggedItemId, targetItemId, &movedItemId)) { + return false; + } + + folderExpansion.Expand(std::string(targetItemId)); + return true; + } + } assetDragCallbacks{ + m_assetSelection, + m_folderExpansion, + *ResolveProjectRuntime(), + m_layout, + GetBrowserModel().GetAssetEntries(), + [this](const UIPoint& point, DropTargetSurface* surface) { + return ResolveAssetDropTargetItemId(point, surface); + } + }; + const GridDrag::ProcessResult assetDragResult = + GridDrag::ProcessInputEvents( + m_assetDragState, + filteredEvents, + assetDragCallbacks); + m_assetDropTargetSurface = + m_assetDragState.dragging && m_assetDragState.validDropTarget + ? assetDragCallbacks.dropTargetSurface + : DropTargetSurface::None; + if (assetDragResult.selectionForced) { + const std::string& draggedItemId = m_assetDragState.draggedItemId.empty() + ? m_assetDragState.armedItemId + : m_assetDragState.draggedItemId; + if (const AssetEntry* draggedAsset = FindAssetEntry(draggedItemId); + draggedAsset != nullptr) { + EmitEvent(EventKind::AssetSelected, EventSource::GridDrag, draggedAsset); + } + } + if (assetDragResult.dropCommitted) { + const bool hadAssetSelection = ResolveProjectRuntime()->HasSelection(); + CloseContextMenu(); + ClearRenameState(); + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId.clear(); + m_lastPrimaryClickTimeMs = 0u; + SyncCurrentFolderSelection(); + + const std::string movedItemId = assetDragCallbacks.movedItemId.empty() + ? assetDragResult.draggedItemId + : assetDragCallbacks.movedItemId; + if (const AssetEntry* movedAsset = FindAssetEntry(movedItemId); + movedAsset != nullptr) { + ResolveProjectRuntime()->SetSelection(movedItemId); + SyncAssetSelectionFromRuntime(); + EmitEvent(EventKind::AssetSelected, EventSource::GridDrag, movedAsset); + } else { + ResolveProjectRuntime()->ClearSelection(); + SyncAssetSelectionFromRuntime(); + if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) { + EmitSelectionClearedEvent(EventSource::GridDrag); + } + } + + m_layout = BuildLayout(panelState->bounds); + m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( + m_layout.treeRect, + GetBrowserModel().GetTreeItems(), + m_folderExpansion, + treeMetrics); + } + + const bool suppressPanelPointerEvents = + m_assetDragState.dragging || + m_assetDragState.requestPointerCapture || + m_assetDragState.requestPointerRelease || + m_treeDragState.armed || + m_treeDragState.dragging || + m_treeDragState.requestPointerCapture || + m_treeDragState.requestPointerRelease; for (const UIInputEvent& event : filteredEvents) { + if (suppressPanelPointerEvents) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + case UIInputEventType::PointerEnter: + continue; + default: + break; + } + } + + if (HandleContextMenuEvent(event)) { + continue; + } + switch (event.type) { case UIInputEventType::FocusLost: m_hoveredAssetItemId.clear(); @@ -369,7 +1676,7 @@ void ProjectPanel::Update( m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position); m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position); const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position); - const auto& assetEntries = m_browserModel.GetAssetEntries(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); m_hoveredAssetItemId = hoveredAssetIndex < assetEntries.size() ? assetEntries[hoveredAssetIndex].itemId @@ -401,11 +1708,12 @@ void ProjectPanel::Update( break; } - const auto& assetEntries = m_browserModel.GetAssetEntries(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { - if (m_assetSelection.HasSelection()) { - m_assetSelection.ClearSelection(); + if (ResolveProjectRuntime()->HasSelection()) { + ResolveProjectRuntime()->ClearSelection(); + SyncAssetSelectionFromRuntime(); EmitSelectionClearedEvent(EventSource::Background); } break; @@ -413,7 +1721,8 @@ void ProjectPanel::Update( const AssetEntry& assetEntry = assetEntries[hitIndex]; const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId); - const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId); + const bool selectionChanged = ResolveProjectRuntime()->SetSelection(assetEntry.itemId); + SyncAssetSelectionFromRuntime(); if (selectionChanged) { EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry); } @@ -433,34 +1742,31 @@ void ProjectPanel::Update( break; } - if (assetEntry.directory) { - NavigateToFolder(assetEntry.itemId, EventSource::GridDoubleClick); - m_layout = BuildLayout(panelState->bounds); - m_hoveredAssetItemId.clear(); - } else { - EmitEvent(EventKind::AssetOpened, EventSource::GridDoubleClick, &assetEntry); - } + OpenProjectItem(assetEntry.itemId, EventSource::GridDoubleClick); break; } if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right && ContainsPoint(m_layout.gridRect, event.position)) { - const auto& assetEntries = m_browserModel.GetAssetEntries(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { EmitEvent( EventKind::ContextMenuRequested, EventSource::Background, static_cast(nullptr)); + OpenContextMenu(event.position, {}, true); break; } const AssetEntry& assetEntry = assetEntries[hitIndex]; if (!m_assetSelection.IsSelected(assetEntry.itemId)) { - m_assetSelection.SetSelection(assetEntry.itemId); + ResolveProjectRuntime()->SetSelection(assetEntry.itemId); + SyncAssetSelectionFromRuntime(); EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry); } EmitEvent(EventKind::ContextMenuRequested, EventSource::GridSecondary, &assetEntry); + OpenContextMenu(event.position, assetEntry.itemId, false); } break; @@ -501,9 +1807,9 @@ void ProjectPanel::Update( ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { Layout layout = {}; - const auto& assetEntries = m_browserModel.GetAssetEntries(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); const std::vector breadcrumbSegments = - m_browserModel.BuildBreadcrumbSegments(); + GetBrowserModel().BuildBreadcrumbSegments(); const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness; layout.bounds = UIRect( bounds.x, @@ -654,12 +1960,45 @@ std::size_t ProjectPanel::HitTestAssetTile(const UIPoint& point) const { return kInvalidLayoutIndex; } +std::string ProjectPanel::ResolveAssetDropTargetItemId( + const UIPoint& point, + DropTargetSurface* surface) const { + if (surface != nullptr) { + *surface = DropTargetSurface::None; + } + + if (ContainsPoint(m_treeFrame.layout.bounds, point)) { + Widgets::UIEditorTreeViewHitTarget hitTarget = + Widgets::HitTestUIEditorTreeView(m_treeFrame.layout, point); + if (hitTarget.itemIndex < GetBrowserModel().GetTreeItems().size() && + (hitTarget.kind == Widgets::UIEditorTreeViewHitTargetKind::Row || + hitTarget.kind == Widgets::UIEditorTreeViewHitTargetKind::Disclosure)) { + if (surface != nullptr) { + *surface = DropTargetSurface::Tree; + } + return GetBrowserModel().GetTreeItems()[hitTarget.itemIndex].itemId; + } + } + + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + const std::size_t assetIndex = HitTestAssetTile(point); + if (assetIndex < assetEntries.size() && + assetEntries[assetIndex].directory) { + if (surface != nullptr) { + *surface = DropTargetSurface::Grid; + } + return assetEntries[assetIndex].itemId; + } + + return {}; +} + void ProjectPanel::Append(UIDrawList& drawList) const { if (!m_visible || m_layout.bounds.width <= 0.0f || m_layout.bounds.height <= 0.0f) { return; } - const auto& assetEntries = m_browserModel.GetAssetEntries(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); drawList.AddFilledRect(m_layout.bounds, kSurfaceColor); drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor); @@ -682,7 +2021,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const { AppendUIEditorTreeViewBackground( drawList, m_treeFrame.layout, - m_browserModel.GetTreeItems(), + GetBrowserModel().GetTreeItems(), m_folderSelection, m_treeInteractionState.treeViewState, treePalette, @@ -690,10 +2029,50 @@ void ProjectPanel::Append(UIDrawList& drawList) const { AppendUIEditorTreeViewForeground( drawList, m_treeFrame.layout, - m_browserModel.GetTreeItems(), + GetBrowserModel().GetTreeItems(), treePalette, treeMetrics); + if (m_treeDragState.dragging && m_treeDragState.validDropTarget) { + if (m_treeDragState.dropToRoot) { + drawList.AddRectOutline( + m_treeFrame.layout.bounds, + kDropPreviewColor, + 1.0f, + 0.0f); + } else { + const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId( + m_treeFrame.layout, + GetBrowserModel().GetTreeItems(), + m_treeDragState.dropTargetItemId); + if (visibleIndex != Widgets::UIEditorTreeViewInvalidIndex && + visibleIndex < m_treeFrame.layout.rowRects.size()) { + drawList.AddRectOutline( + m_treeFrame.layout.rowRects[visibleIndex], + kDropPreviewColor, + 1.0f, + 0.0f); + } + } + } + + if (m_assetDragState.dragging && + m_assetDragState.validDropTarget && + m_assetDropTargetSurface == DropTargetSurface::Tree) { + const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId( + m_treeFrame.layout, + GetBrowserModel().GetTreeItems(), + m_assetDragState.dropTargetItemId); + if (visibleIndex != Widgets::UIEditorTreeViewInvalidIndex && + visibleIndex < m_treeFrame.layout.rowRects.size()) { + drawList.AddRectOutline( + m_treeFrame.layout.rowRects[visibleIndex], + kDropPreviewColor, + 1.0f, + 0.0f); + } + } + drawList.PushClipRect(m_layout.browserHeaderRect); for (std::size_t index = 0u; index < m_layout.breadcrumbItems.size(); ++index) { const BreadcrumbItemLayout& item = m_layout.breadcrumbItems[index]; @@ -745,6 +2124,55 @@ void ProjectPanel::Append(UIDrawList& drawList) const { drawList.PopClipRect(); } + if (m_assetDragState.dragging && + m_assetDragState.validDropTarget && + m_assetDropTargetSurface == DropTargetSurface::Grid) { + for (const AssetTileLayout& tile : m_layout.assetTiles) { + if (tile.itemIndex >= assetEntries.size()) { + continue; + } + + const AssetEntry& assetEntry = assetEntries[tile.itemIndex]; + if (assetEntry.itemId != m_assetDragState.dropTargetItemId) { + continue; + } + + drawList.AddRectOutline( + tile.tileRect, + kDropPreviewColor, + 1.0f, + 0.0f); + break; + } + } + + if (m_renameState.active) { + const Widgets::UIEditorTextFieldPalette textFieldPalette = + BuildUIEditorPropertyGridTextFieldPalette( + ResolveUIEditorPropertyGridPalette(), + ResolveUIEditorTextFieldPalette()); + const Widgets::UIEditorTextFieldMetrics textFieldMetrics = + BuildUIEditorInlineRenameTextFieldMetrics( + BuildRenameBounds(m_renameState.itemId, m_activeRenameSurface), + BuildUIEditorPropertyGridTextFieldMetrics( + ResolveUIEditorPropertyGridMetrics(), + ResolveUIEditorTextFieldMetrics())); + Widgets::AppendUIEditorTextFieldBackground( + drawList, + m_renameFrame.layout, + m_renameState.textFieldSpec, + m_renameState.textFieldInteraction.textFieldState, + textFieldPalette, + textFieldMetrics); + Widgets::AppendUIEditorTextFieldForeground( + drawList, + m_renameFrame.layout, + m_renameState.textFieldSpec, + m_renameState.textFieldInteraction.textFieldState, + textFieldPalette, + textFieldMetrics); + } + if (assetEntries.empty()) { const UIRect messageRect( m_layout.gridRect.x, @@ -757,6 +2185,8 @@ void ProjectPanel::Append(UIDrawList& drawList) const { kTextMuted, kHeaderFontSize); } + + AppendContextMenu(drawList); } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Project/ProjectPanel.h b/new_editor/app/Features/Project/ProjectPanel.h index bf8c3f24..904942b9 100644 --- a/new_editor/app/Features/Project/ProjectPanel.h +++ b/new_editor/app/Features/Project/ProjectPanel.h @@ -1,10 +1,15 @@ #pragma once -#include "Composition/EditorEditCommandRoute.h" +#include "Features/Shared/GridItemDragDrop.h" +#include "Features/Shared/TreeItemDragDrop.h" +#include "Project/EditorProjectRuntime.h" #include "ProjectBrowserModel.h" +#include +#include #include #include +#include #include #include @@ -13,6 +18,7 @@ #include #include +#include #include #include #include @@ -21,7 +27,6 @@ namespace XCEngine::UI::Editor::App { class BuiltInIcons; - class ProjectPanel final : public EditorEditCommandRoute { public: enum class CursorKind : std::uint8_t { @@ -43,9 +48,11 @@ public: None = 0, Tree, Breadcrumb, + Command, GridPrimary, GridDoubleClick, GridSecondary, + GridDrag, Background }; @@ -55,10 +62,13 @@ public: std::string itemId = {}; std::filesystem::path absolutePath = {}; std::string displayName = {}; + ProjectBrowserModel::ItemKind itemKind = + ProjectBrowserModel::ItemKind::File; bool directory = false; }; void Initialize(const std::filesystem::path& repoRoot); + void SetProjectRuntime(EditorProjectRuntime* projectRuntime); void SetBuiltInIcons(const BuiltInIcons* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); void ResetInteractionState(); @@ -74,6 +84,10 @@ public: bool WantsHostPointerRelease() const; bool HasActivePointerCapture() const; const std::vector& GetFrameEvents() const; + UIEditorHostCommandEvaluationResult EvaluateAssetCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchAssetCommand( + std::string_view commandId) override; UIEditorHostCommandEvaluationResult EvaluateEditCommand( std::string_view commandId) const override; UIEditorHostCommandDispatchResult DispatchEditCommand( @@ -83,12 +97,28 @@ private: using BrowserModel = ::XCEngine::UI::Editor::App::ProjectBrowserModel; using FolderEntry = BrowserModel::FolderEntry; using AssetEntry = BrowserModel::AssetEntry; - struct EditCommandTarget { - std::string itemId = {}; - std::filesystem::path absolutePath = {}; - std::string displayName = {}; - bool directory = false; - bool assetsRoot = false; + using EditCommandTarget = EditorProjectRuntime::EditCommandTarget; + using AssetCommandTarget = EditorProjectRuntime::AssetCommandTarget; + enum class RenameSurface : std::uint8_t { + None = 0, + Tree, + Grid + }; + + enum class DropTargetSurface : std::uint8_t { + None = 0, + Tree, + Grid + }; + + struct ContextMenuState { + bool open = false; + bool forceCurrentFolder = false; + ::XCEngine::UI::UIPoint anchorPosition = {}; + std::string targetItemId = {}; + std::vector items = {}; + Widgets::UIEditorMenuPopupLayout layout = {}; + Widgets::UIEditorMenuPopupState widgetState = {}; }; struct BreadcrumbItemLayout { @@ -120,31 +150,98 @@ private: std::vector assetTiles = {}; }; + EditorProjectRuntime* ResolveProjectRuntime(); + const EditorProjectRuntime* ResolveProjectRuntime() const; + bool HasProjectRuntime() const; + BrowserModel& GetBrowserModel(); + const BrowserModel& GetBrowserModel() const; const FolderEntry* FindFolderEntry(std::string_view itemId) const; const AssetEntry* FindAssetEntry(std::string_view itemId) const; - std::optional ResolveEditCommandTarget() const; + AssetCommandTarget ResolveAssetCommandTarget( + std::string_view explicitItemId = {}, + bool forceCurrentFolder = false) const; + std::optional ResolveEditCommandTarget( + std::string_view explicitItemId = {}, + bool forceCurrentFolder = false) const; const UIEditorPanelContentHostPanelState* FindMountedProjectPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const; Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const; std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const; + std::string ResolveAssetDropTargetItemId( + const ::XCEngine::UI::UIPoint& point, + DropTargetSurface* surface = nullptr) const; void SyncCurrentFolderSelection(); bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None); void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder); void EmitEvent(EventKind kind, EventSource source, const AssetEntry* asset); void EmitSelectionClearedEvent(EventSource source); + bool OpenProjectItem(std::string_view itemId, EventSource source); + void OpenContextMenu( + const ::XCEngine::UI::UIPoint& anchorPosition, + std::string_view targetItemId, + bool forceCurrentFolder); + void CloseContextMenu(); + void RebuildContextMenu(); + bool HandleContextMenuEvent(const ::XCEngine::UI::UIInputEvent& event); + bool DispatchContextMenuItem(std::string_view itemId); + void AppendContextMenu(::XCEngine::UI::UIDrawList& drawList) const; + void ClearRenameState(); + void SyncAssetSelectionFromRuntime(); + void QueueRenameSession( + std::string_view itemId, + RenameSurface surface); + bool TryStartQueuedRenameSession(); + void UpdateRenameSession( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents); + ::XCEngine::UI::UIRect BuildRenameBounds( + std::string_view itemId, + RenameSurface surface) const; + const AssetEntry* GetSelectedAssetEntry() const; + const FolderEntry* GetSelectedFolderEntry() const; void ResetTransientFrames(); + std::vector<::XCEngine::UI::UIInputEvent> BuildTreeInteractionInputEvents( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const ::XCEngine::UI::UIRect& bounds, + bool allowInteraction, + bool panelActive) const; + UIEditorHostCommandEvaluationResult EvaluateAssetCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) const; + UIEditorHostCommandDispatchResult DispatchAssetCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder); + UIEditorHostCommandEvaluationResult EvaluateEditCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder) const; + UIEditorHostCommandDispatchResult DispatchEditCommand( + std::string_view commandId, + std::string_view explicitItemId, + bool forceCurrentFolder); - BrowserModel m_browserModel = {}; + std::unique_ptr m_ownedProjectRuntime = {}; + EditorProjectRuntime* m_projectRuntime = nullptr; const BuiltInIcons* m_icons = nullptr; const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr; ::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {}; ::XCEngine::UI::Widgets::UIExpansionModel m_folderExpansion = {}; ::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {}; + GridItemDragDrop::State m_assetDragState = {}; + TreeItemDragDrop::State m_treeDragState = {}; UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {}; + UIEditorInlineRenameSessionState m_renameState = {}; + UIEditorInlineRenameSessionFrame m_renameFrame = {}; std::vector m_frameEvents = {}; Layout m_layout = {}; + ContextMenuState m_contextMenu = {}; + std::string m_pendingRenameItemId = {}; + RenameSurface m_pendingRenameSurface = RenameSurface::None; + RenameSurface m_activeRenameSurface = RenameSurface::None; + DropTargetSurface m_assetDropTargetSurface = DropTargetSurface::None; std::string m_hoveredAssetItemId = {}; std::string m_lastPrimaryClickedAssetId = {}; float m_navigationWidth = 248.0f; diff --git a/new_editor/app/Features/Project/ProjectPanelInternal.h b/new_editor/app/Features/Project/ProjectPanelInternal.h index c6e90290..f695ccb0 100644 --- a/new_editor/app/Features/Project/ProjectPanelInternal.h +++ b/new_editor/app/Features/Project/ProjectPanelInternal.h @@ -4,6 +4,7 @@ #include "Rendering/Assets/BuiltInIcons.h" +#include #include #include @@ -21,7 +22,6 @@ using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; -inline constexpr std::string_view kProjectPanelId = "project"; inline constexpr std::size_t kInvalidLayoutIndex = static_cast(-1); inline constexpr float kBrowserHeaderHeight = 24.0f; @@ -55,6 +55,7 @@ inline constexpr UIColor kTileSelectedColor(0.18f, 0.18f, 0.18f, 1.0f); inline constexpr UIColor kTilePreviewFillColor(0.15f, 0.15f, 0.15f, 1.0f); inline constexpr UIColor kTilePreviewShadeColor(0.12f, 0.12f, 0.12f, 1.0f); inline constexpr UIColor kTilePreviewOutlineColor(0.920f, 0.920f, 0.920f, 0.20f); +inline constexpr UIColor kDropPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); bool ContainsPoint(const UIRect& rect, const UIPoint& point); float ClampNonNegative(float value); diff --git a/new_editor/app/Features/Shared/GridItemDragDrop.h b/new_editor/app/Features/Shared/GridItemDragDrop.h new file mode 100644 index 00000000..3304c9e5 --- /dev/null +++ b/new_editor/app/Features/Shared/GridItemDragDrop.h @@ -0,0 +1,157 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App::GridItemDragDrop { + +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIPointerButton; + +inline constexpr float kDefaultDragThreshold = 4.0f; + +struct State { + std::string armedItemId = {}; + std::string draggedItemId = {}; + std::string dropTargetItemId = {}; + UIPoint pressPosition = {}; + bool armed = false; + bool dragging = false; + bool validDropTarget = false; + bool requestPointerCapture = false; + bool requestPointerRelease = false; +}; + +struct ProcessResult { + bool selectionForced = false; + bool dropCommitted = false; + std::string draggedItemId = {}; + std::string dropTargetItemId = {}; +}; + +inline void ResetTransientRequests(State& state) { + state.requestPointerCapture = false; + state.requestPointerRelease = false; +} + +inline bool HasActivePointerCapture(const State& state) { + return state.dragging; +} + +inline float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) { + const float dx = lhs.x - rhs.x; + const float dy = lhs.y - rhs.y; + return dx * dx + dy * dy; +} + +template +ProcessResult ProcessInputEvents( + State& state, + const std::vector& inputEvents, + Callbacks& callbacks, + float dragThreshold = kDefaultDragThreshold) { + ProcessResult result = {}; + + for (const UIInputEvent& event : inputEvents) { + switch (event.type) { + case UIInputEventType::PointerButtonDown: + if (event.pointerButton == UIPointerButton::Left) { + state.armedItemId = callbacks.ResolveDraggableItem(event.position); + state.pressPosition = event.position; + state.armed = !state.armedItemId.empty(); + if (!state.armed) { + state.draggedItemId.clear(); + state.dropTargetItemId.clear(); + state.validDropTarget = false; + } + } + break; + + case UIInputEventType::PointerMove: + if (state.armed && + !state.dragging && + ComputeSquaredDistance(event.position, state.pressPosition) >= + dragThreshold * dragThreshold) { + state.dragging = !state.armedItemId.empty(); + state.draggedItemId = state.armedItemId; + state.dropTargetItemId.clear(); + state.validDropTarget = false; + if (state.dragging) { + state.requestPointerCapture = true; + if (!callbacks.IsItemSelected(state.draggedItemId)) { + result.selectionForced = + callbacks.SelectDraggedItem(state.draggedItemId); + } + } + } + + if (state.dragging) { + state.dropTargetItemId = + callbacks.ResolveDropTargetItem( + state.draggedItemId, + event.position); + state.validDropTarget = + !state.dropTargetItemId.empty() && + callbacks.CanDropOnItem( + state.draggedItemId, + state.dropTargetItemId); + } + break; + + case UIInputEventType::PointerButtonUp: + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + if (state.dragging) { + if (state.validDropTarget) { + result.draggedItemId = state.draggedItemId; + result.dropTargetItemId = state.dropTargetItemId; + result.dropCommitted = + callbacks.CommitDropOnItem( + state.draggedItemId, + state.dropTargetItemId); + } + + state.armed = false; + state.dragging = false; + state.armedItemId.clear(); + state.draggedItemId.clear(); + state.dropTargetItemId.clear(); + state.validDropTarget = false; + state.requestPointerRelease = true; + } else { + state.armed = false; + state.armedItemId.clear(); + } + break; + + case UIInputEventType::PointerLeave: + if (state.dragging) { + state.dropTargetItemId.clear(); + state.validDropTarget = false; + } + break; + + case UIInputEventType::FocusLost: + { + const bool requestPointerRelease = state.dragging; + state = {}; + state.requestPointerRelease = requestPointerRelease; + } + break; + + default: + break; + } + } + + return result; +} + +} // namespace XCEngine::UI::Editor::App::GridItemDragDrop diff --git a/new_editor/app/Features/Shared/TreeItemDragDrop.h b/new_editor/app/Features/Shared/TreeItemDragDrop.h new file mode 100644 index 00000000..97d819d5 --- /dev/null +++ b/new_editor/app/Features/Shared/TreeItemDragDrop.h @@ -0,0 +1,324 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App::TreeItemDragDrop { + +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UIPointerButton; +using Widgets::HitTestUIEditorTreeView; +using Widgets::UIEditorTreeViewHitTarget; +using Widgets::UIEditorTreeViewHitTargetKind; +using Widgets::UIEditorTreeViewInvalidIndex; + +inline constexpr float kDefaultDragThreshold = 4.0f; + +struct State { + std::string armedItemId = {}; + std::string draggedItemId = {}; + std::string dropTargetItemId = {}; + UIPoint pressPosition = {}; + bool armed = false; + bool dragging = false; + bool dropToRoot = false; + bool validDropTarget = false; + bool requestPointerCapture = false; + bool requestPointerRelease = false; +}; + +struct ProcessResult { + bool selectionForced = false; + bool dropCommitted = false; + bool droppedToRoot = false; + std::string draggedItemId = {}; + std::string dropTargetItemId = {}; +}; + +inline void ResetTransientRequests(State& state) { + state.requestPointerCapture = false; + state.requestPointerRelease = false; +} + +inline bool HasActivePointerCapture(const State& state) { + return state.dragging; +} + +inline bool ContainsPoint(const UIRect& rect, const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +inline float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) { + const float dx = lhs.x - rhs.x; + const float dy = lhs.y - rhs.y; + return dx * dx + dy * dy; +} + +inline const Widgets::UIEditorTreeViewItem* ResolveHitItem( + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + const UIPoint& point, + UIEditorTreeViewHitTarget* hitTargetOutput = nullptr) { + const UIEditorTreeViewHitTarget hitTarget = HitTestUIEditorTreeView(layout, point); + if (hitTargetOutput != nullptr) { + *hitTargetOutput = hitTarget; + } + + if (hitTarget.itemIndex >= items.size()) { + return nullptr; + } + + return &items[hitTarget.itemIndex]; +} + +inline std::size_t FindVisibleIndexForItemId( + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + std::string_view itemId) { + for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) { + const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex]; + if (itemIndex < items.size() && items[itemIndex].itemId == itemId) { + return visibleIndex; + } + } + + return UIEditorTreeViewInvalidIndex; +} + +inline std::vector BuildInteractionInputEvents( + const State& state, + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + const std::vector& rawEvents, + float dragThreshold = kDefaultDragThreshold) { + struct PreviewState { + std::string armedItemId = {}; + UIPoint pressPosition = {}; + bool armed = false; + bool dragging = false; + }; + + PreviewState preview = {}; + preview.armed = state.armed; + preview.armedItemId = state.armedItemId; + preview.pressPosition = state.pressPosition; + preview.dragging = state.dragging; + + std::vector filteredEvents = {}; + filteredEvents.reserve(rawEvents.size()); + for (const UIInputEvent& event : rawEvents) { + bool suppress = false; + + switch (event.type) { + case UIInputEventType::PointerButtonDown: + if (event.pointerButton == UIPointerButton::Left) { + UIEditorTreeViewHitTarget hitTarget = {}; + const Widgets::UIEditorTreeViewItem* hitItem = + ResolveHitItem(layout, items, event.position, &hitTarget); + if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { + preview.armed = true; + preview.armedItemId = hitItem->itemId; + preview.pressPosition = event.position; + } else { + preview.armed = false; + preview.armedItemId.clear(); + } + } + if (preview.dragging) { + suppress = true; + } + break; + + case UIInputEventType::PointerMove: + if (preview.dragging) { + suppress = true; + break; + } + + if (preview.armed && + ComputeSquaredDistance(event.position, preview.pressPosition) >= + dragThreshold * dragThreshold) { + preview.dragging = true; + suppress = true; + } + break; + + case UIInputEventType::PointerButtonUp: + if (event.pointerButton == UIPointerButton::Left) { + if (preview.dragging) { + suppress = true; + preview.dragging = false; + } + preview.armed = false; + preview.armedItemId.clear(); + } else if (preview.dragging) { + suppress = true; + } + break; + + case UIInputEventType::PointerLeave: + if (preview.dragging) { + suppress = true; + } + break; + + case UIInputEventType::FocusLost: + preview.armed = false; + preview.dragging = false; + preview.armedItemId.clear(); + break; + + default: + if (preview.dragging && + (event.type == UIInputEventType::PointerWheel || + event.type == UIInputEventType::PointerEnter)) { + suppress = true; + } + break; + } + + if (!suppress) { + filteredEvents.push_back(event); + } + } + + return filteredEvents; +} + +template +ProcessResult ProcessInputEvents( + State& state, + const Widgets::UIEditorTreeViewLayout& layout, + const std::vector& items, + const std::vector& inputEvents, + const UIRect& bounds, + Callbacks& callbacks, + float dragThreshold = kDefaultDragThreshold) { + ProcessResult result = {}; + + for (const UIInputEvent& event : inputEvents) { + switch (event.type) { + case UIInputEventType::PointerButtonDown: + if (event.pointerButton == UIPointerButton::Left) { + UIEditorTreeViewHitTarget hitTarget = {}; + const Widgets::UIEditorTreeViewItem* hitItem = + ResolveHitItem(layout, items, event.position, &hitTarget); + if (hitItem != nullptr && hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) { + state.armed = true; + state.armedItemId = hitItem->itemId; + state.pressPosition = event.position; + } else { + state.armed = false; + state.armedItemId.clear(); + } + } + break; + + case UIInputEventType::PointerMove: + if (state.armed && + !state.dragging && + ComputeSquaredDistance(event.position, state.pressPosition) >= + dragThreshold * dragThreshold) { + state.dragging = !state.armedItemId.empty(); + state.draggedItemId = state.armedItemId; + state.dropTargetItemId.clear(); + state.dropToRoot = false; + state.validDropTarget = false; + if (state.dragging) { + state.requestPointerCapture = true; + if (!callbacks.IsItemSelected(state.draggedItemId)) { + result.selectionForced = callbacks.SelectDraggedItem(state.draggedItemId); + } + } + } + + if (state.dragging) { + UIEditorTreeViewHitTarget hitTarget = {}; + const Widgets::UIEditorTreeViewItem* hitItem = + ResolveHitItem(layout, items, event.position, &hitTarget); + + state.dropTargetItemId.clear(); + state.dropToRoot = false; + state.validDropTarget = false; + + if (hitItem != nullptr && + (hitTarget.kind == UIEditorTreeViewHitTargetKind::Row || + hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) { + state.dropTargetItemId = hitItem->itemId; + state.validDropTarget = + callbacks.CanDropOnItem(state.draggedItemId, state.dropTargetItemId); + } else if (ContainsPoint(bounds, event.position)) { + state.dropToRoot = true; + state.validDropTarget = callbacks.CanDropToRoot(state.draggedItemId); + } + } + break; + + case UIInputEventType::PointerButtonUp: + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + if (state.dragging) { + if (state.validDropTarget) { + result.draggedItemId = state.draggedItemId; + result.dropTargetItemId = state.dropTargetItemId; + result.droppedToRoot = state.dropToRoot; + result.dropCommitted = + state.dropToRoot + ? callbacks.CommitDropToRoot(state.draggedItemId) + : callbacks.CommitDropOnItem( + state.draggedItemId, + state.dropTargetItemId); + } + + state.armed = false; + state.dragging = false; + state.armedItemId.clear(); + state.draggedItemId.clear(); + state.dropTargetItemId.clear(); + state.dropToRoot = false; + state.validDropTarget = false; + state.requestPointerRelease = true; + } else { + state.armed = false; + state.armedItemId.clear(); + } + break; + + case UIInputEventType::PointerLeave: + if (state.dragging) { + state.dropTargetItemId.clear(); + state.dropToRoot = false; + state.validDropTarget = false; + } + break; + + case UIInputEventType::FocusLost: + { + const bool requestPointerRelease = state.dragging; + state = {}; + state.requestPointerRelease = requestPointerRelease; + } + break; + + default: + break; + } + } + + return result; +} + +} // namespace XCEngine::UI::Editor::App::TreeItemDragDrop diff --git a/new_editor/app/Internal/StringEncoding.h b/new_editor/app/Internal/StringEncoding.h index 7a1adfbe..b1628e66 100644 --- a/new_editor/app/Internal/StringEncoding.h +++ b/new_editor/app/Internal/StringEncoding.h @@ -7,6 +7,38 @@ namespace XCEngine::UI::Editor::App::Internal { +inline std::wstring Utf8ToWide(std::string_view text) { + if (text.empty()) { + return {}; + } + + const int requiredChars = MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + nullptr, + 0); + if (requiredChars <= 0) { + return {}; + } + + std::wstring wide(static_cast(requiredChars), L'\0'); + const int convertedChars = MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + wide.data(), + requiredChars); + if (convertedChars <= 0) { + return {}; + } + + wide.resize(static_cast(convertedChars)); + return wide; +} + inline std::string WideToUtf8(std::wstring_view text) { if (text.empty()) { return {}; diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index f11c7a90..f9cb62ad 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -134,18 +134,28 @@ private: void ClearBorderlessWindowChromeDragRestoreState(); void ClearBorderlessWindowChromeState(); bool HasInteractiveCaptureState() const; + EditorWindowPointerCaptureOwner GetPointerCaptureOwner() const; + bool OwnsPointerCapture(EditorWindowPointerCaptureOwner owner) const; + void AcquirePointerCapture(EditorWindowPointerCaptureOwner owner); + void ReleasePointerCapture(EditorWindowPointerCaptureOwner owner); + void ForceReleasePointerCapture(); + void ClearPointerCaptureOwner(); + void TryStartImmediateShellPointerCapture(LPARAM lParam); void QueuePointerEvent( ::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam); + void QueueSyntheticPointerStateSyncEvent( + const ::XCEngine::UI::UIInputModifiers& modifiers); void QueuePointerLeaveEvent(); void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam); void QueueKeyEvent(::XCEngine::UI::UIInputEventType type, WPARAM wParam, LPARAM lParam); void QueueCharacterEvent(WPARAM wParam, LPARAM lParam); void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type); void SyncInputModifiersFromSystemState(); + void SyncShellCapturedPointerButtonsFromSystemState(); void ResetInputModifiers(); void RequestManualScreenshot(); diff --git a/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp b/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp index 8890d22e..9236070c 100644 --- a/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowBorderlessResize.cpp @@ -40,7 +40,7 @@ bool EditorWindow::HandleBorderlessWindowResizeButtonDown(LPARAM lParam) { } m_chrome.runtime.BeginBorderlessResize(edge, screenPoint, windowRect); - SetCapture(m_window.hwnd); + AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize); InvalidateHostWindow(); return true; } @@ -51,9 +51,7 @@ bool EditorWindow::HandleBorderlessWindowResizeButtonUp() { } m_chrome.runtime.EndBorderlessResize(); - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } + ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize); InvalidateHostWindow(); return true; } @@ -123,9 +121,7 @@ void EditorWindow::ForceClearBorderlessWindowResizeState() { m_chrome.runtime.SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None); m_chrome.runtime.EndBorderlessResize(); - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } + ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize); InvalidateHostWindow(); } diff --git a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp b/new_editor/app/Platform/Win32/EditorWindowFrame.cpp index cf36e05b..5cb22fb9 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrame.cpp @@ -7,6 +7,9 @@ #include #include +#include +#include +#include namespace XCEngine::UI::Editor::App { @@ -14,9 +17,113 @@ using namespace EditorWindowInternal; using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIInputModifiers; using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; +namespace { + +std::string DescribeInputEventType(const UIInputEvent& event) { + switch (event.type) { + case UIInputEventType::PointerMove: return "PointerMove"; + case UIInputEventType::PointerEnter: return "PointerEnter"; + case UIInputEventType::PointerLeave: return "PointerLeave"; + case UIInputEventType::PointerButtonDown: return "PointerDown"; + case UIInputEventType::PointerButtonUp: return "PointerUp"; + case UIInputEventType::PointerWheel: return "PointerWheel"; + case UIInputEventType::KeyDown: return "KeyDown"; + case UIInputEventType::KeyUp: return "KeyUp"; + case UIInputEventType::Character: return "Character"; + case UIInputEventType::FocusGained: return "FocusGained"; + case UIInputEventType::FocusLost: return "FocusLost"; + default: return "Unknown"; + } +} + +bool HasPendingPointerStateReconciliationEvent( + const std::vector& events) { + for (const UIInputEvent& event : events) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + case UIInputEventType::FocusLost: + return true; + case UIInputEventType::PointerLeave: + case UIInputEventType::KeyDown: + case UIInputEventType::KeyUp: + case UIInputEventType::Character: + case UIInputEventType::FocusGained: + case UIInputEventType::None: + default: + break; + } + } + + return false; +} + +std::uint8_t ButtonMask(UIPointerButton button) { + switch (button) { + case UIPointerButton::Left: return 1u << 0u; + case UIPointerButton::Right: return 1u << 1u; + case UIPointerButton::Middle: return 1u << 2u; + case UIPointerButton::X1: return 1u << 3u; + case UIPointerButton::X2: return 1u << 4u; + case UIPointerButton::None: + default: + return 0u; + } +} + +std::uint8_t ButtonMaskFromModifiers(const UIInputModifiers& modifiers) { + std::uint8_t mask = 0u; + if (modifiers.leftMouse) { + mask |= ButtonMask(UIPointerButton::Left); + } + if (modifiers.rightMouse) { + mask |= ButtonMask(UIPointerButton::Right); + } + if (modifiers.middleMouse) { + mask |= ButtonMask(UIPointerButton::Middle); + } + if (modifiers.x1Mouse) { + mask |= ButtonMask(UIPointerButton::X1); + } + if (modifiers.x2Mouse) { + mask |= ButtonMask(UIPointerButton::X2); + } + return mask; +} + +std::uint8_t ResolveExpectedShellCaptureButtons( + const EditorShellRuntime& shellRuntime) { + std::uint8_t expectedButtons = 0u; + const auto& shellState = shellRuntime.GetShellInteractionState(); + const auto& dockHostState = + shellState.workspaceInteractionState.dockHostInteractionState; + if (dockHostState.splitterDragState.active || + !dockHostState.activeTabDragNodeId.empty()) { + expectedButtons |= ButtonMask(UIPointerButton::Left); + } + + for (const auto& panelState : + shellState.workspaceInteractionState.composeState.panelStates) { + const auto& inputBridgeState = panelState.viewportShellState.inputBridgeState; + if (inputBridgeState.captured) { + expectedButtons |= ButtonMask(inputBridgeState.captureButton); + } + } + + return expectedButtons; +} + +} // namespace + EditorWindowFrameTransferRequests EditorWindow::RenderFrame( EditorContext& editorContext, bool globalTabDragActive) { @@ -97,4 +204,213 @@ UIRect EditorWindow::ResolveWorkspaceBounds(float clientWidthDips, float clientH (std::max)(0.0f, clientHeightDips - titleBarHeight)); } +EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( + EditorContext& editorContext, + bool globalTabDragActive, + const UIRect& workspaceBounds, + UIDrawList& drawList) { + SyncShellCapturedPointerButtonsFromSystemState(); + std::vector frameEvents = std::move(m_input.pendingEvents); + m_input.pendingEvents.clear(); + if (!frameEvents.empty() && IsVerboseRuntimeTraceEnabled()) { + LogRuntimeTrace( + "input", + DescribeInputEvents(frameEvents) + " | " + + editorContext.DescribeWorkspaceState( + m_composition.workspaceController, + m_composition.shellRuntime.GetShellInteractionState())); + } + + const Host::D3D12WindowRenderLoopFrameContext frameContext = + m_render.windowRenderLoop.BeginFrame(); + if (!frameContext.warning.empty()) { + LogRuntimeTrace("viewport", frameContext.warning); + } + + editorContext.AttachTextMeasurer(m_render.renderer); + const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); + m_composition.shellRuntime.Update( + editorContext, + m_composition.workspaceController, + workspaceBounds, + frameEvents, + BuildCaptureStatusText(), + m_window.primary + ? EditorShellVariant::Primary + : EditorShellVariant::DetachedWindow, + useDetachedTitleBarTabStrip, + useDetachedTitleBarTabStrip ? kBorderlessTitleBarHeightDips : 0.0f); + const UIEditorShellInteractionFrame& shellFrame = + m_composition.shellRuntime.GetShellFrame(); + const UIEditorDockHostInteractionState& dockHostInteractionState = + m_composition.shellRuntime + .GetShellInteractionState() + .workspaceInteractionState + .dockHostInteractionState; + + LogFrameInteractionTrace(editorContext, frameEvents, shellFrame); + const EditorWindowFrameTransferRequests transferRequests = + BuildShellTransferRequests(globalTabDragActive, dockHostInteractionState, shellFrame); + + ApplyHostCaptureRequests(shellFrame.result); + for (const WorkspaceTraceEntry& entry : m_composition.shellRuntime.GetTraceEntries()) { + LogRuntimeTrace(entry.channel, entry.message); + } + ApplyHostedContentCaptureRequests(); + ApplyCurrentCursor(); + m_composition.shellRuntime.Append(drawList); + if (frameContext.canRenderViewports) { + m_composition.shellRuntime.RenderRequestedViewports(frameContext.renderContext); + } + return transferRequests; +} + +void EditorWindow::RenderInvalidFrame( + EditorContext& editorContext, + UIDrawList& drawList) const { + drawList.AddText( + UIPoint(28.0f, 28.0f), + "Editor shell asset invalid.", + EditorWindowInternal::kShellTextColor, + 16.0f); + drawList.AddText( + UIPoint(28.0f, 54.0f), + editorContext.GetValidationMessage().empty() + ? std::string("Unknown validation error.") + : editorContext.GetValidationMessage(), + EditorWindowInternal::kShellMutedTextColor, + 12.0f); +} + +void EditorWindow::LogFrameInteractionTrace( + EditorContext& editorContext, + const std::vector& frameEvents, + const UIEditorShellInteractionFrame& shellFrame) const { + if (!IsVerboseRuntimeTraceEnabled() || + (frameEvents.empty() && + !shellFrame.result.workspaceResult.dockHostResult.layoutChanged && + !shellFrame.result.workspaceResult.dockHostResult.commandExecuted)) { + return; + } + + std::ostringstream frameTrace = {}; + frameTrace << "result consumed=" + << (shellFrame.result.consumed ? "true" : "false") + << " layoutChanged=" + << (shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false") + << " commandExecuted=" + << (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") + << " active=" + << m_composition.workspaceController.GetWorkspace().activePanelId + << " message=" + << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; + LogRuntimeTrace("frame", frameTrace.str()); +} + +EditorWindowFrameTransferRequests EditorWindow::BuildShellTransferRequests( + bool globalTabDragActive, + const UIEditorDockHostInteractionState& dockHostInteractionState, + const UIEditorShellInteractionFrame& shellFrame) const { + EditorWindowFrameTransferRequests transferRequests = {}; + POINT screenPoint = {}; + const bool hasScreenPoint = GetCursorPos(&screenPoint) != FALSE; + + if (!globalTabDragActive && + !dockHostInteractionState.activeTabDragNodeId.empty() && + !dockHostInteractionState.activeTabDragPanelId.empty() && + hasScreenPoint) { + transferRequests.beginGlobalTabDrag = EditorWindowPanelTransferRequest{ + dockHostInteractionState.activeTabDragNodeId, + dockHostInteractionState.activeTabDragPanelId, + screenPoint, + }; + } + + if (shellFrame.result.workspaceResult.dockHostResult.detachRequested && + hasScreenPoint) { + transferRequests.detachPanel = EditorWindowPanelTransferRequest{ + shellFrame.result.workspaceResult.dockHostResult.detachedNodeId, + shellFrame.result.workspaceResult.dockHostResult.detachedPanelId, + screenPoint, + }; + } + + return transferRequests; +} + +std::string EditorWindow::BuildCaptureStatusText() const { + if (m_render.autoScreenshot.HasPendingCapture()) { + return "Shot pending..."; + } + + if (!m_render.autoScreenshot.GetLastCaptureError().empty()) { + return TruncateText(m_render.autoScreenshot.GetLastCaptureError(), 38u); + } + + if (!m_render.autoScreenshot.GetLastCaptureSummary().empty()) { + return TruncateText(m_render.autoScreenshot.GetLastCaptureSummary(), 38u); + } + + return {}; +} + +void EditorWindow::SyncShellCapturedPointerButtonsFromSystemState() { + m_input.modifierTracker.SyncFromSystemState(); + + const std::uint8_t expectedButtons = + ResolveExpectedShellCaptureButtons(m_composition.shellRuntime); + if (expectedButtons == 0u || + HasPendingPointerStateReconciliationEvent(m_input.pendingEvents)) { + return; + } + + const UIInputModifiers modifiers = + m_input.modifierTracker.GetCurrentModifiers(); + if ((ButtonMaskFromModifiers(modifiers) & expectedButtons) == expectedButtons) { + return; + } + + QueueSyntheticPointerStateSyncEvent(modifiers); +} + +void EditorWindow::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { + if (result.requestPointerCapture) { + AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); + } + if (result.releasePointerCapture) { + ReleasePointerCapture(EditorWindowPointerCaptureOwner::Shell); + } +} + +void EditorWindow::ApplyHostedContentCaptureRequests() { + if (m_composition.shellRuntime.WantsHostPointerCapture()) { + AcquirePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); + } + + if (m_composition.shellRuntime.WantsHostPointerRelease() && + !m_composition.shellRuntime.HasShellInteractiveCapture()) { + ReleasePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); + } +} + +std::string EditorWindow::DescribeInputEvents( + const std::vector& events) const { + std::ostringstream stream = {}; + stream << "events=["; + for (std::size_t index = 0; index < events.size(); ++index) { + if (index > 0u) { + stream << " | "; + } + + const UIInputEvent& event = events[index]; + stream << DescribeInputEventType(event) + << '@' + << static_cast(event.position.x) + << ',' + << static_cast(event.position.y); + } + stream << ']'; + return stream.str(); +} + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameRuntime.cpp b/new_editor/app/Platform/Win32/EditorWindowFrameRuntime.cpp deleted file mode 100644 index 78ee96b3..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowFrameRuntime.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowInputInternal.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" -#include "Platform/Win32/EditorWindowStyle.h" -#include "State/EditorContext.h" - -#include -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; -using namespace EditorWindowInputInternal; -using ::XCEngine::UI::UIDrawList; -using ::XCEngine::UI::UIInputEvent; -using ::XCEngine::UI::UIPoint; -using ::XCEngine::UI::UIRect; - -EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( - EditorContext& editorContext, - bool globalTabDragActive, - const UIRect& workspaceBounds, - UIDrawList& drawList) { - std::vector frameEvents = std::move(m_input.pendingEvents); - m_input.pendingEvents.clear(); - if (!frameEvents.empty() && IsVerboseRuntimeTraceEnabled()) { - LogRuntimeTrace( - "input", - DescribeInputEvents(frameEvents) + " | " + - editorContext.DescribeWorkspaceState( - m_composition.workspaceController, - m_composition.shellRuntime.GetShellInteractionState())); - } - - const Host::D3D12WindowRenderLoopFrameContext frameContext = - m_render.windowRenderLoop.BeginFrame(); - if (!frameContext.warning.empty()) { - LogRuntimeTrace("viewport", frameContext.warning); - } - - editorContext.AttachTextMeasurer(m_render.renderer); - const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); - m_composition.shellRuntime.Update( - editorContext, - m_composition.workspaceController, - workspaceBounds, - frameEvents, - BuildCaptureStatusText(), - m_window.primary - ? EditorShellVariant::Primary - : EditorShellVariant::DetachedWindow, - useDetachedTitleBarTabStrip, - useDetachedTitleBarTabStrip ? kBorderlessTitleBarHeightDips : 0.0f); - const UIEditorShellInteractionFrame& shellFrame = - m_composition.shellRuntime.GetShellFrame(); - const UIEditorDockHostInteractionState& dockHostInteractionState = - m_composition.shellRuntime - .GetShellInteractionState() - .workspaceInteractionState - .dockHostInteractionState; - - LogFrameInteractionTrace(editorContext, frameEvents, shellFrame); - const EditorWindowFrameTransferRequests transferRequests = - BuildShellTransferRequests(globalTabDragActive, dockHostInteractionState, shellFrame); - - ApplyHostCaptureRequests(shellFrame.result); - for (const WorkspaceTraceEntry& entry : m_composition.shellRuntime.GetTraceEntries()) { - LogRuntimeTrace(entry.channel, entry.message); - } - ApplyHostedContentCaptureRequests(); - ApplyCurrentCursor(); - m_composition.shellRuntime.Append(drawList); - if (frameContext.canRenderViewports) { - m_composition.shellRuntime.RenderRequestedViewports(frameContext.renderContext); - } - return transferRequests; -} - -void EditorWindow::RenderInvalidFrame( - EditorContext& editorContext, - UIDrawList& drawList) const { - drawList.AddText( - UIPoint(28.0f, 28.0f), - "Editor shell asset invalid.", - EditorWindowInternal::kShellTextColor, - 16.0f); - drawList.AddText( - UIPoint(28.0f, 54.0f), - editorContext.GetValidationMessage().empty() - ? std::string("Unknown validation error.") - : editorContext.GetValidationMessage(), - EditorWindowInternal::kShellMutedTextColor, - 12.0f); -} - -void EditorWindow::LogFrameInteractionTrace( - EditorContext& editorContext, - const std::vector& frameEvents, - const UIEditorShellInteractionFrame& shellFrame) const { - if (!IsVerboseRuntimeTraceEnabled() || - (frameEvents.empty() && - !shellFrame.result.workspaceResult.dockHostResult.layoutChanged && - !shellFrame.result.workspaceResult.dockHostResult.commandExecuted)) { - return; - } - - std::ostringstream frameTrace = {}; - frameTrace << "result consumed=" - << (shellFrame.result.consumed ? "true" : "false") - << " layoutChanged=" - << (shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false") - << " commandExecuted=" - << (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") - << " active=" - << m_composition.workspaceController.GetWorkspace().activePanelId - << " message=" - << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; - LogRuntimeTrace("frame", frameTrace.str()); -} - -EditorWindowFrameTransferRequests EditorWindow::BuildShellTransferRequests( - bool globalTabDragActive, - const UIEditorDockHostInteractionState& dockHostInteractionState, - const UIEditorShellInteractionFrame& shellFrame) const { - EditorWindowFrameTransferRequests transferRequests = {}; - POINT screenPoint = {}; - const bool hasScreenPoint = GetCursorPos(&screenPoint) != FALSE; - - if (!globalTabDragActive && - !dockHostInteractionState.activeTabDragNodeId.empty() && - !dockHostInteractionState.activeTabDragPanelId.empty() && - hasScreenPoint) { - transferRequests.beginGlobalTabDrag = EditorWindowPanelTransferRequest{ - dockHostInteractionState.activeTabDragNodeId, - dockHostInteractionState.activeTabDragPanelId, - screenPoint, - }; - } - - if (shellFrame.result.workspaceResult.dockHostResult.detachRequested && - hasScreenPoint) { - transferRequests.detachPanel = EditorWindowPanelTransferRequest{ - shellFrame.result.workspaceResult.dockHostResult.detachedNodeId, - shellFrame.result.workspaceResult.dockHostResult.detachedPanelId, - screenPoint, - }; - } - - return transferRequests; -} - -std::string EditorWindow::BuildCaptureStatusText() const { - if (m_render.autoScreenshot.HasPendingCapture()) { - return "Shot pending..."; - } - - if (!m_render.autoScreenshot.GetLastCaptureError().empty()) { - return TruncateText(m_render.autoScreenshot.GetLastCaptureError(), 38u); - } - - if (!m_render.autoScreenshot.GetLastCaptureSummary().empty()) { - return TruncateText(m_render.autoScreenshot.GetLastCaptureSummary(), 38u); - } - - return {}; -} - -void EditorWindow::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { - if (result.requestPointerCapture && GetCapture() != m_window.hwnd) { - SetCapture(m_window.hwnd); - } - if (result.releasePointerCapture && GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } -} - -void EditorWindow::ApplyHostedContentCaptureRequests() { - if (m_composition.shellRuntime.WantsHostPointerCapture() && - GetCapture() != m_window.hwnd) { - SetCapture(m_window.hwnd); - } - - if (m_composition.shellRuntime.WantsHostPointerRelease() && - GetCapture() == m_window.hwnd && - !m_composition.shellRuntime.HasShellInteractiveCapture()) { - ReleaseCapture(); - } -} - -std::string EditorWindow::DescribeInputEvents( - const std::vector& events) const { - std::ostringstream stream = {}; - stream << "events=["; - for (std::size_t index = 0; index < events.size(); ++index) { - if (index > 0u) { - stream << " | "; - } - - const UIInputEvent& event = events[index]; - stream << DescribeInputEventType(event) - << '@' - << static_cast(event.position.x) - << ',' - << static_cast(event.position.y); - } - stream << ']'; - return stream.str(); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowInitialization.cpp b/new_editor/app/Platform/Win32/EditorWindowInitialization.cpp deleted file mode 100644 index 71fcbb8a..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowInitialization.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" - -#include "Bootstrap/EditorResources.h" -#include "Platform/Win32/EditorWindowPlatformInternal.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" -#include "State/EditorContext.h" - -#include -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; - -bool EditorWindow::Initialize( - const std::filesystem::path& repoRoot, - EditorContext& editorContext, - const std::filesystem::path& captureRoot, - bool autoCaptureOnStartup) { - if (m_window.hwnd == nullptr) { - LogRuntimeTrace("app", "window initialize skipped: hwnd is null"); - return false; - } - - Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); - m_chrome.runtime.Reset(); - m_chrome.runtime.SetWindowDpi(QueryWindowDpi(m_window.hwnd)); - m_render.renderer.SetDpiScale(GetDpiScale()); - - std::ostringstream dpiTrace = {}; - dpiTrace << "initial dpi=" << m_chrome.runtime.GetWindowDpi() - << " scale=" << GetDpiScale(); - LogRuntimeTrace("window", dpiTrace.str()); - - if (!m_render.renderer.Initialize(m_window.hwnd)) { - LogRuntimeTrace("app", "renderer initialization failed"); - return false; - } - - RECT clientRect = {}; - GetClientRect(m_window.hwnd, &clientRect); - const int clientWidth = (std::max)(clientRect.right - clientRect.left, 1L); - const int clientHeight = (std::max)(clientRect.bottom - clientRect.top, 1L); - if (!m_render.windowRenderer.Initialize(m_window.hwnd, clientWidth, clientHeight)) { - LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); - m_render.renderer.Shutdown(); - return false; - } - - const Host::D3D12WindowRenderLoopAttachResult attachResult = - m_render.windowRenderLoop.Attach(m_render.renderer, m_render.windowRenderer); - if (!attachResult.interopWarning.empty()) { - LogRuntimeTrace("app", attachResult.interopWarning); - } - - editorContext.AttachTextMeasurer(m_render.renderer); - m_composition.shellRuntime.Initialize(repoRoot, m_render.renderer); - m_composition.shellRuntime.AttachViewportWindowRenderer(m_render.windowRenderer); - m_composition.shellRuntime.SetViewportSurfacePresentationEnabled( - attachResult.hasViewportSurfacePresentation); - - std::string titleBarLogoError = {}; - if (!LoadEmbeddedPngTexture( - m_render.renderer, - IDR_PNG_LOGO_ICON, - m_render.titleBarLogoIcon, - titleBarLogoError)) { - LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); - } - if (!m_composition.shellRuntime.GetBuiltInIconError().empty()) { - LogRuntimeTrace("icons", m_composition.shellRuntime.GetBuiltInIconError()); - } - - LogRuntimeTrace( - "app", - "shell runtime initialized: " + - editorContext.DescribeWorkspaceState( - m_composition.workspaceController, - m_composition.shellRuntime.GetShellInteractionState())); - m_render.ready = true; - - m_render.autoScreenshot.Initialize(captureRoot); - if (autoCaptureOnStartup && IsAutoCaptureOnStartupEnabled()) { - m_render.autoScreenshot.RequestCapture("startup"); - editorContext.SetStatus("Capture", "Startup capture requested."); - } - - return true; -} - -void EditorWindow::Shutdown() { - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } - - m_render.ready = false; - m_render.autoScreenshot.Shutdown(); - m_composition.shellRuntime.Shutdown(); - m_render.renderer.ReleaseTexture(m_render.titleBarLogoIcon); - m_render.windowRenderLoop.Detach(); - m_render.windowRenderer.Shutdown(); - m_render.renderer.Shutdown(); - m_input.pendingEvents.clear(); - m_chrome.chromeState = {}; - m_chrome.runtime.Reset(); -} - -void EditorWindow::ResetInteractionState() { - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } - - m_input.pendingEvents.clear(); - m_input.trackingMouseLeave = false; - m_input.modifierTracker.Reset(); - m_composition.shellRuntime.ResetInteractionState(); - m_chrome.chromeState = {}; - m_chrome.runtime.EndBorderlessResize(); - m_chrome.runtime.EndBorderlessWindowDragRestore(); - m_chrome.runtime.EndInteractiveResize(); - m_chrome.runtime.SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None); - m_chrome.runtime.ClearPredictedClientPixelSize(); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowInput.cpp b/new_editor/app/Platform/Win32/EditorWindowInput.cpp index 5c9f48ad..7cae9f70 100644 --- a/new_editor/app/Platform/Win32/EditorWindowInput.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowInput.cpp @@ -1,13 +1,13 @@ #include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowInputInternal.h" +#include #include +#include #include namespace XCEngine::UI::Editor::App { -using namespace EditorWindowInputInternal; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; @@ -33,6 +33,83 @@ bool IsScreenPointOverWindow(HWND hwnd, const POINT& screenPoint) { screenPoint.y >= windowRect.top && screenPoint.y < windowRect.bottom; } +std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { + using ::XCEngine::Input::KeyCode; + + switch (wParam) { + case 'A': return static_cast(KeyCode::A); + case 'B': return static_cast(KeyCode::B); + case 'C': return static_cast(KeyCode::C); + case 'D': return static_cast(KeyCode::D); + case 'E': return static_cast(KeyCode::E); + case 'F': return static_cast(KeyCode::F); + case 'G': return static_cast(KeyCode::G); + case 'H': return static_cast(KeyCode::H); + case 'I': return static_cast(KeyCode::I); + case 'J': return static_cast(KeyCode::J); + case 'K': return static_cast(KeyCode::K); + case 'L': return static_cast(KeyCode::L); + case 'M': return static_cast(KeyCode::M); + case 'N': return static_cast(KeyCode::N); + case 'O': return static_cast(KeyCode::O); + case 'P': return static_cast(KeyCode::P); + case 'Q': return static_cast(KeyCode::Q); + case 'R': return static_cast(KeyCode::R); + case 'S': return static_cast(KeyCode::S); + case 'T': return static_cast(KeyCode::T); + case 'U': return static_cast(KeyCode::U); + case 'V': return static_cast(KeyCode::V); + case 'W': return static_cast(KeyCode::W); + case 'X': return static_cast(KeyCode::X); + case 'Y': return static_cast(KeyCode::Y); + case 'Z': return static_cast(KeyCode::Z); + case '0': return static_cast(KeyCode::Zero); + case '1': return static_cast(KeyCode::One); + case '2': return static_cast(KeyCode::Two); + case '3': return static_cast(KeyCode::Three); + case '4': return static_cast(KeyCode::Four); + case '5': return static_cast(KeyCode::Five); + case '6': return static_cast(KeyCode::Six); + case '7': return static_cast(KeyCode::Seven); + case '8': return static_cast(KeyCode::Eight); + case '9': return static_cast(KeyCode::Nine); + case VK_SPACE: return static_cast(KeyCode::Space); + case VK_TAB: return static_cast(KeyCode::Tab); + case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_ESCAPE: return static_cast(KeyCode::Escape); + case VK_SHIFT: return static_cast(KeyCode::LeftShift); + case VK_CONTROL: return static_cast(KeyCode::LeftCtrl); + case VK_MENU: return static_cast(KeyCode::LeftAlt); + case VK_UP: return static_cast(KeyCode::Up); + case VK_DOWN: return static_cast(KeyCode::Down); + case VK_LEFT: return static_cast(KeyCode::Left); + case VK_RIGHT: return static_cast(KeyCode::Right); + case VK_HOME: return static_cast(KeyCode::Home); + case VK_END: return static_cast(KeyCode::End); + case VK_PRIOR: return static_cast(KeyCode::PageUp); + case VK_NEXT: return static_cast(KeyCode::PageDown); + case VK_DELETE: return static_cast(KeyCode::Delete); + case VK_BACK: return static_cast(KeyCode::Backspace); + case VK_F1: return static_cast(KeyCode::F1); + case VK_F2: return static_cast(KeyCode::F2); + case VK_F3: return static_cast(KeyCode::F3); + case VK_F4: return static_cast(KeyCode::F4); + case VK_F5: return static_cast(KeyCode::F5); + case VK_F6: return static_cast(KeyCode::F6); + case VK_F7: return static_cast(KeyCode::F7); + case VK_F8: return static_cast(KeyCode::F8); + case VK_F9: return static_cast(KeyCode::F9); + case VK_F10: return static_cast(KeyCode::F10); + case VK_F11: return static_cast(KeyCode::F11); + case VK_F12: return static_cast(KeyCode::F12); + default: return static_cast(KeyCode::None); + } +} + +bool IsRepeatKeyMessage(LPARAM lParam) { + return (static_cast(lParam) & (1ul << 30)) != 0ul; +} + } // namespace bool EditorWindow::ApplyCurrentCursor() const { @@ -54,7 +131,69 @@ bool EditorWindow::HasInteractiveCaptureState() const { return m_composition.shellRuntime.HasInteractiveCapture() || m_chrome.runtime.IsBorderlessWindowDragRestoreArmed() || m_chrome.runtime.IsBorderlessResizeActive() || - GetCapture() == m_window.hwnd; + m_input.pointerCaptureOwner != EditorWindowPointerCaptureOwner::None; +} + +EditorWindowPointerCaptureOwner EditorWindow::GetPointerCaptureOwner() const { + return m_input.pointerCaptureOwner; +} + +bool EditorWindow::OwnsPointerCapture(EditorWindowPointerCaptureOwner owner) const { + return m_input.pointerCaptureOwner == owner; +} + +void EditorWindow::AcquirePointerCapture(EditorWindowPointerCaptureOwner owner) { + if (owner == EditorWindowPointerCaptureOwner::None || + m_window.hwnd == nullptr || + !IsWindow(m_window.hwnd)) { + return; + } + + m_input.pointerCaptureOwner = owner; + if (GetCapture() != m_window.hwnd) { + SetCapture(m_window.hwnd); + } +} + +void EditorWindow::ReleasePointerCapture(EditorWindowPointerCaptureOwner owner) { + if (m_input.pointerCaptureOwner != owner) { + return; + } + + m_input.pointerCaptureOwner = EditorWindowPointerCaptureOwner::None; + if (m_window.hwnd != nullptr && GetCapture() == m_window.hwnd) { + ReleaseCapture(); + } +} + +void EditorWindow::ForceReleasePointerCapture() { + m_input.pointerCaptureOwner = EditorWindowPointerCaptureOwner::None; + if (m_window.hwnd != nullptr && GetCapture() == m_window.hwnd) { + ReleaseCapture(); + } +} + +void EditorWindow::ClearPointerCaptureOwner() { + m_input.pointerCaptureOwner = EditorWindowPointerCaptureOwner::None; +} + +void EditorWindow::TryStartImmediateShellPointerCapture(LPARAM lParam) { + if (m_window.hwnd == nullptr || + !IsWindow(m_window.hwnd) || + GetCapture() == m_window.hwnd) { + return; + } + + const ::XCEngine::UI::UIPoint clientPoint = ConvertClientPixelsToDips( + GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + if (!ShouldStartImmediateUIEditorShellPointerCapture( + m_composition.shellRuntime.GetShellFrame(), + clientPoint)) { + return; + } + + AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); } void EditorWindow::QueuePointerEvent( @@ -68,8 +207,31 @@ void EditorWindow::QueuePointerEvent( event.position = ConvertClientPixelsToDips( GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); - event.modifiers = - m_input.modifierTracker.BuildPointerModifiers(static_cast(wParam)); + event.modifiers = m_input.modifierTracker.ApplyPointerMessage( + type, + button, + static_cast(wParam)); + m_input.pendingEvents.push_back(event); +} + +void EditorWindow::QueueSyntheticPointerStateSyncEvent( + const ::XCEngine::UI::UIInputModifiers& modifiers) { + if (m_window.hwnd == nullptr || !IsWindow(m_window.hwnd)) { + return; + } + + POINT screenPoint = {}; + if (!GetCursorPos(&screenPoint)) { + return; + } + if (!ScreenToClient(m_window.hwnd, &screenPoint)) { + return; + } + + UIInputEvent event = {}; + event.type = UIInputEventType::PointerMove; + event.position = ConvertClientPixelsToDips(screenPoint.x, screenPoint.y); + event.modifiers = modifiers; m_input.pendingEvents.push_back(event); } @@ -82,6 +244,7 @@ void EditorWindow::QueuePointerLeaveEvent() { ScreenToClient(m_window.hwnd, &clientPoint); event.position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y); } + event.modifiers = m_input.modifierTracker.GetCurrentModifiers(); m_input.pendingEvents.push_back(event); } @@ -100,8 +263,10 @@ void EditorWindow::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARA event.type = UIInputEventType::PointerWheel; event.position = ConvertClientPixelsToDips(screenPoint.x, screenPoint.y); event.wheelDelta = static_cast(wheelDelta); - event.modifiers = - m_input.modifierTracker.BuildPointerModifiers(static_cast(wParam)); + event.modifiers = m_input.modifierTracker.ApplyPointerMessage( + UIInputEventType::PointerWheel, + UIPointerButton::None, + static_cast(wParam)); m_input.pendingEvents.push_back(event); } diff --git a/new_editor/app/Platform/Win32/EditorWindowInputInternal.cpp b/new_editor/app/Platform/Win32/EditorWindowInputInternal.cpp deleted file mode 100644 index a6a7038b..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowInputInternal.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "Platform/Win32/EditorWindowInputInternal.h" - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInputInternal { - -std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { - using ::XCEngine::Input::KeyCode; - - switch (wParam) { - case 'A': return static_cast(KeyCode::A); - case 'B': return static_cast(KeyCode::B); - case 'C': return static_cast(KeyCode::C); - case 'D': return static_cast(KeyCode::D); - case 'E': return static_cast(KeyCode::E); - case 'F': return static_cast(KeyCode::F); - case 'G': return static_cast(KeyCode::G); - case 'H': return static_cast(KeyCode::H); - case 'I': return static_cast(KeyCode::I); - case 'J': return static_cast(KeyCode::J); - case 'K': return static_cast(KeyCode::K); - case 'L': return static_cast(KeyCode::L); - case 'M': return static_cast(KeyCode::M); - case 'N': return static_cast(KeyCode::N); - case 'O': return static_cast(KeyCode::O); - case 'P': return static_cast(KeyCode::P); - case 'Q': return static_cast(KeyCode::Q); - case 'R': return static_cast(KeyCode::R); - case 'S': return static_cast(KeyCode::S); - case 'T': return static_cast(KeyCode::T); - case 'U': return static_cast(KeyCode::U); - case 'V': return static_cast(KeyCode::V); - case 'W': return static_cast(KeyCode::W); - case 'X': return static_cast(KeyCode::X); - case 'Y': return static_cast(KeyCode::Y); - case 'Z': return static_cast(KeyCode::Z); - case '0': return static_cast(KeyCode::Zero); - case '1': return static_cast(KeyCode::One); - case '2': return static_cast(KeyCode::Two); - case '3': return static_cast(KeyCode::Three); - case '4': return static_cast(KeyCode::Four); - case '5': return static_cast(KeyCode::Five); - case '6': return static_cast(KeyCode::Six); - case '7': return static_cast(KeyCode::Seven); - case '8': return static_cast(KeyCode::Eight); - case '9': return static_cast(KeyCode::Nine); - case VK_SPACE: return static_cast(KeyCode::Space); - case VK_TAB: return static_cast(KeyCode::Tab); - case VK_RETURN: return static_cast(KeyCode::Enter); - case VK_ESCAPE: return static_cast(KeyCode::Escape); - case VK_SHIFT: return static_cast(KeyCode::LeftShift); - case VK_CONTROL: return static_cast(KeyCode::LeftCtrl); - case VK_MENU: return static_cast(KeyCode::LeftAlt); - case VK_UP: return static_cast(KeyCode::Up); - case VK_DOWN: return static_cast(KeyCode::Down); - case VK_LEFT: return static_cast(KeyCode::Left); - case VK_RIGHT: return static_cast(KeyCode::Right); - case VK_HOME: return static_cast(KeyCode::Home); - case VK_END: return static_cast(KeyCode::End); - case VK_PRIOR: return static_cast(KeyCode::PageUp); - case VK_NEXT: return static_cast(KeyCode::PageDown); - case VK_DELETE: return static_cast(KeyCode::Delete); - case VK_BACK: return static_cast(KeyCode::Backspace); - case VK_F1: return static_cast(KeyCode::F1); - case VK_F2: return static_cast(KeyCode::F2); - case VK_F3: return static_cast(KeyCode::F3); - case VK_F4: return static_cast(KeyCode::F4); - case VK_F5: return static_cast(KeyCode::F5); - case VK_F6: return static_cast(KeyCode::F6); - case VK_F7: return static_cast(KeyCode::F7); - case VK_F8: return static_cast(KeyCode::F8); - case VK_F9: return static_cast(KeyCode::F9); - case VK_F10: return static_cast(KeyCode::F10); - case VK_F11: return static_cast(KeyCode::F11); - case VK_F12: return static_cast(KeyCode::F12); - default: return static_cast(KeyCode::None); - } -} - -bool IsRepeatKeyMessage(LPARAM lParam) { - return (static_cast(lParam) & (1ul << 30)) != 0ul; -} - -std::string DescribeInputEventType(const ::XCEngine::UI::UIInputEvent& event) { - using ::XCEngine::UI::UIInputEventType; - - switch (event.type) { - case UIInputEventType::PointerMove: return "PointerMove"; - case UIInputEventType::PointerEnter: return "PointerEnter"; - case UIInputEventType::PointerLeave: return "PointerLeave"; - case UIInputEventType::PointerButtonDown: return "PointerDown"; - case UIInputEventType::PointerButtonUp: return "PointerUp"; - case UIInputEventType::PointerWheel: return "PointerWheel"; - case UIInputEventType::KeyDown: return "KeyDown"; - case UIInputEventType::KeyUp: return "KeyUp"; - case UIInputEventType::Character: return "Character"; - case UIInputEventType::FocusGained: return "FocusGained"; - case UIInputEventType::FocusLost: return "FocusLost"; - default: return "Unknown"; - } -} - -} // namespace XCEngine::UI::Editor::App::EditorWindowInputInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowInputInternal.h b/new_editor/app/Platform/Win32/EditorWindowInputInternal.h deleted file mode 100644 index 68475a2a..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowInputInternal.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInputInternal { - -std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam); -bool IsRepeatKeyMessage(LPARAM lParam); -std::string DescribeInputEventType(const ::XCEngine::UI::UIInputEvent& event); - -} // namespace XCEngine::UI::Editor::App::EditorWindowInputInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp index c95c2efc..7f12f519 100644 --- a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp @@ -269,9 +269,7 @@ bool EditorWindow::Initialize( } void EditorWindow::Shutdown() { - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } + ForceReleasePointerCapture(); m_render.ready = false; m_render.autoScreenshot.Shutdown(); @@ -286,9 +284,7 @@ void EditorWindow::Shutdown() { } void EditorWindow::ResetInteractionState() { - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } + ForceReleasePointerCapture(); m_input.pendingEvents.clear(); m_input.trackingMouseLeave = false; diff --git a/new_editor/app/Platform/Win32/EditorWindowMetrics.cpp b/new_editor/app/Platform/Win32/EditorWindowMetrics.cpp deleted file mode 100644 index cab12a11..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowMetrics.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" - -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; -using ::XCEngine::UI::UIPoint; - -bool EditorWindow::ApplyWindowResize(UINT width, UINT height) { - if (!m_render.ready || width == 0u || height == 0u) { - return false; - } - - const Host::D3D12WindowRenderLoopResizeResult resizeResult = - m_render.windowRenderLoop.ApplyResize(width, height); - m_composition.shellRuntime.SetViewportSurfacePresentationEnabled( - resizeResult.hasViewportSurfacePresentation); - - if (!resizeResult.windowRendererWarning.empty()) { - LogRuntimeTrace("present", resizeResult.windowRendererWarning); - } - - if (!resizeResult.interopWarning.empty()) { - LogRuntimeTrace("present", resizeResult.interopWarning); - } - - return resizeResult.hasViewportSurfacePresentation; -} - -bool EditorWindow::QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const { - outWidth = 0u; - outHeight = 0u; - if (m_window.hwnd == nullptr || !IsWindow(m_window.hwnd)) { - return false; - } - - RECT clientRect = {}; - if (!GetClientRect(m_window.hwnd, &clientRect)) { - return false; - } - - const LONG width = clientRect.right - clientRect.left; - const LONG height = clientRect.bottom - clientRect.top; - if (width <= 0 || height <= 0) { - return false; - } - - outWidth = static_cast(width); - outHeight = static_cast(height); - return true; -} - -bool EditorWindow::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const { - if (m_chrome.runtime.TryGetPredictedClientPixelSize(outWidth, outHeight)) { - return true; - } - - return QueryCurrentClientPixelSize(outWidth, outHeight); -} - -float EditorWindow::GetDpiScale() const { - return m_chrome.runtime.GetDpiScale(kBaseDpiScale); -} - -float EditorWindow::PixelsToDips(float pixels) const { - const float dpiScale = GetDpiScale(); - return dpiScale > 0.0f ? pixels / dpiScale : pixels; -} - -UIPoint EditorWindow::ConvertClientPixelsToDips(LONG x, LONG y) const { - return UIPoint( - PixelsToDips(static_cast(x)), - PixelsToDips(static_cast(y))); -} - -UIPoint EditorWindow::ConvertScreenPixelsToClientDips(const POINT& screenPoint) const { - POINT clientPoint = screenPoint; - if (m_window.hwnd != nullptr) { - ScreenToClient(m_window.hwnd, &clientPoint); - } - - const float dpiScale = m_chrome.runtime.GetDpiScale(kBaseDpiScale); - return UIPoint( - dpiScale > 0.0f - ? static_cast(clientPoint.x) / dpiScale - : static_cast(clientPoint.x), - dpiScale > 0.0f - ? static_cast(clientPoint.y) / dpiScale - : static_cast(clientPoint.y)); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.cpp b/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.cpp deleted file mode 100644 index f3bf6307..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowPlatformInternal.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "Platform/Win32/EditorWindowPlatformInternal.h" - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -UINT QuerySystemDpi() { - HDC screenDc = GetDC(nullptr); - if (screenDc == nullptr) { - return kDefaultDpi; - } - - const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX); - ReleaseDC(nullptr, screenDc); - return dpiX > 0 ? static_cast(dpiX) : kDefaultDpi; -} - -UINT QueryWindowDpi(HWND hwnd) { - if (hwnd != nullptr) { - const HMODULE user32 = GetModuleHandleW(L"user32.dll"); - if (user32 != nullptr) { - using GetDpiForWindowFn = UINT(WINAPI*)(HWND); - const auto getDpiForWindow = - reinterpret_cast(GetProcAddress(user32, "GetDpiForWindow")); - if (getDpiForWindow != nullptr) { - const UINT dpi = getDpiForWindow(hwnd); - if (dpi != 0u) { - return dpi; - } - } - } - } - - return QuerySystemDpi(); -} - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowPointerCapture.h b/new_editor/app/Platform/Win32/EditorWindowPointerCapture.h new file mode 100644 index 00000000..eb12a4d2 --- /dev/null +++ b/new_editor/app/Platform/Win32/EditorWindowPointerCapture.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace XCEngine::UI::Editor::App { + +enum class EditorWindowPointerCaptureOwner : std::uint8_t { + None = 0, + Shell, + HostedContent, + BorderlessResize, + BorderlessChrome, + GlobalTabDrag, +}; + +constexpr bool CanRouteEditorWindowGlobalTabDragPointerMessages( + EditorWindowPointerCaptureOwner owner, + bool ownsActiveGlobalTabDrag) { + return ownsActiveGlobalTabDrag && + owner == EditorWindowPointerCaptureOwner::GlobalTabDrag; +} + +constexpr bool CanRouteEditorWindowBorderlessResizePointerMessages( + EditorWindowPointerCaptureOwner owner) { + return owner == EditorWindowPointerCaptureOwner::BorderlessResize; +} + +constexpr bool CanRouteEditorWindowBorderlessChromePointerMessages( + EditorWindowPointerCaptureOwner owner) { + return owner == EditorWindowPointerCaptureOwner::BorderlessChrome; +} + +constexpr bool CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner owner, + bool shellInteractiveCaptureActive, + bool hostedContentCaptureActive) { + return owner == EditorWindowPointerCaptureOwner::None && + !shellInteractiveCaptureActive && + !hostedContentCaptureActive; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowResizeLifecycle.cpp b/new_editor/app/Platform/Win32/EditorWindowResizeLifecycle.cpp deleted file mode 100644 index 29ac17b9..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowResizeLifecycle.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" -#include "Platform/Win32/EditorWindowRuntimeInternal.h" - -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; - -void EditorWindow::OnResize(UINT width, UINT height) { - bool matchesPredictedClientSize = false; - UINT predictedWidth = 0u; - UINT predictedHeight = 0u; - if (m_chrome.runtime.TryGetPredictedClientPixelSize(predictedWidth, predictedHeight)) { - matchesPredictedClientSize = - predictedWidth == width && - predictedHeight == height; - } - - m_chrome.runtime.ClearPredictedClientPixelSize(); - if (IsBorderlessWindowEnabled() && m_window.hwnd != nullptr) { - Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); - } - - if (!matchesPredictedClientSize) { - ApplyWindowResize(width, height); - } -} - -void EditorWindow::OnEnterSizeMove() { - m_chrome.runtime.BeginInteractiveResize(); -} - -void EditorWindow::OnExitSizeMove() { - m_chrome.runtime.EndInteractiveResize(); - m_chrome.runtime.ClearPredictedClientPixelSize(); - UINT width = 0u; - UINT height = 0u; - if (QueryCurrentClientPixelSize(width, height)) { - ApplyWindowResize(width, height); - } -} - -void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { - m_chrome.runtime.SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); - m_render.renderer.SetDpiScale(GetDpiScale()); - if (m_window.hwnd != nullptr) { - const LONG windowWidth = suggestedRect.right - suggestedRect.left; - const LONG windowHeight = suggestedRect.bottom - suggestedRect.top; - SetWindowPos( - m_window.hwnd, - nullptr, - suggestedRect.left, - suggestedRect.top, - windowWidth, - windowHeight, - SWP_NOZORDER | SWP_NOACTIVATE); - UINT clientWidth = 0u; - UINT clientHeight = 0u; - if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { - ApplyWindowResize(clientWidth, clientHeight); - } - Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); - } - - std::ostringstream trace = {}; - trace << "dpi changed to " << m_chrome.runtime.GetWindowDpi() - << " scale=" << GetDpiScale(); - LogRuntimeTrace("window", trace.str()); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.cpp b/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.cpp deleted file mode 100644 index 0e4b4a3e..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeInternal.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "Platform/Win32/EditorWindowRuntimeInternal.h" - -#include - -namespace XCEngine::UI::Editor::App::EditorWindowInternal { - -bool ResolveVerboseRuntimeTraceEnabled() { - wchar_t buffer[8] = {}; - const DWORD length = GetEnvironmentVariableW( - L"XCUIEDITOR_VERBOSE_TRACE", - buffer, - static_cast(std::size(buffer))); - return length > 0u && buffer[0] != L'0'; -} - -void LogRuntimeTrace(std::string_view channel, std::string_view message) { - AppendUIEditorRuntimeTrace(channel, message); -} - -bool IsAutoCaptureOnStartupEnabled() { - return App::Internal::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); -} - -} // namespace XCEngine::UI::Editor::App::EditorWindowInternal diff --git a/new_editor/app/Platform/Win32/EditorWindowState.h b/new_editor/app/Platform/Win32/EditorWindowState.h index 191115d1..8e21b6f5 100644 --- a/new_editor/app/Platform/Win32/EditorWindowState.h +++ b/new_editor/app/Platform/Win32/EditorWindowState.h @@ -4,6 +4,7 @@ #define NOMINMAX #endif +#include "Platform/Win32/EditorWindowPointerCapture.h" #include "Composition/EditorShellRuntime.h" #include @@ -48,6 +49,8 @@ struct EditorWindowInputState { Host::InputModifierTracker modifierTracker = {}; std::vector<::XCEngine::UI::UIInputEvent> pendingEvents = {}; bool trackingMouseLeave = false; + EditorWindowPointerCaptureOwner pointerCaptureOwner = + EditorWindowPointerCaptureOwner::None; }; struct EditorWindowCompositionState { diff --git a/new_editor/app/Platform/Win32/EditorWindowTitleBarDragRestore.cpp b/new_editor/app/Platform/Win32/EditorWindowTitleBarDragRestore.cpp deleted file mode 100644 index 2cce7756..00000000 --- a/new_editor/app/Platform/Win32/EditorWindowTitleBarDragRestore.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "Platform/Win32/EditorWindow.h" -#include "Platform/Win32/EditorWindowConstants.h" - -#include - -namespace XCEngine::UI::Editor::App { - -using namespace EditorWindowInternal; - -bool EditorWindow::HandleBorderlessWindowChromeDragRestorePointerMove( - EditorContext& editorContext, - bool globalTabDragActive) { - if (!m_chrome.runtime.IsBorderlessWindowDragRestoreArmed() || m_window.hwnd == nullptr) { - return false; - } - - POINT currentScreenPoint = {}; - if (!GetCursorPos(¤tScreenPoint)) { - return true; - } - - const POINT initialScreenPoint = - m_chrome.runtime.GetBorderlessWindowDragRestoreInitialScreenPoint(); - const int dragThresholdX = (std::max)(GetSystemMetrics(SM_CXDRAG), 1); - const int dragThresholdY = (std::max)(GetSystemMetrics(SM_CYDRAG), 1); - const LONG deltaX = currentScreenPoint.x - initialScreenPoint.x; - const LONG deltaY = currentScreenPoint.y - initialScreenPoint.y; - if (std::abs(deltaX) < dragThresholdX && - std::abs(deltaY) < dragThresholdY) { - return true; - } - - RECT restoreRect = {}; - RECT currentRect = {}; - RECT workAreaRect = {}; - if (!m_chrome.runtime.TryGetBorderlessWindowRestoreRect(restoreRect) || - !QueryCurrentWindowRect(currentRect) || - !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { - ClearBorderlessWindowChromeDragRestoreState(); - return true; - } - - const int restoreWidth = restoreRect.right - restoreRect.left; - const int restoreHeight = restoreRect.bottom - restoreRect.top; - const int currentWidth = currentRect.right - currentRect.left; - if (restoreWidth <= 0 || restoreHeight <= 0 || currentWidth <= 0) { - ClearBorderlessWindowChromeDragRestoreState(); - return true; - } - - const float pointerRatio = - static_cast(currentScreenPoint.x - currentRect.left) / - static_cast(currentWidth); - const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f); - const int newLeft = - (std::clamp)( - currentScreenPoint.x - - static_cast(clampedPointerRatio * static_cast(restoreWidth)), - workAreaRect.left, - workAreaRect.right - restoreWidth); - const int titleBarHeightPixels = - static_cast(kBorderlessTitleBarHeightDips * GetDpiScale()); - const int newTop = - (std::clamp)( - currentScreenPoint.y - (std::max)(titleBarHeightPixels / 2, 1), - workAreaRect.top, - workAreaRect.bottom - restoreHeight); - const RECT targetRect = { - newLeft, - newTop, - newLeft + restoreWidth, - newTop + restoreHeight - }; - - m_chrome.runtime.SetBorderlessWindowMaximized(false); - ApplyPredictedWindowRectTransition( - editorContext, - globalTabDragActive, - targetRect); - ClearBorderlessWindowChromeDragRestoreState(); - ReleaseCapture(); - SendMessageW(m_window.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); - return true; -} - -void EditorWindow::ClearBorderlessWindowChromeDragRestoreState() { - if (!m_chrome.runtime.IsBorderlessWindowDragRestoreArmed()) { - return; - } - - m_chrome.runtime.EndBorderlessWindowDragRestore(); - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp b/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp index 897d6350..8685de4b 100644 --- a/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowTitleBarInteraction.cpp @@ -1,6 +1,8 @@ #include "Platform/Win32/EditorWindow.h" #include "Platform/Win32/EditorWindowConstants.h" +#include + #include namespace XCEngine::UI::Editor::App { @@ -49,9 +51,7 @@ bool EditorWindow::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton: case Host::BorderlessWindowChromeHitTarget::CloseButton: m_chrome.chromeState.pressedTarget = hitTarget; - if (m_window.hwnd != nullptr) { - SetCapture(m_window.hwnd); - } + AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome); InvalidateHostWindow(); return true; case Host::BorderlessWindowChromeHitTarget::DragRegion: @@ -60,12 +60,12 @@ bool EditorWindow::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_chrome.runtime.BeginBorderlessWindowDragRestore(screenPoint); - SetCapture(m_window.hwnd); + AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome); return true; } } - ReleaseCapture(); + ForceReleasePointerCapture(); SendMessageW(m_window.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); } return true; @@ -96,9 +96,7 @@ bool EditorWindow::HandleBorderlessWindowChromeButtonUp( HitTestBorderlessWindowChrome(lParam); m_chrome.chromeState.pressedTarget = Host::BorderlessWindowChromeHitTarget::None; - if (GetCapture() == m_window.hwnd) { - ReleaseCapture(); - } + ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome); InvalidateHostWindow(); if (pressedTarget == releasedTarget) { @@ -130,6 +128,90 @@ bool EditorWindow::HandleBorderlessWindowChromeDoubleClick( return true; } +bool EditorWindow::HandleBorderlessWindowChromeDragRestorePointerMove( + EditorContext& editorContext, + bool globalTabDragActive) { + if (!m_chrome.runtime.IsBorderlessWindowDragRestoreArmed() || m_window.hwnd == nullptr) { + return false; + } + + POINT currentScreenPoint = {}; + if (!GetCursorPos(¤tScreenPoint)) { + return true; + } + + const POINT initialScreenPoint = + m_chrome.runtime.GetBorderlessWindowDragRestoreInitialScreenPoint(); + const int dragThresholdX = (std::max)(GetSystemMetrics(SM_CXDRAG), 1); + const int dragThresholdY = (std::max)(GetSystemMetrics(SM_CYDRAG), 1); + const LONG deltaX = currentScreenPoint.x - initialScreenPoint.x; + const LONG deltaY = currentScreenPoint.y - initialScreenPoint.y; + if (std::abs(deltaX) < dragThresholdX && + std::abs(deltaY) < dragThresholdY) { + return true; + } + + RECT restoreRect = {}; + RECT currentRect = {}; + RECT workAreaRect = {}; + if (!m_chrome.runtime.TryGetBorderlessWindowRestoreRect(restoreRect) || + !QueryCurrentWindowRect(currentRect) || + !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { + ClearBorderlessWindowChromeDragRestoreState(); + return true; + } + + const int restoreWidth = restoreRect.right - restoreRect.left; + const int restoreHeight = restoreRect.bottom - restoreRect.top; + const int currentWidth = currentRect.right - currentRect.left; + if (restoreWidth <= 0 || restoreHeight <= 0 || currentWidth <= 0) { + ClearBorderlessWindowChromeDragRestoreState(); + return true; + } + + const float pointerRatio = + static_cast(currentScreenPoint.x - currentRect.left) / + static_cast(currentWidth); + const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f); + const int newLeft = + (std::clamp)( + currentScreenPoint.x - + static_cast(clampedPointerRatio * static_cast(restoreWidth)), + workAreaRect.left, + workAreaRect.right - restoreWidth); + const int titleBarHeightPixels = + static_cast(kBorderlessTitleBarHeightDips * GetDpiScale()); + const int newTop = + (std::clamp)( + currentScreenPoint.y - (std::max)(titleBarHeightPixels / 2, 1), + workAreaRect.top, + workAreaRect.bottom - restoreHeight); + const RECT targetRect = { + newLeft, + newTop, + newLeft + restoreWidth, + newTop + restoreHeight + }; + + m_chrome.runtime.SetBorderlessWindowMaximized(false); + ApplyPredictedWindowRectTransition( + editorContext, + globalTabDragActive, + targetRect); + ClearBorderlessWindowChromeDragRestoreState(); + SendMessageW(m_window.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); + return true; +} + +void EditorWindow::ClearBorderlessWindowChromeDragRestoreState() { + if (!m_chrome.runtime.IsBorderlessWindowDragRestoreArmed()) { + return; + } + + m_chrome.runtime.EndBorderlessWindowDragRestore(); + ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome); +} + void EditorWindow::ClearBorderlessWindowChromeState() { if (m_chrome.chromeState.hoveredTarget == Host::BorderlessWindowChromeHitTarget::None && diff --git a/new_editor/app/Platform/Win32/InputModifierTracker.h b/new_editor/app/Platform/Win32/InputModifierTracker.h index c7ea143a..0da9a538 100644 --- a/new_editor/app/Platform/Win32/InputModifierTracker.h +++ b/new_editor/app/Platform/Win32/InputModifierTracker.h @@ -24,6 +24,11 @@ public: m_rightAlt = false; m_leftSuper = false; m_rightSuper = false; + m_leftMouse = false; + m_rightMouse = false; + m_middleMouse = false; + m_x1Mouse = false; + m_x2Mouse = false; } void SyncFromSystemState() { @@ -35,6 +40,11 @@ public: m_rightAlt = (GetKeyState(VK_RMENU) & 0x8000) != 0; m_leftSuper = (GetKeyState(VK_LWIN) & 0x8000) != 0; m_rightSuper = (GetKeyState(VK_RWIN) & 0x8000) != 0; + m_leftMouse = (GetKeyState(VK_LBUTTON) & 0x8000) != 0; + m_rightMouse = (GetKeyState(VK_RBUTTON) & 0x8000) != 0; + m_middleMouse = (GetKeyState(VK_MBUTTON) & 0x8000) != 0; + m_x1Mouse = (GetKeyState(VK_XBUTTON1) & 0x8000) != 0; + m_x2Mouse = (GetKeyState(VK_XBUTTON2) & 0x8000) != 0; } ::XCEngine::UI::UIInputModifiers GetCurrentModifiers() const { @@ -43,11 +53,25 @@ public: ::XCEngine::UI::UIInputModifiers BuildPointerModifiers(std::size_t wParam) const { ::XCEngine::UI::UIInputModifiers modifiers = BuildModifiers(); - modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0; - modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0; + ApplyPointerWParam(modifiers, wParam); return modifiers; } + ::XCEngine::UI::UIInputModifiers ApplyPointerMessage( + ::XCEngine::UI::UIInputEventType type, + ::XCEngine::UI::UIPointerButton button, + std::size_t wParam) { + ::XCEngine::UI::UIInputModifiers modifiers = BuildPointerModifiers(wParam); + if (type == ::XCEngine::UI::UIInputEventType::PointerButtonDown) { + SetPointerButton(modifiers, button, true); + } else if (type == ::XCEngine::UI::UIInputEventType::PointerButtonUp) { + SetPointerButton(modifiers, button, false); + } + + ApplyPointerState(modifiers); + return BuildModifiers(); + } + ::XCEngine::UI::UIInputModifiers ApplyKeyMessage( ::XCEngine::UI::UIInputEventType type, WPARAM wParam, @@ -119,6 +143,52 @@ private: } } + static void ApplyPointerWParam( + ::XCEngine::UI::UIInputModifiers& modifiers, + std::size_t wParam) { + modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0; + modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0; + modifiers.leftMouse = (wParam & MK_LBUTTON) != 0; + modifiers.rightMouse = (wParam & MK_RBUTTON) != 0; + modifiers.middleMouse = (wParam & MK_MBUTTON) != 0; + modifiers.x1Mouse = (wParam & MK_XBUTTON1) != 0; + modifiers.x2Mouse = (wParam & MK_XBUTTON2) != 0; + } + + static void SetPointerButton( + ::XCEngine::UI::UIInputModifiers& modifiers, + ::XCEngine::UI::UIPointerButton button, + bool pressed) { + switch (button) { + case ::XCEngine::UI::UIPointerButton::Left: + modifiers.leftMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::Right: + modifiers.rightMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::Middle: + modifiers.middleMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::X1: + modifiers.x1Mouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::X2: + modifiers.x2Mouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::None: + default: + break; + } + } + + void ApplyPointerState(const ::XCEngine::UI::UIInputModifiers& modifiers) { + m_leftMouse = modifiers.leftMouse; + m_rightMouse = modifiers.rightMouse; + m_middleMouse = modifiers.middleMouse; + m_x1Mouse = modifiers.x1Mouse; + m_x2Mouse = modifiers.x2Mouse; + } + void SetModifierState(ModifierKey key, bool pressed) { switch (key) { case ModifierKey::LeftShift: @@ -157,6 +227,11 @@ private: modifiers.control = m_leftControl || m_rightControl; modifiers.alt = m_leftAlt || m_rightAlt; modifiers.super = m_leftSuper || m_rightSuper; + modifiers.leftMouse = m_leftMouse; + modifiers.rightMouse = m_rightMouse; + modifiers.middleMouse = m_middleMouse; + modifiers.x1Mouse = m_x1Mouse; + modifiers.x2Mouse = m_x2Mouse; return modifiers; } @@ -168,6 +243,11 @@ private: bool m_rightAlt = false; bool m_leftSuper = false; bool m_rightSuper = false; + bool m_leftMouse = false; + bool m_rightMouse = false; + bool m_middleMouse = false; + bool m_x1Mouse = false; + bool m_x2Mouse = false; }; } // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Platform/Win32/WindowManager/Creation.cpp b/new_editor/app/Platform/Win32/WindowManager/Creation.cpp deleted file mode 100644 index e938850a..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/Creation.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "State/EditorContext.h" -#include "Bootstrap/EditorResources.h" -#include "Platform/Win32/EditorWindow.h" - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -EditorWindow* EditorWindowHostRuntime::CreateEditorWindow( - UIEditorWorkspaceController workspaceController, - const CreateParams& params) { - auto windowPtr = std::make_unique( - params.windowId, - params.title.empty() ? std::wstring(L"XCEngine Editor") : params.title, - params.primary, - std::move(workspaceController)); - EditorWindow* const rawWindow = windowPtr.get(); - m_windows.push_back(std::move(windowPtr)); - - const auto eraseRawWindow = [this, rawWindow]() { - const auto it = std::find_if( - m_windows.begin(), - m_windows.end(), - [rawWindow](const std::unique_ptr& candidate) { - return candidate.get() == rawWindow; - }); - if (it != m_windows.end()) { - m_windows.erase(it); - } - }; - - m_pendingCreateWindow = rawWindow; - const HWND hwnd = CreateWindowExW( - WS_EX_APPWINDOW, - m_hostConfig.windowClassName, - rawWindow->GetTitle().c_str(), - m_hostConfig.windowStyle, - params.initialX, - params.initialY, - params.initialWidth, - params.initialHeight, - nullptr, - nullptr, - m_hostConfig.hInstance, - m_hostConfig.windowUserData); - m_pendingCreateWindow = nullptr; - if (hwnd == nullptr) { - eraseRawWindow(); - return nullptr; - } - - if (!rawWindow->HasHwnd()) { - rawWindow->AttachHwnd(hwnd); - } - - auto failWindowInitialization = [&](std::string_view message) { - LogRuntimeTrace("window", std::string(message)); - DestroyEditorWindow(*rawWindow); - eraseRawWindow(); - return static_cast(nullptr); - }; - - const HICON bigIcon = static_cast( - LoadImageW( - m_hostConfig.hInstance, - MAKEINTRESOURCEW(IDI_APP_ICON), - IMAGE_ICON, - 0, - 0, - LR_DEFAULTSIZE)); - const HICON smallIcon = static_cast( - LoadImageW( - m_hostConfig.hInstance, - MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), - IMAGE_ICON, - GetSystemMetrics(SM_CXSMICON), - GetSystemMetrics(SM_CYSMICON), - LR_DEFAULTCOLOR)); - if (bigIcon != nullptr) { - SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(bigIcon)); - } - if (smallIcon != nullptr) { - SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); - } - - if (!rawWindow->Initialize( - m_repoRoot, - m_editorContext, - m_editorContext.GetShellAsset().captureRootPath, - params.autoCaptureOnStartup)) { - return failWindowInitialization("managed window initialization failed"); - } - - ShowWindow(hwnd, params.showCommand); - UpdateWindow(hwnd); - return rawWindow; -} - -void EditorWindowHostRuntime::HandlePendingNativeWindowCreated(HWND hwnd) { - if (m_pendingCreateWindow != nullptr && !m_pendingCreateWindow->HasHwnd()) { - m_pendingCreateWindow->AttachHwnd(hwnd); - } -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDrop.cpp b/new_editor/app/Platform/Win32/WindowManager/CrossWindowDrop.cpp deleted file mode 100644 index 8e54e82b..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDrop.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "Platform/Win32/WindowManager/CrossWindowDropInternal.h" -#include "State/EditorContext.h" -#include "Platform/Win32/EditorWindow.h" - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -using Win32::Internal::CrossWindowDockDropTarget; -using Win32::Internal::TryResolveCrossWindowDockDropTarget; -using ::XCEngine::UI::UIPoint; - -bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND hwnd) { - if (!m_globalTabDragSession.active) { - return false; - } - - const EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); - if (ownerWindow == nullptr || ownerWindow->GetHwnd() != hwnd) { - return false; - } - - POINT screenPoint = m_globalTabDragSession.screenPoint; - GetCursorPos(&screenPoint); - - const std::string panelWindowId = m_globalTabDragSession.panelWindowId; - const std::string sourceNodeId = m_globalTabDragSession.sourceNodeId; - const std::string panelId = m_globalTabDragSession.panelId; - EndGlobalTabDragSession(); - - EditorWindow* targetWindow = FindTopmostWindowAtScreenPoint(screenPoint, panelWindowId); - if (targetWindow == nullptr || targetWindow->GetHwnd() == nullptr) { - return true; - } - - const UIPoint targetPoint = - targetWindow->ConvertScreenPixelsToClientDips(screenPoint); - const Widgets::UIEditorDockHostLayout& targetLayout = - targetWindow->GetShellFrame() - .workspaceInteractionFrame - .dockHostFrame - .layout; - CrossWindowDockDropTarget dropTarget = {}; - if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { - return true; - } - - UIEditorWindowWorkspaceController windowWorkspaceController = - BuildLiveWindowWorkspaceController(targetWindow->GetWindowId()); - const UIEditorWindowWorkspaceOperationResult result = - dropTarget.placement == UIEditorWorkspaceDockPlacement::Center - ? windowWorkspaceController.MovePanelToStack( - panelWindowId, - sourceNodeId, - panelId, - targetWindow->GetWindowId(), - dropTarget.nodeId, - dropTarget.insertionIndex) - : windowWorkspaceController.DockPanelRelative( - panelWindowId, - sourceNodeId, - panelId, - targetWindow->GetWindowId(), - dropTarget.nodeId, - dropTarget.placement); - if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { - LogRuntimeTrace("drag", "cross-window drop rejected: " + result.message); - return true; - } - - if (!SynchronizeWindowsFromController( - windowWorkspaceController, - {}, - screenPoint)) { - LogRuntimeTrace("drag", "failed to synchronize windows after cross-window drop"); - return true; - } - - if (EditorWindow* updatedTargetWindow = m_hostRuntime.FindWindow(targetWindow->GetWindowId()); - updatedTargetWindow != nullptr && - updatedTargetWindow->GetHwnd() != nullptr) { - SetForegroundWindow(updatedTargetWindow->GetHwnd()); - } - LogRuntimeTrace( - "drag", - "committed cross-window drop panel '" + panelId + - "' into window '" + std::string(targetWindow->GetWindowId()) + "'"); - return true; -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.cpp b/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.cpp deleted file mode 100644 index d15743b7..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "Platform/Win32/WindowManager/CrossWindowDropInternal.h" - -#include - -#include - -namespace XCEngine::UI::Editor::App::Win32::Internal { - -bool TryResolveCrossWindowDockDropTarget( - const Widgets::UIEditorDockHostLayout& layout, - const ::XCEngine::UI::UIPoint& point, - CrossWindowDockDropTarget& outTarget) { - using ::XCEngine::UI::UIPoint; - using ::XCEngine::UI::UIRect; - - const auto isPointInsideRect = [](const UIRect& rect, const UIPoint& targetPoint) { - return targetPoint.x >= rect.x && - targetPoint.x <= rect.x + rect.width && - targetPoint.y >= rect.y && - targetPoint.y <= rect.y + rect.height; - }; - - const auto resolveInsertionIndex = [&](const Widgets::UIEditorDockHostTabStackLayout& tabStack) { - if (!isPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { - return Widgets::UIEditorTabStripInvalidIndex; - } - - std::size_t insertionIndex = 0u; - for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) { - const float midpoint = rect.x + rect.width * 0.5f; - if (point.x > midpoint) { - ++insertionIndex; - } - } - return insertionIndex; - }; - - const auto resolvePlacement = [&](const Widgets::UIEditorDockHostTabStackLayout& tabStack) { - if (isPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { - return UIEditorWorkspaceDockPlacement::Center; - } - - const float leftDistance = point.x - tabStack.bounds.x; - const float rightDistance = tabStack.bounds.x + tabStack.bounds.width - point.x; - const float topDistance = point.y - tabStack.bounds.y; - const float bottomDistance = tabStack.bounds.y + tabStack.bounds.height - point.y; - const float minHorizontalThreshold = tabStack.bounds.width * 0.25f; - const float minVerticalThreshold = tabStack.bounds.height * 0.25f; - const float nearestEdge = (std::min)( - (std::min)(leftDistance, rightDistance), - (std::min)(topDistance, bottomDistance)); - - if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) { - return UIEditorWorkspaceDockPlacement::Left; - } - if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) { - return UIEditorWorkspaceDockPlacement::Right; - } - if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) { - return UIEditorWorkspaceDockPlacement::Top; - } - if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) { - return UIEditorWorkspaceDockPlacement::Bottom; - } - - return UIEditorWorkspaceDockPlacement::Center; - }; - - outTarget = {}; - if (!isPointInsideRect(layout.bounds, point)) { - return false; - } - - const Widgets::UIEditorDockHostHitTarget hitTarget = - Widgets::HitTestUIEditorDockHost(layout, point); - for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { - if ((!hitTarget.nodeId.empty() && tabStack.nodeId != hitTarget.nodeId) || - !isPointInsideRect(tabStack.bounds, point)) { - continue; - } - - outTarget.valid = true; - outTarget.nodeId = tabStack.nodeId; - outTarget.placement = resolvePlacement(tabStack); - if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { - outTarget.insertionIndex = resolveInsertionIndex(tabStack); - if (outTarget.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) { - outTarget.insertionIndex = tabStack.items.size(); - } - } - return true; - } - - for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { - if (isPointInsideRect(tabStack.bounds, point)) { - outTarget.valid = true; - outTarget.nodeId = tabStack.nodeId; - outTarget.placement = resolvePlacement(tabStack); - if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { - outTarget.insertionIndex = tabStack.items.size(); - } - return true; - } - } - - return false; -} - -} // namespace XCEngine::UI::Editor::App::Win32::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.h b/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.h deleted file mode 100644 index 0b1de4b5..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/CrossWindowDropInternal.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace XCEngine::UI { -struct UIPoint; -} - -namespace XCEngine::UI::Editor::App::Win32::Internal { - -struct CrossWindowDockDropTarget { - bool valid = false; - std::string nodeId = {}; - UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; - std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; -}; - -bool TryResolveCrossWindowDockDropTarget( - const Widgets::UIEditorDockHostLayout& layout, - const ::XCEngine::UI::UIPoint& point, - CrossWindowDockDropTarget& outTarget); - -} // namespace XCEngine::UI::Editor::App::Win32::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/Detach.cpp b/new_editor/app/Platform/Win32/WindowManager/Detach.cpp deleted file mode 100644 index 47721949..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/Detach.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "State/EditorContext.h" -#include "Platform/Win32/EditorWindow.h" - -#include - -namespace XCEngine::UI::Editor::App::Internal { - -bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( - EditorWindow& sourceWindow, - const EditorWindowPanelTransferRequest& request) { - const std::string sourceWindowId(sourceWindow.GetWindowId()); - UIEditorWindowWorkspaceController windowWorkspaceController = - BuildLiveWindowWorkspaceController(sourceWindowId); - const UIEditorWindowWorkspaceOperationResult result = - windowWorkspaceController.DetachPanelToNewWindow( - sourceWindowId, - request.nodeId, - request.panelId); - if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { - LogRuntimeTrace("detach", "detach request rejected: " + result.message); - return false; - } - - if (!SynchronizeWindowsFromController( - windowWorkspaceController, - result.targetWindowId, - request.screenPoint)) { - LogRuntimeTrace("detach", "failed to synchronize detached window state"); - return false; - } - - if (EditorWindow* detachedWindow = m_hostRuntime.FindWindow(result.targetWindowId); - detachedWindow != nullptr && - detachedWindow->GetHwnd() != nullptr) { - SetForegroundWindow(detachedWindow->GetHwnd()); - } - - LogRuntimeTrace( - "detach", - "detached panel '" + request.panelId + "' from window '" + sourceWindowId + - "' to window '" + result.targetWindowId + "'"); - return true; -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp index f5329839..90447a7f 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp @@ -236,9 +236,7 @@ bool EditorWindowHostRuntime::HasWindows() const { void EditorWindowHostRuntime::DestroyEditorWindow(EditorWindow& window) { const HWND hwnd = window.GetHwnd(); - if (GetCapture() == hwnd) { - ReleaseCapture(); - } + window.ForceReleasePointerCapture(); window.Shutdown(); if (hwnd != nullptr && IsWindow(hwnd)) { diff --git a/new_editor/app/Platform/Win32/WindowManager/Lookup.cpp b/new_editor/app/Platform/Win32/WindowManager/Lookup.cpp deleted file mode 100644 index 7e52e1ee..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/Lookup.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "Platform/Win32/EditorWindow.h" - -namespace XCEngine::UI::Editor::App::Internal { - -EditorWindow* EditorWindowHostRuntime::FindWindow(HWND hwnd) { - if (hwnd == nullptr) { - return nullptr; - } - - for (const std::unique_ptr& window : m_windows) { - if (window != nullptr && window->GetHwnd() == hwnd) { - return window.get(); - } - } - - return nullptr; -} - -const EditorWindow* EditorWindowHostRuntime::FindWindow(HWND hwnd) const { - return const_cast(this)->FindWindow(hwnd); -} - -EditorWindow* EditorWindowHostRuntime::FindWindow(std::string_view windowId) { - if (windowId.empty()) { - return nullptr; - } - - for (const std::unique_ptr& window : m_windows) { - if (window != nullptr && window->GetWindowId() == windowId) { - return window.get(); - } - } - - return nullptr; -} - -const EditorWindow* EditorWindowHostRuntime::FindWindow(std::string_view windowId) const { - return const_cast(this)->FindWindow(windowId); -} - -EditorWindow* EditorWindowHostRuntime::FindPrimaryWindow() { - for (const std::unique_ptr& window : m_windows) { - if (window != nullptr && window->IsPrimary()) { - return window.get(); - } - } - - return nullptr; -} - -const EditorWindow* EditorWindowHostRuntime::FindPrimaryWindow() const { - return const_cast(this)->FindPrimaryWindow(); -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp index 858f0802..7a4864ca 100644 --- a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp @@ -336,9 +336,7 @@ void EditorWindowWorkspaceCoordinator::EndGlobalTabDragSession() { if (EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); ownerWindow != nullptr) { - if (GetCapture() == ownerWindow->GetHwnd()) { - ReleaseCapture(); - } + ownerWindow->ReleasePointerCapture(EditorWindowPointerCaptureOwner::GlobalTabDrag); if (!ownerWindow->IsClosing()) { ownerWindow->ResetInteractionState(); } @@ -491,7 +489,8 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( request.screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); - SetCapture(detachedWindow->GetHwnd()); + detachedWindow->AcquirePointerCapture( + EditorWindowPointerCaptureOwner::GlobalTabDrag); SetForegroundWindow(detachedWindow->GetHwnd()); LogRuntimeTrace( "drag", @@ -539,9 +538,7 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( request.screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); - if (sourceWindow.GetHwnd() != nullptr) { - SetCapture(sourceWindow.GetHwnd()); - } + sourceWindow.AcquirePointerCapture(EditorWindowPointerCaptureOwner::GlobalTabDrag); LogRuntimeTrace( "drag", "started global tab drag from detached window '" + diff --git a/new_editor/app/Platform/Win32/WindowManager/WindowSet.cpp b/new_editor/app/Platform/Win32/WindowManager/WindowSet.cpp deleted file mode 100644 index 151ce0cb..00000000 --- a/new_editor/app/Platform/Win32/WindowManager/WindowSet.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "Platform/Win32/WindowManager/Internal.h" - -#include "State/EditorContext.h" -#include "Platform/Win32/EditorWindow.h" - -#include -#include - -namespace XCEngine::UI::Editor::App::Internal { - -UIEditorWindowWorkspaceSet EditorWindowWorkspaceCoordinator::BuildWindowWorkspaceSet( - std::string_view activeWindowId) const { - UIEditorWindowWorkspaceSet windowSet = {}; - if (const EditorWindow* primaryWindow = m_hostRuntime.FindPrimaryWindow(); - primaryWindow != nullptr) { - windowSet.primaryWindowId = std::string(primaryWindow->GetWindowId()); - } - - for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { - if (window == nullptr || window->GetHwnd() == nullptr) { - continue; - } - - UIEditorWindowWorkspaceState entry = {}; - entry.windowId = std::string(window->GetWindowId()); - entry.workspace = window->GetWorkspaceController().GetWorkspace(); - entry.session = window->GetWorkspaceController().GetSession(); - windowSet.windows.push_back(std::move(entry)); - } - - windowSet.activeWindowId = - !activeWindowId.empty() && m_hostRuntime.FindWindow(activeWindowId) != nullptr - ? std::string(activeWindowId) - : windowSet.primaryWindowId; - - return windowSet; -} - -UIEditorWindowWorkspaceController -EditorWindowWorkspaceCoordinator::BuildLiveWindowWorkspaceController( - std::string_view activeWindowId) const { - return UIEditorWindowWorkspaceController( - m_hostRuntime.GetEditorContext().GetShellAsset().panelRegistry, - BuildWindowWorkspaceSet(activeWindowId)); -} - -UIEditorWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceControllerForWindow( - const UIEditorWindowWorkspaceState& windowState) const { - return UIEditorWorkspaceController( - m_hostRuntime.GetEditorContext().GetShellAsset().panelRegistry, - windowState.workspace, - windowState.session); -} - -} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp index b38e990b..7944ed6b 100644 --- a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp @@ -91,9 +91,7 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( EditorWindow& window = *it->get(); const HWND hwnd = window.GetHwnd(); - if (GetCapture() == hwnd) { - ReleaseCapture(); - } + window.ForceReleasePointerCapture(); window.Shutdown(); if (hwnd != nullptr && IsWindow(hwnd)) { diff --git a/new_editor/app/Platform/Win32/WindowMessageDispatcher.cpp b/new_editor/app/Platform/Win32/WindowMessageDispatcher.cpp index a308352f..9205b753 100644 --- a/new_editor/app/Platform/Win32/WindowMessageDispatcher.cpp +++ b/new_editor/app/Platform/Win32/WindowMessageDispatcher.cpp @@ -1,6 +1,7 @@ #include "WindowMessageDispatcher.h" #include "Platform/Win32/EditorWindow.h" +#include "Platform/Win32/EditorWindowPointerCapture.h" #include "WindowMessageHost.h" #include @@ -59,6 +60,13 @@ bool WindowMessageDispatcher::TryHandleChromeHoverConsumption( const DispatchContext& context, LPARAM lParam, LRESULT& outResult) { + if (!App::CanConsumeEditorWindowChromeHover( + context.window.GetPointerCaptureOwner(), + context.window.GetShellRuntime().HasShellInteractiveCapture(), + context.window.GetShellRuntime().HasHostedContentCapture())) { + return false; + } + const bool resizeHoverChanged = context.window.UpdateBorderlessWindowResizeHover(lParam); if (context.window.UpdateBorderlessWindowChromeHover(lParam)) { context.window.InvalidateHostWindow(); @@ -93,17 +101,24 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( LRESULT& outResult) { switch (message) { case WM_MOUSEMOVE: - if (context.windowHost.HandleGlobalTabDragPointerMove(context.hwnd)) { + if (App::CanRouteEditorWindowGlobalTabDragPointerMessages( + context.window.GetPointerCaptureOwner(), + context.windowHost.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) && + context.windowHost.HandleGlobalTabDragPointerMove(context.hwnd)) { outResult = 0; return true; } - if (context.window.HandleBorderlessWindowResizePointerMove( + if (App::CanRouteEditorWindowBorderlessResizePointerMessages( + context.window.GetPointerCaptureOwner()) && + context.window.HandleBorderlessWindowResizePointerMove( context.windowHost.GetEditorContext(), context.windowHost.IsGlobalTabDragActive())) { outResult = 0; return true; } - if (context.window.HandleBorderlessWindowChromeDragRestorePointerMove( + if (App::CanRouteEditorWindowBorderlessChromePointerMessages( + context.window.GetPointerCaptureOwner()) && + context.window.HandleBorderlessWindowChromeDragRestorePointerMove( context.windowHost.GetEditorContext(), context.windowHost.IsGlobalTabDragActive())) { outResult = 0; @@ -129,6 +144,9 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( outResult = 0; return true; case WM_LBUTTONDOWN: + if (context.windowHost.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) { + context.windowHost.EndGlobalTabDragSession(); + } if (context.window.HandleBorderlessWindowResizeButtonDown(lParam)) { outResult = 0; return true; @@ -138,6 +156,7 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( return true; } SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Left, @@ -145,16 +164,43 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( lParam); outResult = 0; return true; + case WM_RBUTTONDOWN: + SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonDown, + ::XCEngine::UI::UIPointerButton::Right, + wParam, + lParam); + outResult = 0; + return true; + case WM_MBUTTONDOWN: + SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonDown, + ::XCEngine::UI::UIPointerButton::Middle, + wParam, + lParam); + outResult = 0; + return true; case WM_LBUTTONUP: - if (context.windowHost.HandleGlobalTabDragPointerButtonUp(context.hwnd)) { + if (App::CanRouteEditorWindowGlobalTabDragPointerMessages( + context.window.GetPointerCaptureOwner(), + context.windowHost.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) && + context.windowHost.HandleGlobalTabDragPointerButtonUp(context.hwnd)) { outResult = 0; return true; } - if (context.window.HandleBorderlessWindowResizeButtonUp()) { + if (App::CanRouteEditorWindowBorderlessResizePointerMessages( + context.window.GetPointerCaptureOwner()) && + context.window.HandleBorderlessWindowResizeButtonUp()) { outResult = 0; return true; } - if (context.window.HandleBorderlessWindowChromeButtonUp( + if (App::CanRouteEditorWindowBorderlessChromePointerMessages( + context.window.GetPointerCaptureOwner()) && + context.window.HandleBorderlessWindowChromeButtonUp( context.windowHost.GetEditorContext(), context.windowHost.IsGlobalTabDragActive(), lParam)) { @@ -168,6 +214,22 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( lParam); outResult = 0; return true; + case WM_RBUTTONUP: + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonUp, + ::XCEngine::UI::UIPointerButton::Right, + wParam, + lParam); + outResult = 0; + return true; + case WM_MBUTTONUP: + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonUp, + ::XCEngine::UI::UIPointerButton::Middle, + wParam, + lParam); + outResult = 0; + return true; case WM_LBUTTONDBLCLK: if (context.window.HandleBorderlessWindowChromeDoubleClick( context.windowHost.GetEditorContext(), @@ -176,7 +238,35 @@ bool WindowMessageDispatcher::TryDispatchWindowPointerMessage( outResult = 0; return true; } - return false; + SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonDown, + ::XCEngine::UI::UIPointerButton::Left, + wParam, + lParam); + outResult = 0; + return true; + case WM_RBUTTONDBLCLK: + SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonDown, + ::XCEngine::UI::UIPointerButton::Right, + wParam, + lParam); + outResult = 0; + return true; + case WM_MBUTTONDBLCLK: + SetFocus(context.hwnd); + context.window.TryStartImmediateShellPointerCapture(lParam); + context.window.QueuePointerEvent( + ::XCEngine::UI::UIInputEventType::PointerButtonDown, + ::XCEngine::UI::UIPointerButton::Middle, + wParam, + lParam); + outResult = 0; + return true; case WM_MOUSEWHEEL: context.window.QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); outResult = 0; @@ -208,6 +298,9 @@ bool WindowMessageDispatcher::TryDispatchWindowInputMessage( outResult = 0; return true; case WM_CAPTURECHANGED: + if (reinterpret_cast(lParam) != context.hwnd) { + context.window.ClearPointerCaptureOwner(); + } if (context.windowHost.OwnsActiveGlobalTabDrag(context.window.GetWindowId()) && reinterpret_cast(lParam) != context.hwnd) { context.windowHost.EndGlobalTabDragSession(); diff --git a/new_editor/app/Project/EditorProjectRuntime.cpp b/new_editor/app/Project/EditorProjectRuntime.cpp new file mode 100644 index 00000000..27a25e01 --- /dev/null +++ b/new_editor/app/Project/EditorProjectRuntime.cpp @@ -0,0 +1,454 @@ +#include "Project/EditorProjectRuntime.h" + +#include "State/EditorSelectionStamp.h" + +namespace XCEngine::UI::Editor::App { + +namespace { + +using AssetCommandTarget = EditorProjectRuntime::AssetCommandTarget; +using EditCommandTarget = EditorProjectRuntime::EditCommandTarget; + +bool IsSameOrDescendantItemId( + std::string_view candidateId, + std::string_view ancestorId) { + if (candidateId == ancestorId) { + return true; + } + + if (candidateId.size() <= ancestorId.size() || + candidateId.substr(0u, ancestorId.size()) != ancestorId) { + return false; + } + + return candidateId[ancestorId.size()] == '/'; +} + +void PopulateAssetCommandTargetFromAsset( + AssetCommandTarget& target, + const ProjectBrowserModel& browserModel, + const ProjectBrowserModel::AssetEntry& asset, + const ProjectBrowserModel::FolderEntry* containerFolder) { + target.subjectAsset = &asset; + target.subjectItemId = asset.itemId; + target.subjectPath = asset.absolutePath; + target.subjectDisplayName = asset.displayName; + target.subjectRelativePath = + browserModel.BuildProjectRelativePath(asset.itemId); + target.showInExplorerSelectTarget = true; + target.containerFolder = containerFolder; +} + +void PopulateAssetCommandTargetFromFolder( + AssetCommandTarget& target, + const ProjectBrowserModel& browserModel, + const ProjectBrowserModel::FolderEntry& folder) { + target.subjectFolder = &folder; + target.subjectItemId = folder.itemId; + target.subjectPath = folder.absolutePath; + target.subjectDisplayName = folder.label; + target.subjectRelativePath = + browserModel.BuildProjectRelativePath(folder.itemId); + target.showInExplorerSelectTarget = false; + target.containerFolder = &folder; +} + +EditCommandTarget BuildEditCommandTarget( + const ProjectBrowserModel::AssetEntry& asset) { + EditCommandTarget target = {}; + target.itemId = asset.itemId; + target.absolutePath = asset.absolutePath; + target.displayName = asset.displayName; + target.itemKind = asset.kind; + target.directory = asset.directory; + return target; +} + +EditCommandTarget BuildEditCommandTarget( + const ProjectBrowserModel& browserModel, + const ProjectBrowserModel::FolderEntry& folder) { + EditCommandTarget target = {}; + target.itemId = folder.itemId; + target.absolutePath = folder.absolutePath; + target.displayName = folder.label; + target.itemKind = ProjectBrowserModel::ItemKind::Folder; + target.directory = true; + target.assetsRoot = browserModel.IsAssetsRoot(folder.itemId); + return target; +} + +} // namespace + +bool EditorProjectRuntime::Initialize(const std::filesystem::path& repoRoot) { + m_browserModel.Initialize(repoRoot); + m_selection = {}; + m_pendingSceneOpenPath.reset(); + return true; +} + +void EditorProjectRuntime::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { + m_browserModel.SetFolderIcon(icon); +} + +void EditorProjectRuntime::Refresh() { + m_browserModel.Refresh(); + RevalidateSelection(); +} + +const ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() const { + return m_browserModel; +} + +ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() { + return m_browserModel; +} + +bool EditorProjectRuntime::HasSelection() const { + return m_selection.kind == EditorSelectionKind::ProjectItem && + !m_selection.itemId.empty(); +} + +const EditorSelectionState& EditorProjectRuntime::GetSelection() const { + return m_selection; +} + +std::uint64_t EditorProjectRuntime::GetSelectionStamp() const { + return m_selection.stamp; +} + +bool EditorProjectRuntime::SetSelection(std::string_view itemId) { + const ProjectBrowserModel::AssetEntry* asset = FindAssetEntry(itemId); + if (asset == nullptr) { + return false; + } + + const bool changed = + m_selection.kind != EditorSelectionKind::ProjectItem || + m_selection.itemId != asset->itemId || + m_selection.absolutePath != asset->absolutePath || + m_selection.directory != asset->directory; + ApplySelection(*asset, !changed); + return changed; +} + +void EditorProjectRuntime::ClearSelection() { + m_selection = {}; + m_selection.stamp = GenerateEditorSelectionStamp(); +} + +bool EditorProjectRuntime::NavigateToFolder(std::string_view itemId) { + if (!m_browserModel.NavigateToFolder(itemId)) { + return false; + } + + ClearSelection(); + return true; +} + +bool EditorProjectRuntime::OpenItem(std::string_view itemId) { + const ProjectBrowserModel::AssetEntry* asset = FindAssetEntry(itemId); + if (asset == nullptr) { + return false; + } + + if (asset->directory) { + return NavigateToFolder(asset->itemId); + } + + if (!asset->canOpen) { + return false; + } + + if (asset->kind == ProjectBrowserModel::ItemKind::Scene && + !asset->absolutePath.empty()) { + m_pendingSceneOpenPath = asset->absolutePath; + return true; + } + + return false; +} + +std::optional EditorProjectRuntime::ConsumePendingSceneOpenPath() { + if (!m_pendingSceneOpenPath.has_value()) { + return std::nullopt; + } + + std::optional result = m_pendingSceneOpenPath; + m_pendingSceneOpenPath.reset(); + return result; +} + +const ProjectBrowserModel::FolderEntry* EditorProjectRuntime::FindFolderEntry( + std::string_view itemId) const { + return m_browserModel.FindFolderEntry(itemId); +} + +const ProjectBrowserModel::AssetEntry* EditorProjectRuntime::FindAssetEntry( + std::string_view itemId) const { + return m_browserModel.FindAssetEntry(itemId); +} + +EditorProjectRuntime::AssetCommandTarget +EditorProjectRuntime::ResolveAssetCommandTarget( + std::string_view explicitItemId, + bool forceCurrentFolder) const { + AssetCommandTarget target = {}; + target.currentFolder = FindFolderEntry(m_browserModel.GetCurrentFolderId()); + + const ProjectBrowserModel::AssetEntry* explicitAsset = + explicitItemId.empty() ? nullptr : FindAssetEntry(explicitItemId); + const ProjectBrowserModel::FolderEntry* explicitFolder = + explicitItemId.empty() ? nullptr : FindFolderEntry(explicitItemId); + + if (!forceCurrentFolder && explicitAsset != nullptr) { + PopulateAssetCommandTargetFromAsset( + target, + m_browserModel, + *explicitAsset, + explicitAsset->directory + ? FindFolderEntry(explicitAsset->itemId) + : target.currentFolder); + return target; + } + + if (!forceCurrentFolder && explicitFolder != nullptr) { + PopulateAssetCommandTargetFromFolder( + target, + m_browserModel, + *explicitFolder); + return target; + } + + if (forceCurrentFolder) { + if (target.currentFolder != nullptr) { + PopulateAssetCommandTargetFromFolder( + target, + m_browserModel, + *target.currentFolder); + } + return target; + } + + if (HasSelection()) { + if (const ProjectBrowserModel::AssetEntry* selectedAsset = + FindAssetEntry(m_selection.itemId); + selectedAsset != nullptr) { + PopulateAssetCommandTargetFromAsset( + target, + m_browserModel, + *selectedAsset, + selectedAsset->directory + ? FindFolderEntry(selectedAsset->itemId) + : target.currentFolder); + return target; + } + } + + if (target.currentFolder != nullptr) { + PopulateAssetCommandTargetFromFolder( + target, + m_browserModel, + *target.currentFolder); + } + return target; +} + +std::optional +EditorProjectRuntime::ResolveEditCommandTarget( + std::string_view explicitItemId, + bool forceCurrentFolder) const { + if (forceCurrentFolder) { + return std::nullopt; + } + + if (!explicitItemId.empty()) { + if (const ProjectBrowserModel::AssetEntry* asset = + FindAssetEntry(explicitItemId); + asset != nullptr) { + return BuildEditCommandTarget(*asset); + } + + if (const ProjectBrowserModel::FolderEntry* folder = + FindFolderEntry(explicitItemId); + folder != nullptr) { + return BuildEditCommandTarget(m_browserModel, *folder); + } + + return std::nullopt; + } + + if (HasSelection()) { + if (const ProjectBrowserModel::AssetEntry* selectedAsset = + FindAssetEntry(m_selection.itemId); + selectedAsset != nullptr) { + return BuildEditCommandTarget(*selectedAsset); + } + } + + if (const ProjectBrowserModel::FolderEntry* currentFolder = + FindFolderEntry(m_browserModel.GetCurrentFolderId()); + currentFolder != nullptr) { + return BuildEditCommandTarget(m_browserModel, *currentFolder); + } + + return std::nullopt; +} + +bool EditorProjectRuntime::CreateFolder( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdFolderId) { + const bool created = + m_browserModel.CreateFolder(parentFolderId, requestedName, createdFolderId); + RevalidateSelection(); + return created; +} + +bool EditorProjectRuntime::CreateMaterial( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdItemId) { + const bool created = + m_browserModel.CreateMaterial(parentFolderId, requestedName, createdItemId); + RevalidateSelection(); + return created; +} + +bool EditorProjectRuntime::RenameItem( + std::string_view itemId, + std::string_view newName, + std::string* renamedItemId) { + std::string resolvedRenamedItemId = {}; + std::string* renameOutput = + renamedItemId != nullptr ? renamedItemId : &resolvedRenamedItemId; + if (!m_browserModel.RenameItem(itemId, newName, renameOutput)) { + return false; + } + + if (SelectionTargetsItem(itemId)) { + if (!renameOutput->empty() && !SetSelection(*renameOutput)) { + ClearSelection(); + } + } else { + RevalidateSelection(); + } + return true; +} + +bool EditorProjectRuntime::DeleteItem(std::string_view itemId) { + const bool selectionAffected = SelectionTargetsItem(itemId); + if (!m_browserModel.DeleteItem(itemId)) { + return false; + } + + if (selectionAffected) { + ClearSelection(); + } else { + RevalidateSelection(); + } + return true; +} + +bool EditorProjectRuntime::CanMoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId) const { + return m_browserModel.CanMoveItemToFolder(itemId, targetFolderId); +} + +bool EditorProjectRuntime::MoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId, + std::string* movedItemId) { + const bool selectionAffected = SelectionTargetsItem(itemId); + if (!m_browserModel.MoveItemToFolder(itemId, targetFolderId, movedItemId)) { + return false; + } + + if (selectionAffected) { + ClearSelection(); + } else { + RevalidateSelection(); + } + return true; +} + +std::optional EditorProjectRuntime::GetParentFolderId( + std::string_view itemId) const { + return m_browserModel.GetParentFolderId(itemId); +} + +bool EditorProjectRuntime::CanReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId) const { + return m_browserModel.CanReparentFolder(sourceFolderId, targetParentId); +} + +bool EditorProjectRuntime::ReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId, + std::string* movedFolderId) { + const bool selectionAffected = SelectionTargetsItem(sourceFolderId); + if (!m_browserModel.ReparentFolder(sourceFolderId, targetParentId, movedFolderId)) { + return false; + } + + if (selectionAffected) { + ClearSelection(); + } else { + RevalidateSelection(); + } + return true; +} + +bool EditorProjectRuntime::MoveFolderToRoot( + std::string_view sourceFolderId, + std::string* movedFolderId) { + const bool selectionAffected = SelectionTargetsItem(sourceFolderId); + if (!m_browserModel.MoveFolderToRoot(sourceFolderId, movedFolderId)) { + return false; + } + + if (selectionAffected) { + ClearSelection(); + } else { + RevalidateSelection(); + } + return true; +} + +void EditorProjectRuntime::ApplySelection( + const ProjectBrowserModel::AssetEntry& asset, + bool preserveStamp) { + const std::uint64_t previousStamp = m_selection.stamp; + m_selection = {}; + m_selection.kind = EditorSelectionKind::ProjectItem; + m_selection.itemId = asset.itemId; + m_selection.displayName = asset.displayName.empty() + ? asset.nameWithExtension + : asset.displayName; + m_selection.absolutePath = asset.absolutePath; + m_selection.directory = asset.directory; + m_selection.stamp = preserveStamp && previousStamp != 0u + ? previousStamp + : GenerateEditorSelectionStamp(); +} + +void EditorProjectRuntime::RevalidateSelection() { + if (!HasSelection()) { + return; + } + + const ProjectBrowserModel::AssetEntry* asset = FindAssetEntry(m_selection.itemId); + if (asset == nullptr) { + ClearSelection(); + return; + } + + ApplySelection(*asset, true); +} + +bool EditorProjectRuntime::SelectionTargetsItem(std::string_view itemId) const { + return HasSelection() && IsSameOrDescendantItemId(m_selection.itemId, itemId); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Project/EditorProjectRuntime.h b/new_editor/app/Project/EditorProjectRuntime.h new file mode 100644 index 00000000..eeaed41a --- /dev/null +++ b/new_editor/app/Project/EditorProjectRuntime.h @@ -0,0 +1,112 @@ +#pragma once + +#include "Features/Project/ProjectBrowserModel.h" + +#include + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorProjectRuntime { +public: + struct EditCommandTarget { + std::string itemId = {}; + std::filesystem::path absolutePath = {}; + std::string displayName = {}; + ProjectBrowserModel::ItemKind itemKind = + ProjectBrowserModel::ItemKind::File; + bool directory = false; + bool assetsRoot = false; + }; + + struct AssetCommandTarget { + const ProjectBrowserModel::FolderEntry* currentFolder = nullptr; + const ProjectBrowserModel::FolderEntry* containerFolder = nullptr; + const ProjectBrowserModel::FolderEntry* subjectFolder = nullptr; + const ProjectBrowserModel::AssetEntry* subjectAsset = nullptr; + std::string subjectItemId = {}; + std::filesystem::path subjectPath = {}; + std::string subjectDisplayName = {}; + std::string subjectRelativePath = {}; + bool showInExplorerSelectTarget = false; + }; + + bool Initialize(const std::filesystem::path& repoRoot); + void SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon); + void Refresh(); + + const ProjectBrowserModel& GetBrowserModel() const; + ProjectBrowserModel& GetBrowserModel(); + + bool HasSelection() const; + const EditorSelectionState& GetSelection() const; + std::uint64_t GetSelectionStamp() const; + bool SetSelection(std::string_view itemId); + void ClearSelection(); + + bool NavigateToFolder(std::string_view itemId); + bool OpenItem(std::string_view itemId); + std::optional ConsumePendingSceneOpenPath(); + + const ProjectBrowserModel::FolderEntry* FindFolderEntry( + std::string_view itemId) const; + const ProjectBrowserModel::AssetEntry* FindAssetEntry( + std::string_view itemId) const; + AssetCommandTarget ResolveAssetCommandTarget( + std::string_view explicitItemId = {}, + bool forceCurrentFolder = false) const; + std::optional ResolveEditCommandTarget( + std::string_view explicitItemId = {}, + bool forceCurrentFolder = false) const; + + bool CreateFolder( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdFolderId = nullptr); + bool CreateMaterial( + std::string_view parentFolderId, + std::string_view requestedName, + std::string* createdItemId = nullptr); + bool RenameItem( + std::string_view itemId, + std::string_view newName, + std::string* renamedItemId = nullptr); + bool DeleteItem(std::string_view itemId); + bool CanMoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId) const; + bool MoveItemToFolder( + std::string_view itemId, + std::string_view targetFolderId, + std::string* movedItemId = nullptr); + std::optional GetParentFolderId(std::string_view itemId) const; + bool CanReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId) const; + bool ReparentFolder( + std::string_view sourceFolderId, + std::string_view targetParentId, + std::string* movedFolderId = nullptr); + bool MoveFolderToRoot( + std::string_view sourceFolderId, + std::string* movedFolderId = nullptr); + +private: + void ApplySelection( + const ProjectBrowserModel::AssetEntry& asset, + bool preserveStamp); + void RevalidateSelection(); + bool SelectionTargetsItem(std::string_view itemId) const; + + ProjectBrowserModel m_browserModel = {}; + EditorSelectionState m_selection = {}; + std::optional m_pendingSceneOpenPath = std::nullopt; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp new file mode 100644 index 00000000..7dc38475 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp @@ -0,0 +1,642 @@ +#include "Rendering/Viewport/Passes/SceneViewportGridPass.h" + +#include "Rendering/Viewport/SceneViewportResourcePaths.h" + +#include "Rendering/Internal/RenderSurfacePipelineUtils.h" +#include "Rendering/Internal/ShaderVariantUtils.h" +#include "Rendering/Materials/RenderMaterialStateUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +constexpr float kCameraHeightScaleFactor = 0.50f; +constexpr float kTransitionStart = 0.65f; +constexpr float kTransitionEnd = 0.95f; +constexpr float kMinimumVerticalViewComponent = 0.15f; + +struct GridConstants { + ::XCEngine::Math::Matrix4x4 viewProjection = + ::XCEngine::Math::Matrix4x4::Identity(); + ::XCEngine::Math::Vector4 cameraPositionAndScale = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 cameraRightAndFade = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 cameraUpAndTanHalfFov = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 cameraForwardAndAspect = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 viewportNearFar = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 gridTransition = + ::XCEngine::Math::Vector4::Zero(); +}; + +const ::XCEngine::Resources::ShaderPass* FindInfiniteGridCompatiblePass( + const ::XCEngine::Resources::Shader& shader, + ::XCEngine::Resources::ShaderBackend backend) { + const ::XCEngine::Resources::ShaderPass* gridPass = + shader.FindPass("InfiniteGrid"); + if (gridPass != nullptr && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + gridPass->name, + backend)) { + return gridPass; + } + + if (shader.GetPassCount() > 0 && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + shader.GetPasses()[0].name, + backend)) { + return &shader.GetPasses()[0]; + } + + return nullptr; +} + +::XCEngine::RHI::GraphicsPipelineDesc CreatePipelineDesc( + ::XCEngine::RHI::RHIType backendType, + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const ::XCEngine::Resources::Shader& shader, + const ::XCEngine::Containers::String& passName, + const ::XCEngine::Rendering::RenderSurface& surface) { + ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast( + ::XCEngine::RHI::PrimitiveTopologyType::Triangle); + ::XCEngine::Rendering::Internal:: + ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + surface, + pipelineDesc); + pipelineDesc.depthStencilFormat = static_cast( + ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface)); + + const ::XCEngine::Resources::ShaderPass* shaderPass = + shader.FindPass(passName); + if (shaderPass != nullptr && shaderPass->hasFixedFunctionState) { + ::XCEngine::Rendering::ApplyRenderState( + shaderPass->fixedFunctionState, + pipelineDesc); + } else { + ::XCEngine::Resources::MaterialRenderState fallbackState = {}; + fallbackState.cullMode = + ::XCEngine::Resources::MaterialCullMode::None; + fallbackState.depthWriteEnable = false; + fallbackState.depthTestEnable = true; + fallbackState.depthFunc = + ::XCEngine::Resources::MaterialComparisonFunc::LessEqual; + fallbackState.blendEnable = true; + fallbackState.srcBlend = + ::XCEngine::Resources::MaterialBlendFactor::SrcAlpha; + fallbackState.dstBlend = + ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha; + fallbackState.srcBlendAlpha = + ::XCEngine::Resources::MaterialBlendFactor::One; + fallbackState.dstBlendAlpha = + ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha; + fallbackState.blendOp = + ::XCEngine::Resources::MaterialBlendOp::Add; + fallbackState.blendOpAlpha = + ::XCEngine::Resources::MaterialBlendOp::Add; + fallbackState.colorWriteMask = static_cast( + ::XCEngine::RHI::ColorWriteMask::All); + ::XCEngine::Rendering::ApplyRenderState( + fallbackState, + pipelineDesc); + } + + const ::XCEngine::Resources::ShaderBackend backend = + ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + if (const ::XCEngine::Resources::ShaderStageVariant* vertexVariant = + shader.FindVariant( + passName, + ::XCEngine::Resources::ShaderType::Vertex, + backend)) { + if (shaderPass != nullptr) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + *shaderPass, + backend, + *vertexVariant, + pipelineDesc.vertexShader); + } + } + if (const ::XCEngine::Resources::ShaderStageVariant* fragmentVariant = + shader.FindVariant( + passName, + ::XCEngine::Resources::ShaderType::Fragment, + backend)) { + if (shaderPass != nullptr) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + *shaderPass, + backend, + *fragmentVariant, + pipelineDesc.fragmentShader); + } + } + + return pipelineDesc; +} + +float SnapGridSpacing(float targetSpacing) { + const float clampedTarget = (std::max)(targetSpacing, 0.02f); + const float exponent = std::floor(std::log10(clampedTarget)); + return std::pow(10.0f, exponent); +} + +float ComputeTransitionBlend(float targetSpacing, float baseScale) { + const float normalizedSpacing = + (std::max)(targetSpacing / (std::max)(baseScale, 1e-4f), 1.0f); + const float transitionPosition = std::log10(normalizedSpacing); + const float t = + (transitionPosition - kTransitionStart) / + (kTransitionEnd - kTransitionStart); + const float saturated = (std::clamp)(t, 0.0f, 1.0f); + return saturated * saturated * (3.0f - 2.0f * saturated); +} + +float ComputeViewDistanceToGridPlane(const SceneViewportGridPassData& data) { + const float cameraHeight = std::abs(data.cameraPosition.y); + const ::XCEngine::Math::Vector3 forward = data.cameraForward.Normalized(); + + const bool lookingTowardGrid = + (data.cameraPosition.y >= 0.0f && forward.y < 0.0f) || + (data.cameraPosition.y < 0.0f && forward.y > 0.0f); + if (!lookingTowardGrid) { + return cameraHeight; + } + + const float verticalViewComponent = + (std::max)(std::abs(forward.y), kMinimumVerticalViewComponent); + return cameraHeight / verticalViewComponent; +} + +::XCEngine::Math::Matrix4x4 BuildInfiniteGridViewMatrix( + const SceneViewportGridPassData& data) { + const ::XCEngine::Math::Vector3 right = data.cameraRight.Normalized(); + const ::XCEngine::Math::Vector3 up = data.cameraUp.Normalized(); + const ::XCEngine::Math::Vector3 forward = data.cameraForward.Normalized(); + + ::XCEngine::Math::Matrix4x4 view = + ::XCEngine::Math::Matrix4x4::Identity(); + view.m[0][0] = right.x; + view.m[0][1] = right.y; + view.m[0][2] = right.z; + view.m[0][3] = + -::XCEngine::Math::Vector3::Dot(right, data.cameraPosition); + + view.m[1][0] = up.x; + view.m[1][1] = up.y; + view.m[1][2] = up.z; + view.m[1][3] = -::XCEngine::Math::Vector3::Dot(up, data.cameraPosition); + + view.m[2][0] = forward.x; + view.m[2][1] = forward.y; + view.m[2][2] = forward.z; + view.m[2][3] = + -::XCEngine::Math::Vector3::Dot(forward, data.cameraPosition); + return view; +} + +::XCEngine::Math::Matrix4x4 BuildInfiniteGridProjectionMatrix( + const SceneViewportGridPassData& data, + float viewportWidth, + float viewportHeight) { + const float aspect = viewportHeight > 0.0f + ? viewportWidth / viewportHeight + : 1.0f; + return ::XCEngine::Math::Matrix4x4::Perspective( + data.verticalFovDegrees * ::XCEngine::Math::DEG_TO_RAD, + aspect, + data.nearClipPlane, + data.farClipPlane); +} + +class SceneViewportGridPass final : public ::XCEngine::Rendering::RenderPass { +public: + SceneViewportGridPass( + SceneViewportGridPassRenderer& renderer, + const SceneViewportGridPassData& data) + : m_renderer(renderer) + , m_data(data) { + } + + const char* GetName() const override { + return "SceneViewportGrid"; + } + + bool Execute( + const ::XCEngine::Rendering::RenderPassContext& context) override { + return m_renderer.Render( + context.renderContext, + context.surface, + m_data); + } + +private: + SceneViewportGridPassRenderer& m_renderer; + SceneViewportGridPassData m_data = {}; +}; + +} // namespace + +InfiniteGridParameters BuildInfiniteGridParameters( + const SceneViewportGridPassData& data) { + InfiniteGridParameters parameters = {}; + if (!data.valid) { + return parameters; + } + + const float cameraHeight = std::abs(data.cameraPosition.y); + const float viewDistance = ComputeViewDistanceToGridPlane(data); + const float targetSpacing = + (std::max)(cameraHeight * kCameraHeightScaleFactor, 0.1f); + + parameters.valid = true; + parameters.baseScale = SnapGridSpacing(targetSpacing); + parameters.transitionBlend = + ComputeTransitionBlend(targetSpacing, parameters.baseScale); + parameters.fadeDistance = + (std::max)(parameters.baseScale * 320.0f, viewDistance * 80.0f); + return parameters; +} + +class SceneViewportGridPassRenderer::Impl { +public: + Impl() + : m_shaderPath(GetSceneViewportInfiniteGridShaderPath()) { + } + + void Shutdown() { + DestroyResources(); + } + + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const SceneViewportGridPassData& data) { + if (!data.valid || !renderContext.IsValid()) { + return false; + } + + const std::vector<::XCEngine::RHI::RHIResourceView*>& colorAttachments = + surface.GetColorAttachments(); + if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + colorAttachments.empty() || + colorAttachments[0] == nullptr || + surface.GetDepthAttachment() == nullptr) { + return false; + } + + const ::XCEngine::Math::RectInt renderArea = surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + + if (!EnsureInitialized(renderContext, surface)) { + return false; + } + + const InfiniteGridParameters parameters = + BuildInfiniteGridParameters(data); + if (!parameters.valid) { + return false; + } + + const ::XCEngine::Math::Matrix4x4 viewProjection = + BuildInfiniteGridProjectionMatrix( + data, + static_cast(renderArea.width), + static_cast(renderArea.height)) * + BuildInfiniteGridViewMatrix(data); + + const float aspect = renderArea.height > 0 + ? static_cast(renderArea.width) / + static_cast(renderArea.height) + : 1.0f; + + GridConstants constants = {}; + constants.viewProjection = viewProjection.Transpose(); + constants.cameraPositionAndScale = + ::XCEngine::Math::Vector4( + data.cameraPosition, + parameters.baseScale); + constants.cameraRightAndFade = + ::XCEngine::Math::Vector4( + data.cameraRight, + parameters.fadeDistance); + constants.cameraUpAndTanHalfFov = ::XCEngine::Math::Vector4( + data.cameraUp, + std::tan( + data.verticalFovDegrees * + ::XCEngine::Math::DEG_TO_RAD * + 0.5f)); + constants.cameraForwardAndAspect = + ::XCEngine::Math::Vector4(data.cameraForward, aspect); + constants.viewportNearFar = ::XCEngine::Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + data.nearClipPlane, + data.farClipPlane); + constants.gridTransition = + ::XCEngine::Math::Vector4( + parameters.transitionBlend, + 0.0f, + 0.0f, + 0.0f); + + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + + ::XCEngine::RHI::RHICommandList* commandList = + renderContext.commandList; + ::XCEngine::RHI::RHIResourceView* renderTarget = colorAttachments[0]; + ::XCEngine::RHI::RHIResourceView* depthAttachment = + surface.GetDepthAttachment(); + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + surface.GetColorStateAfter(), + ::XCEngine::RHI::ResourceStates::RenderTarget); + commandList->TransitionBarrier( + depthAttachment, + surface.GetDepthStateAfter(), + ::XCEngine::RHI::ResourceStates::DepthWrite); + } + + commandList->SetRenderTargets(1, &renderTarget, depthAttachment); + + const ::XCEngine::RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const ::XCEngine::RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology( + ::XCEngine::RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + ::XCEngine::RHI::RHIDescriptorSet* descriptorSets[] = { + m_constantSet + }; + commandList->SetGraphicsDescriptorSets( + 0, + 1, + descriptorSets, + m_pipelineLayout); + commandList->Draw(3, 1, 0, 0); + commandList->EndRenderPass(); + + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + ::XCEngine::RHI::ResourceStates::RenderTarget, + surface.GetColorStateAfter()); + commandList->TransitionBarrier( + depthAttachment, + ::XCEngine::RHI::ResourceStates::DepthWrite, + surface.GetDepthStateAfter()); + } + + return true; + } + +private: + bool EnsureInitialized( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + const ::XCEngine::RHI::Format renderTargetFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u); + const ::XCEngine::RHI::Format depthFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface); + const std::uint32_t renderTargetSampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + if (m_pipelineState != nullptr && + m_pipelineLayout != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType && + m_renderTargetFormat == renderTargetFormat && + m_depthStencilFormat == depthFormat && + m_renderTargetSampleCount == renderTargetSampleCount) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext, surface); + } + + bool CreateResources( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + if (!renderContext.IsValid()) { + return false; + } + + if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u) == ::XCEngine::RHI::Format::Unknown || + ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface) == + ::XCEngine::RHI::Format::Unknown) { + return false; + } + + if (m_shaderPath.Empty()) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportGridPassRenderer requires a valid shader path"); + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + m_shader = ::XCEngine::Resources::ResourceManager::Get() + .Load<::XCEngine::Resources::Shader>(m_shaderPath); + if (!m_shader.IsValid()) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportGridPassRenderer failed to load infinite-grid shader"); + DestroyResources(); + return false; + } + + const ::XCEngine::Resources::ShaderBackend backend = + ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + const ::XCEngine::Resources::ShaderPass* infiniteGridPass = + FindInfiniteGridCompatiblePass(*m_shader.Get(), backend); + if (infiniteGridPass == nullptr) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportGridPassRenderer could not resolve InfiniteGrid pass"); + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast( + ::XCEngine::RHI::DescriptorType::CBV); + constantBinding.count = 1; + + ::XCEngine::RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + ::XCEngine::RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = + ::XCEngine::RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + const ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = + CreatePipelineDesc( + m_backendType, + m_pipelineLayout, + *m_shader.Get(), + infiniteGridPass->name, + surface); + m_pipelineState = m_device->CreatePipelineState(pipelineDesc); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + m_renderTargetFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u); + m_depthStencilFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface); + m_renderTargetSampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + return true; + } + + void DestroyResources() { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = ::XCEngine::RHI::RHIType::D3D12; + m_shader.Reset(); + m_renderTargetFormat = ::XCEngine::RHI::Format::Unknown; + m_depthStencilFormat = ::XCEngine::RHI::Format::Unknown; + m_renderTargetSampleCount = 1u; + } + + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = + ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_pipelineState = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_constantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_constantSet = nullptr; + ::XCEngine::Containers::String m_shaderPath = {}; + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> + m_shader = {}; + ::XCEngine::RHI::Format m_renderTargetFormat = + ::XCEngine::RHI::Format::Unknown; + ::XCEngine::RHI::Format m_depthStencilFormat = + ::XCEngine::RHI::Format::Unknown; + std::uint32_t m_renderTargetSampleCount = 1u; +}; + +SceneViewportGridPassRenderer::SceneViewportGridPassRenderer() + : m_impl(std::make_unique()) { +} + +SceneViewportGridPassRenderer::~SceneViewportGridPassRenderer() = default; + +void SceneViewportGridPassRenderer::Shutdown() { + m_impl->Shutdown(); +} + +bool SceneViewportGridPassRenderer::Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const SceneViewportGridPassData& data) { + return m_impl->Render(renderContext, surface, data); +} + +std::unique_ptr<::XCEngine::Rendering::RenderPass> CreateSceneViewportGridPass( + SceneViewportGridPassRenderer& renderer, + const SceneViewportGridPassData& data) { + return std::make_unique(renderer, data); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.h b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.h new file mode 100644 index 00000000..4047d1c3 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportGridPass.h @@ -0,0 +1,38 @@ +#pragma once + +#include "Rendering/Viewport/SceneViewportPassSpecs.h" + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class SceneViewportGridPassRenderer { +public: + SceneViewportGridPassRenderer(); + ~SceneViewportGridPassRenderer(); + SceneViewportGridPassRenderer(const SceneViewportGridPassRenderer&) = delete; + SceneViewportGridPassRenderer& operator=(const SceneViewportGridPassRenderer&) = delete; + SceneViewportGridPassRenderer(SceneViewportGridPassRenderer&&) = delete; + SceneViewportGridPassRenderer& operator=(SceneViewportGridPassRenderer&&) = delete; + + void Shutdown(); + + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const SceneViewportGridPassData& data); + +private: + class Impl; + std::unique_ptr m_impl = {}; +}; + +std::unique_ptr<::XCEngine::Rendering::RenderPass> CreateSceneViewportGridPass( + SceneViewportGridPassRenderer& renderer, + const SceneViewportGridPassData& data); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp new file mode 100644 index 00000000..ffce755b --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp @@ -0,0 +1,746 @@ +#include "Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h" + +#include "Rendering/Viewport/ViewportRenderTargets.h" + +#include "Rendering/Internal/RenderSurfacePipelineUtils.h" +#include "Rendering/Internal/ShaderVariantUtils.h" +#include "Rendering/Materials/RenderMaterialStateUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +class SceneViewportSelectionMaskPass final + : public ::XCEngine::Rendering::Passes::BuiltinDepthStylePassBase { +public: + SceneViewportSelectionMaskPass() + : BuiltinDepthStylePassBase( + ::XCEngine::Rendering::BuiltinMaterialPass::SelectionMask, + ::XCEngine::Resources::GetBuiltinSelectionMaskShaderPath()) { + } + + const char* GetName() const override { + return "SceneViewportSelectionMaskPass"; + } + + bool Render( + const ::XCEngine::Rendering::RenderContext& context, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::Rendering::RenderSceneData& sceneData, + const std::vector& selectedObjectIds) { + m_selectedObjectIds.clear(); + m_selectedObjectIds.reserve(selectedObjectIds.size()); + for (const std::uint64_t selectedObjectId : selectedObjectIds) { + ::XCEngine::Rendering::RenderObjectId renderObjectId = + ::XCEngine::Rendering::kInvalidRenderObjectId; + if (::XCEngine::Rendering::TryConvertRuntimeObjectIdToRenderObjectId( + selectedObjectId, + renderObjectId)) { + m_selectedObjectIds.push_back(renderObjectId); + } + } + + if (m_selectedObjectIds.empty()) { + return false; + } + + ::XCEngine::Rendering::RenderSceneData selectionMaskSceneData = sceneData; + selectionMaskSceneData.cameraData.clearFlags = + ::XCEngine::Rendering::RenderClearFlags::Color; + selectionMaskSceneData.cameraData.clearColor = + ::XCEngine::Math::Color::Black(); + + const ::XCEngine::Rendering::RenderPassContext passContext = { + context, + surface, + selectionMaskSceneData, + nullptr, + nullptr, + ::XCEngine::RHI::ResourceStates::Common + }; + return Execute(passContext); + } + +protected: + bool ShouldRenderVisibleItem( + const ::XCEngine::Rendering::VisibleRenderItem& visibleItem) const override { + if (!::XCEngine::Rendering::IsValidRenderObjectId( + visibleItem.renderObjectId)) { + return false; + } + + return std::find( + m_selectedObjectIds.begin(), + m_selectedObjectIds.end(), + visibleItem.renderObjectId) != m_selectedObjectIds.end(); + } + +private: + std::vector<::XCEngine::Rendering::RenderObjectId> m_selectedObjectIds = {}; +}; + +const ::XCEngine::Resources::ShaderPass* FindSelectionOutlineCompatiblePass( + const ::XCEngine::Resources::Shader& shader, + ::XCEngine::Resources::ShaderBackend backend) { + const ::XCEngine::Resources::ShaderPass* outlinePass = + shader.FindPass("SelectionOutline"); + if (outlinePass != nullptr && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + outlinePass->name, + backend)) { + return outlinePass; + } + + const ::XCEngine::Resources::ShaderPass* editorOutlinePass = + shader.FindPass("EditorSelectionOutline"); + if (editorOutlinePass != nullptr && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + editorOutlinePass->name, + backend)) { + return editorOutlinePass; + } + + if (shader.GetPassCount() > 0 && + ::XCEngine::Rendering::Internal::ShaderPassHasGraphicsVariants( + shader, + shader.GetPasses()[0].name, + backend)) { + return &shader.GetPasses()[0]; + } + + return nullptr; +} + +::XCEngine::RHI::GraphicsPipelineDesc CreatePipelineDesc( + ::XCEngine::RHI::RHIType backendType, + ::XCEngine::RHI::RHIPipelineLayout* pipelineLayout, + const ::XCEngine::Resources::Shader& shader, + const ::XCEngine::Containers::String& passName, + const ::XCEngine::Rendering::RenderSurface& surface) { + ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = + static_cast( + ::XCEngine::RHI::PrimitiveTopologyType::Triangle); + ::XCEngine::Rendering::Internal:: + ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + surface, + pipelineDesc); + pipelineDesc.depthStencilFormat = + static_cast(::XCEngine::RHI::Format::Unknown); + + const ::XCEngine::Resources::ShaderPass* shaderPass = + shader.FindPass(passName); + if (shaderPass != nullptr && shaderPass->hasFixedFunctionState) { + ::XCEngine::Rendering::ApplyRenderState( + shaderPass->fixedFunctionState, + pipelineDesc); + } else { + ::XCEngine::Resources::MaterialRenderState fallbackState = {}; + fallbackState.cullMode = + ::XCEngine::Resources::MaterialCullMode::None; + fallbackState.depthWriteEnable = false; + fallbackState.depthTestEnable = false; + fallbackState.depthFunc = + ::XCEngine::Resources::MaterialComparisonFunc::Always; + fallbackState.blendEnable = true; + fallbackState.srcBlend = + ::XCEngine::Resources::MaterialBlendFactor::SrcAlpha; + fallbackState.dstBlend = + ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha; + fallbackState.srcBlendAlpha = + ::XCEngine::Resources::MaterialBlendFactor::One; + fallbackState.dstBlendAlpha = + ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha; + fallbackState.blendOp = + ::XCEngine::Resources::MaterialBlendOp::Add; + fallbackState.blendOpAlpha = + ::XCEngine::Resources::MaterialBlendOp::Add; + fallbackState.colorWriteMask = static_cast( + ::XCEngine::RHI::ColorWriteMask::All); + ::XCEngine::Rendering::ApplyRenderState( + fallbackState, + pipelineDesc); + } + + const ::XCEngine::Resources::ShaderBackend backend = + ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + if (const ::XCEngine::Resources::ShaderStageVariant* vertexVariant = + shader.FindVariant( + passName, + ::XCEngine::Resources::ShaderType::Vertex, + backend)) { + if (shaderPass != nullptr) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + *shaderPass, + backend, + *vertexVariant, + pipelineDesc.vertexShader); + } + } + if (const ::XCEngine::Resources::ShaderStageVariant* fragmentVariant = + shader.FindVariant( + passName, + ::XCEngine::Resources::ShaderType::Fragment, + backend)) { + if (shaderPass != nullptr) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + *shaderPass, + backend, + *fragmentVariant, + pipelineDesc.fragmentShader); + } + } + + return pipelineDesc; +} + +class SceneViewportSelectionOutlinePass final + : public ::XCEngine::Rendering::RenderPass { +public: + SceneViewportSelectionOutlinePass( + SceneViewportSelectionOutlinePassRenderer& renderer, + ViewportRenderTargets* targets, + std::vector selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) + : m_renderer(renderer) + , m_targets(targets) + , m_selectedObjectIds(std::move(selectedObjectIds)) + , m_style(style) { + } + + const char* GetName() const override { + return "SceneViewportSelectionOutline"; + } + + bool Execute( + const ::XCEngine::Rendering::RenderPassContext& context) override { + return m_renderer.Render( + context.renderContext, + context.surface, + context.sceneData, + *m_targets, + m_selectedObjectIds, + m_style); + } + +private: + SceneViewportSelectionOutlinePassRenderer& m_renderer; + ViewportRenderTargets* m_targets = nullptr; + std::vector m_selectedObjectIds = {}; + SceneViewportSelectionOutlineStyle m_style = {}; +}; + +} // namespace + +class SceneViewportSelectionOutlinePassRenderer::Impl { +public: + struct OutlineConstants { + ::XCEngine::Math::Vector4 viewportSizeAndTexelSize = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 outlineColor = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 outlineInfo = + ::XCEngine::Math::Vector4::Zero(); + ::XCEngine::Math::Vector4 depthParams = + ::XCEngine::Math::Vector4::Zero(); + }; + + Impl() + : m_selectionMaskPass(std::make_unique()) + , m_shaderPath( + ::XCEngine::Resources::GetBuiltinSelectionOutlineShaderPath()) { + } + + void Shutdown() { + if (m_selectionMaskPass != nullptr) { + m_selectionMaskPass->Shutdown(); + } + DestroyResources(); + } + + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::Rendering::RenderSceneData& sceneData, + ViewportRenderTargets& targets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) { + ::XCEngine::Rendering::RenderSurface selectionMaskSurface = + BuildViewportSelectionMaskSurface(targets); + selectionMaskSurface.SetRenderArea(surface.GetRenderArea()); + + if (!m_selectionMaskPass->Render( + renderContext, + selectionMaskSurface, + sceneData, + selectedObjectIds)) { + return false; + } + + targets.selectionMaskState = + ::XCEngine::RHI::ResourceStates::PixelShaderResource; + return RenderOutline( + renderContext, + surface, + targets.selectionMaskShaderView, + targets.selectionMaskState, + targets.depthShaderView, + surface.GetDepthStateAfter(), + style); + } + +private: + bool RenderOutline( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + ::XCEngine::RHI::RHIResourceView* selectionMaskTextureView, + ::XCEngine::RHI::ResourceStates selectionMaskState, + ::XCEngine::RHI::RHIResourceView* depthTextureView, + ::XCEngine::RHI::ResourceStates depthTextureState, + const SceneViewportSelectionOutlineStyle& style) { + if (!renderContext.IsValid() || + selectionMaskTextureView == nullptr || + depthTextureView == nullptr) { + return false; + } + + const std::vector<::XCEngine::RHI::RHIResourceView*>& colorAttachments = + surface.GetColorAttachments(); + if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + colorAttachments.empty() || + colorAttachments[0] == nullptr) { + return false; + } + + const ::XCEngine::Math::RectInt renderArea = surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + + if (!EnsureInitialized(renderContext, surface)) { + return false; + } + + OutlineConstants constants = {}; + constants.viewportSizeAndTexelSize = ::XCEngine::Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + surface.GetWidth() > 0 + ? 1.0f / static_cast(surface.GetWidth()) + : 0.0f, + surface.GetHeight() > 0 + ? 1.0f / static_cast(surface.GetHeight()) + : 0.0f); + constants.outlineColor = ::XCEngine::Math::Vector4( + style.outlineColor.r, + style.outlineColor.g, + style.outlineColor.b, + style.outlineColor.a); + constants.outlineInfo = ::XCEngine::Math::Vector4( + style.debugSelectionMask ? 1.0f : 0.0f, + style.outlineWidthPixels, + 0.0f, + 0.0f); + constants.depthParams = + ::XCEngine::Math::Vector4(1.0e-5f, 0.0f, 0.0f, 0.0f); + + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + m_textureSet->Update(0, selectionMaskTextureView); + m_textureSet->Update(1, depthTextureView); + + ::XCEngine::RHI::RHICommandList* commandList = + renderContext.commandList; + ::XCEngine::RHI::RHIResourceView* renderTarget = colorAttachments[0]; + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + surface.GetColorStateAfter(), + ::XCEngine::RHI::ResourceStates::RenderTarget); + commandList->TransitionBarrier( + selectionMaskTextureView, + selectionMaskState, + ::XCEngine::RHI::ResourceStates::PixelShaderResource); + commandList->TransitionBarrier( + depthTextureView, + depthTextureState, + ::XCEngine::RHI::ResourceStates::PixelShaderResource); + } + commandList->SetRenderTargets(1, &renderTarget, nullptr); + + const ::XCEngine::RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const ::XCEngine::RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology( + ::XCEngine::RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + ::XCEngine::RHI::RHIDescriptorSet* descriptorSets[] = { + m_constantSet, + m_textureSet + }; + commandList->SetGraphicsDescriptorSets( + 0, + 2, + descriptorSets, + m_pipelineLayout); + commandList->Draw(3, 1, 0, 0); + commandList->EndRenderPass(); + + if (surface.IsAutoTransitionEnabled()) { + commandList->TransitionBarrier( + renderTarget, + ::XCEngine::RHI::ResourceStates::RenderTarget, + surface.GetColorStateAfter()); + commandList->TransitionBarrier( + selectionMaskTextureView, + ::XCEngine::RHI::ResourceStates::PixelShaderResource, + selectionMaskState); + commandList->TransitionBarrier( + depthTextureView, + ::XCEngine::RHI::ResourceStates::PixelShaderResource, + depthTextureState); + } + return true; + } + + bool EnsureInitialized( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + const ::XCEngine::RHI::Format renderTargetFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u); + const std::uint32_t renderTargetSampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount( + surface); + if (m_pipelineLayout != nullptr && + m_pipelineState != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_texturePool != nullptr && + m_textureSet != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType && + m_renderTargetFormat == renderTargetFormat && + m_renderTargetSampleCount == renderTargetSampleCount) { + return true; + } + + if (HasCreatedResources()) { + DestroyResources(); + } + return CreateResources(renderContext, surface); + } + + bool CreateResources( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface) { + if (!renderContext.IsValid()) { + return false; + } + + if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) || + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u) == ::XCEngine::RHI::Format::Unknown) { + return false; + } + + if (m_shaderPath.Empty()) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportSelectionOutlinePassRenderer requires a valid shader path"); + return false; + } + + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> + shader = + ::XCEngine::Resources::ResourceManager::Get() + .Load<::XCEngine::Resources::Shader>(m_shaderPath); + if (!shader.IsValid()) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportSelectionOutlinePassRenderer failed to load selection-outline shader"); + ResetState(); + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + m_shader.emplace(std::move(shader)); + + const ::XCEngine::Resources::ShaderBackend backend = + ::XCEngine::Rendering::Internal::ToShaderBackend(m_backendType); + const ::XCEngine::Resources::ShaderPass* outlinePass = + FindSelectionOutlineCompatiblePass(*m_shader->Get(), backend); + if (outlinePass == nullptr) { + ::XCEngine::Debug::Logger::Get().Error( + ::XCEngine::Debug::LogCategory::Rendering, + "SceneViewportSelectionOutlinePassRenderer could not resolve SelectionOutline pass"); + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = + static_cast(::XCEngine::RHI::DescriptorType::CBV); + constantBinding.count = 1; + + ::XCEngine::RHI::DescriptorSetLayoutBinding textureBindings[2] = {}; + textureBindings[0].binding = 0; + textureBindings[0].type = + static_cast(::XCEngine::RHI::DescriptorType::SRV); + textureBindings[0].count = 1; + textureBindings[1].binding = 1; + textureBindings[1].type = + static_cast(::XCEngine::RHI::DescriptorType::SRV); + textureBindings[1].count = 1; + + ::XCEngine::RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + ::XCEngine::RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = textureBindings; + textureLayout.bindingCount = 2; + + ::XCEngine::RHI::DescriptorSetLayoutDesc setLayouts[2] = {}; + setLayouts[0] = constantLayout; + setLayouts[1] = textureLayout; + + ::XCEngine::RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = 2; + m_pipelineLayout = + m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = + ::XCEngine::RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + ::XCEngine::RHI::DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = + ::XCEngine::RHI::DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 2; + texturePoolDesc.shaderVisible = true; + m_texturePool = m_device->CreateDescriptorPool(texturePoolDesc); + if (m_texturePool == nullptr) { + DestroyResources(); + return false; + } + + m_textureSet = m_texturePool->AllocateSet(textureLayout); + if (m_textureSet == nullptr) { + DestroyResources(); + return false; + } + + m_pipelineState = m_device->CreatePipelineState( + CreatePipelineDesc( + m_backendType, + m_pipelineLayout, + *m_shader->Get(), + outlinePass->name, + surface)); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + m_renderTargetFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat( + surface, + 0u); + m_renderTargetSampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount( + surface); + return true; + } + + bool HasCreatedResources() const { + return m_device != nullptr || + m_pipelineLayout != nullptr || + m_pipelineState != nullptr || + m_constantPool != nullptr || + m_constantSet != nullptr || + m_texturePool != nullptr || + m_textureSet != nullptr || + m_shader.has_value(); + } + + void DestroyResources() { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_textureSet != nullptr) { + m_textureSet->Shutdown(); + delete m_textureSet; + m_textureSet = nullptr; + } + + if (m_texturePool != nullptr) { + m_texturePool->Shutdown(); + delete m_texturePool; + m_texturePool = nullptr; + } + + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + if (m_shader.has_value()) { + m_shader.reset(); + } + + ResetState(); + } + + void ResetState() { + m_device = nullptr; + m_backendType = ::XCEngine::RHI::RHIType::D3D12; + m_pipelineLayout = nullptr; + m_pipelineState = nullptr; + m_constantPool = nullptr; + m_constantSet = nullptr; + m_texturePool = nullptr; + m_textureSet = nullptr; + m_shader.reset(); + m_renderTargetFormat = ::XCEngine::RHI::Format::Unknown; + m_renderTargetSampleCount = 1u; + } + + std::unique_ptr m_selectionMaskPass = {}; + ::XCEngine::RHI::RHIDevice* m_device = nullptr; + ::XCEngine::RHI::RHIType m_backendType = + ::XCEngine::RHI::RHIType::D3D12; + ::XCEngine::RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_pipelineState = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_constantPool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_constantSet = nullptr; + ::XCEngine::RHI::RHIDescriptorPool* m_texturePool = nullptr; + ::XCEngine::RHI::RHIDescriptorSet* m_textureSet = nullptr; + ::XCEngine::Containers::String m_shaderPath = {}; + std::optional< + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>> + m_shader = {}; + ::XCEngine::RHI::Format m_renderTargetFormat = + ::XCEngine::RHI::Format::Unknown; + std::uint32_t m_renderTargetSampleCount = 1u; +}; + +SceneViewportSelectionOutlinePassRenderer:: + SceneViewportSelectionOutlinePassRenderer() + : m_impl(std::make_unique()) { +} + +SceneViewportSelectionOutlinePassRenderer:: + ~SceneViewportSelectionOutlinePassRenderer() = default; + +void SceneViewportSelectionOutlinePassRenderer::Shutdown() { + m_impl->Shutdown(); +} + +bool SceneViewportSelectionOutlinePassRenderer::Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::Rendering::RenderSceneData& sceneData, + ViewportRenderTargets& targets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) { + return m_impl->Render( + renderContext, + surface, + sceneData, + targets, + selectedObjectIds, + style); +} + +std::unique_ptr<::XCEngine::Rendering::RenderPass> +CreateSceneViewportSelectionOutlinePass( + SceneViewportSelectionOutlinePassRenderer& renderer, + ViewportRenderTargets* targets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) { + return std::make_unique( + renderer, + targets, + selectedObjectIds, + style); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h new file mode 100644 index 00000000..e2fcc060 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h @@ -0,0 +1,52 @@ +#pragma once + +#include "Rendering/Viewport/SceneViewportPassSpecs.h" + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct ViewportRenderTargets; + +class SceneViewportSelectionOutlinePassRenderer { +public: + SceneViewportSelectionOutlinePassRenderer(); + ~SceneViewportSelectionOutlinePassRenderer(); + SceneViewportSelectionOutlinePassRenderer( + const SceneViewportSelectionOutlinePassRenderer&) = delete; + SceneViewportSelectionOutlinePassRenderer& operator=( + const SceneViewportSelectionOutlinePassRenderer&) = delete; + SceneViewportSelectionOutlinePassRenderer( + SceneViewportSelectionOutlinePassRenderer&&) = delete; + SceneViewportSelectionOutlinePassRenderer& operator=( + SceneViewportSelectionOutlinePassRenderer&&) = delete; + + void Shutdown(); + + bool Render( + const ::XCEngine::Rendering::RenderContext& renderContext, + const ::XCEngine::Rendering::RenderSurface& surface, + const ::XCEngine::Rendering::RenderSceneData& sceneData, + ViewportRenderTargets& targets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style); + +private: + class Impl; + std::unique_ptr m_impl = {}; +}; + +std::unique_ptr<::XCEngine::Rendering::RenderPass> +CreateSceneViewportSelectionOutlinePass( + SceneViewportSelectionOutlinePassRenderer& renderer, + ViewportRenderTargets* targets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/RenderTargetManager/Cleanup.cpp b/new_editor/app/Rendering/Viewport/RenderTargetManager/Cleanup.cpp deleted file mode 100644 index 31d2b0bc..00000000 --- a/new_editor/app/Rendering/Viewport/RenderTargetManager/Cleanup.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "Rendering/Viewport/ViewportRenderTargets.h" - -namespace XCEngine::UI::Editor::App { - -namespace { - -template -void ShutdownAndDeleteViewportResource(ResourceType*& resource) { - if (resource == nullptr) { - return; - } - - resource->Shutdown(); - delete resource; - resource = nullptr; -} - -void ResetViewportTargetMetadata(ViewportRenderTargets& targets) { - targets.width = 0; - targets.height = 0; - targets.srvCpuHandle = {}; - targets.srvGpuHandle = {}; - targets.textureHandle = {}; - targets.colorState = ::XCEngine::RHI::ResourceStates::Common; - targets.objectIdState = ::XCEngine::RHI::ResourceStates::Common; - targets.selectionMaskState = ::XCEngine::RHI::ResourceStates::Common; - targets.hasValidObjectIdFrame = false; -} - -} // namespace - -void ViewportRenderTargetManager::DestroyTargets( - Host::D3D12ShaderResourceDescriptorAllocator* textureDescriptorAllocator, - ViewportRenderTargets& targets) const { - if (textureDescriptorAllocator != nullptr && targets.srvCpuHandle.ptr != 0) { - textureDescriptorAllocator->Free(targets.srvCpuHandle, targets.srvGpuHandle); - } - - ShutdownAndDeleteViewportResource(targets.objectIdView); - ShutdownAndDeleteViewportResource(targets.objectIdShaderView); - ShutdownAndDeleteViewportResource(targets.objectIdDepthView); - ShutdownAndDeleteViewportResource(targets.objectIdDepthTexture); - ShutdownAndDeleteViewportResource(targets.objectIdTexture); - ShutdownAndDeleteViewportResource(targets.selectionMaskView); - ShutdownAndDeleteViewportResource(targets.selectionMaskShaderView); - ShutdownAndDeleteViewportResource(targets.selectionMaskTexture); - ShutdownAndDeleteViewportResource(targets.depthShaderView); - ShutdownAndDeleteViewportResource(targets.depthView); - ShutdownAndDeleteViewportResource(targets.depthTexture); - ShutdownAndDeleteViewportResource(targets.colorView); - ShutdownAndDeleteViewportResource(targets.colorTexture); - - ResetViewportTargetMetadata(targets); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/RenderTargetManager/Surfaces.cpp b/new_editor/app/Rendering/Viewport/RenderTargetManager/Surfaces.cpp deleted file mode 100644 index 64cfd0d8..00000000 --- a/new_editor/app/Rendering/Viewport/RenderTargetManager/Surfaces.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "Rendering/Viewport/ViewportRenderTargets.h" - -namespace XCEngine::UI::Editor::App { - -ViewportResourceReuseQuery BuildViewportRenderTargetsReuseQuery( - ViewportKind kind, - const ViewportRenderTargets& targets, - std::uint32_t requestedWidth, - std::uint32_t requestedHeight) { - ViewportResourceReuseQuery query = {}; - query.kind = kind; - query.width = targets.width; - query.height = targets.height; - query.requestedWidth = requestedWidth; - query.requestedHeight = requestedHeight; - query.resources.hasColorTexture = targets.colorTexture != nullptr; - query.resources.hasColorView = targets.colorView != nullptr; - query.resources.hasDepthTexture = targets.depthTexture != nullptr; - query.resources.hasDepthView = targets.depthView != nullptr; - query.resources.hasDepthShaderView = targets.depthShaderView != nullptr; - query.resources.hasObjectIdTexture = targets.objectIdTexture != nullptr; - query.resources.hasObjectIdDepthTexture = targets.objectIdDepthTexture != nullptr; - query.resources.hasObjectIdDepthView = targets.objectIdDepthView != nullptr; - query.resources.hasObjectIdView = targets.objectIdView != nullptr; - query.resources.hasObjectIdShaderView = targets.objectIdShaderView != nullptr; - query.resources.hasSelectionMaskTexture = targets.selectionMaskTexture != nullptr; - query.resources.hasSelectionMaskView = targets.selectionMaskView != nullptr; - query.resources.hasSelectionMaskShaderView = targets.selectionMaskShaderView != nullptr; - query.resources.hasTextureDescriptor = targets.textureHandle.IsValid(); - return query; -} - -::XCEngine::Rendering::RenderSurface BuildViewportColorSurface( - const ViewportRenderTargets& targets) { - return BuildViewportRenderSurface( - targets.width, - targets.height, - targets.colorView, - targets.depthView, - targets.colorState); -} - -::XCEngine::Rendering::RenderSurface BuildViewportObjectIdSurface( - const ViewportRenderTargets& targets) { - return BuildViewportRenderSurface( - targets.width, - targets.height, - targets.objectIdView, - targets.objectIdDepthView, - targets.objectIdState); -} - -::XCEngine::Rendering::RenderSurface BuildViewportSelectionMaskSurface( - const ViewportRenderTargets& targets) { - return BuildViewportRenderSurface( - targets.width, - targets.height, - targets.selectionMaskView, - targets.depthView, - targets.selectionMaskState); -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportPassSpecs.h b/new_editor/app/Rendering/Viewport/SceneViewportPassSpecs.h new file mode 100644 index 00000000..ef130471 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/SceneViewportPassSpecs.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct SceneViewportGridPassData { + bool valid = false; + ::XCEngine::Math::Vector3 cameraPosition = ::XCEngine::Math::Vector3::Zero(); + ::XCEngine::Math::Vector3 cameraForward = ::XCEngine::Math::Vector3::Forward(); + ::XCEngine::Math::Vector3 cameraRight = ::XCEngine::Math::Vector3::Right(); + ::XCEngine::Math::Vector3 cameraUp = ::XCEngine::Math::Vector3::Up(); + float verticalFovDegrees = 60.0f; + float nearClipPlane = 0.03f; + float farClipPlane = 2000.0f; + float orbitDistance = 6.0f; +}; + +struct InfiniteGridParameters { + bool valid = false; + float baseScale = 1.0f; + float transitionBlend = 0.0f; + float fadeDistance = 500.0f; +}; + +InfiniteGridParameters BuildInfiniteGridParameters( + const SceneViewportGridPassData& data); + +struct SceneViewportSelectionOutlineStyle { + ::XCEngine::Math::Color outlineColor = ::XCEngine::Math::Color(1.0f, 0.4f, 0.0f, 1.0f); + float outlineWidthPixels = 2.0f; + bool debugSelectionMask = false; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp new file mode 100644 index 00000000..26382a2d --- /dev/null +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp @@ -0,0 +1,32 @@ +#include "Rendering/Viewport/SceneViewportRenderPassBundle.h" + +namespace XCEngine::UI::Editor::App { + +void SceneViewportRenderPassBundle::Shutdown() { + m_gridRenderer.Shutdown(); + m_selectionOutlineRenderer.Shutdown(); +} + +SceneViewportRenderPlanBuildResult +SceneViewportRenderPassBundle::BuildRenderPlan( + ViewportRenderTargets& targets, + const SceneViewportRenderRequest& request) { + return BuildSceneViewportRenderPlan( + targets, + request, + [this](const SceneViewportGridPassData& data) { + return CreateSceneViewportGridPass(m_gridRenderer, data); + }, + [this]( + ViewportRenderTargets* outlineTargets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) { + return CreateSceneViewportSelectionOutlinePass( + m_selectionOutlineRenderer, + outlineTargets, + selectedObjectIds, + style); + }); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h new file mode 100644 index 00000000..68caae5a --- /dev/null +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPassBundle.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Rendering/Viewport/Passes/SceneViewportGridPass.h" +#include "Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.h" +#include "Rendering/Viewport/SceneViewportRenderPlan.h" + +namespace XCEngine::UI::Editor::App { + +class SceneViewportRenderPassBundle { +public: + ~SceneViewportRenderPassBundle() = default; + + void Shutdown(); + + SceneViewportRenderPlanBuildResult BuildRenderPlan( + ViewportRenderTargets& targets, + const SceneViewportRenderRequest& request); + +private: + SceneViewportGridPassRenderer m_gridRenderer = {}; + SceneViewportSelectionOutlinePassRenderer m_selectionOutlineRenderer = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h b/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h new file mode 100644 index 00000000..fb6ada57 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/SceneViewportRenderPlan.h @@ -0,0 +1,167 @@ +#pragma once + +#include "Rendering/Viewport/SceneViewportPassSpecs.h" +#include "Rendering/Viewport/ViewportRenderTargets.h" +#include "Scene/EditorSceneRuntime.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct SceneViewportRenderPlan { + ::XCEngine::Rendering::RenderPassSequence postScenePasses = {}; + bool usesGridPass = false; + bool usesSelectionOutline = false; + bool hasClearColorOverride = true; + ::XCEngine::Math::Color clearColorOverride = + ::XCEngine::Math::Color(0.27f, 0.27f, 0.27f, 1.0f); + + bool HasPostScenePasses() const { + return postScenePasses.GetPassCount() > 0u; + } +}; + +using SceneViewportGridPassFactory = std::function< + std::unique_ptr<::XCEngine::Rendering::RenderPass>( + const SceneViewportGridPassData&)>; +using SceneViewportSelectionOutlinePassFactory = std::function< + std::unique_ptr<::XCEngine::Rendering::RenderPass>( + ViewportRenderTargets*, + const std::vector&, + const SceneViewportSelectionOutlineStyle&)>; + +struct SceneViewportRenderPlanBuildResult { + SceneViewportRenderPlan plan = {}; + const char* warningStatusText = nullptr; +}; + +inline SceneViewportSelectionOutlineStyle BuildSceneViewportSelectionOutlineStyle( + bool debugSelectionMask = false) { + SceneViewportSelectionOutlineStyle style = {}; + style.debugSelectionMask = debugSelectionMask; + return style; +} + +inline bool CanRenderSceneViewportSelectionOutline( + const ViewportRenderTargets& targets) { + return targets.selectionMaskView != nullptr && + targets.selectionMaskShaderView != nullptr && + targets.depthView != nullptr && + targets.depthShaderView != nullptr; +} + +inline SceneViewportGridPassData BuildSceneViewportGridPassData( + const SceneViewportRenderRequest& request) { + SceneViewportGridPassData data = {}; + if (request.camera == nullptr || + request.camera->GetGameObject() == nullptr || + request.camera->GetGameObject()->GetTransform() == nullptr) { + return data; + } + + const auto* transform = request.camera->GetGameObject()->GetTransform(); + data.valid = true; + data.cameraPosition = transform->GetPosition(); + data.cameraForward = transform->GetForward(); + data.cameraRight = transform->GetRight(); + data.cameraUp = transform->GetUp(); + data.verticalFovDegrees = request.camera->GetFieldOfView(); + data.nearClipPlane = request.camera->GetNearClipPlane(); + data.farClipPlane = request.camera->GetFarClipPlane(); + data.orbitDistance = request.orbitDistance; + return data; +} + +inline SceneViewportRenderPlanBuildResult BuildSceneViewportRenderPlan( + ViewportRenderTargets& targets, + const SceneViewportRenderRequest& request, + const SceneViewportGridPassFactory& gridPassFactory, + const SceneViewportSelectionOutlinePassFactory& selectionOutlinePassFactory) { + SceneViewportRenderPlanBuildResult result = {}; + const SceneViewportGridPassData gridPassData = + BuildSceneViewportGridPassData(request); + if (gridPassData.valid && gridPassFactory != nullptr) { + std::unique_ptr<::XCEngine::Rendering::RenderPass> gridPass = + gridPassFactory(gridPassData); + if (gridPass != nullptr) { + result.plan.postScenePasses.AddPass(std::move(gridPass)); + result.plan.usesGridPass = true; + } + } + + if (request.selectedObjectIds.empty()) { + return result; + } + + if (!CanRenderSceneViewportSelectionOutline(targets) || + selectionOutlinePassFactory == nullptr) { + result.warningStatusText = "Scene selection outline resources are unavailable"; + return result; + } + + std::unique_ptr<::XCEngine::Rendering::RenderPass> selectionOutlinePass = + selectionOutlinePassFactory( + &targets, + request.selectedObjectIds, + BuildSceneViewportSelectionOutlineStyle(request.debugSelectionMask)); + if (selectionOutlinePass != nullptr) { + result.plan.postScenePasses.AddPass(std::move(selectionOutlinePass)); + result.plan.usesSelectionOutline = true; + return result; + } + + result.warningStatusText = "Scene selection outline pass creation failed"; + return result; +} + +inline void ApplySceneViewportRenderPlan( + const ViewportRenderTargets& targets, + SceneViewportRenderPlan& plan, + ::XCEngine::Rendering::CameraFramePlan& framePlan) { + framePlan.preScenePasses = nullptr; + framePlan.postScenePasses = nullptr; + framePlan.overlayPasses = nullptr; + framePlan.request.objectId = {}; + + if (plan.HasPostScenePasses()) { + framePlan.postScenePasses = &plan.postScenePasses; + } + + if (targets.objectIdView == nullptr || targets.objectIdDepthView == nullptr) { + framePlan.request.hasClearColorOverride = plan.hasClearColorOverride; + framePlan.request.clearColorOverride = plan.clearColorOverride; + return; + } + + framePlan.request.objectId.surface = BuildViewportObjectIdSurface(targets); + framePlan.request.objectId.surface.SetRenderArea( + framePlan.request.surface.GetRenderArea()); + framePlan.request.hasClearColorOverride = plan.hasClearColorOverride; + framePlan.request.clearColorOverride = plan.clearColorOverride; +} + +inline void MarkSceneViewportRenderSuccess( + ViewportRenderTargets& targets, + const SceneViewportRenderPlan& plan, + const ::XCEngine::Rendering::CameraFramePlan& framePlan) { + targets.colorState = ::XCEngine::RHI::ResourceStates::PixelShaderResource; + targets.objectIdState = + framePlan.request.objectId.IsRequested() + ? ::XCEngine::RHI::ResourceStates::PixelShaderResource + : ::XCEngine::RHI::ResourceStates::Common; + targets.selectionMaskState = + plan.usesSelectionOutline + ? ::XCEngine::RHI::ResourceStates::PixelShaderResource + : ::XCEngine::RHI::ResourceStates::Common; + targets.hasValidObjectIdFrame = framePlan.request.objectId.IsRequested(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h b/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h new file mode 100644 index 00000000..002801a0 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/SceneViewportResourcePaths.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace Internal { + +inline ::XCEngine::Containers::String NormalizeSceneViewportResourcePath( + const std::filesystem::path& path) { + return ::XCEngine::Containers::String( + path.lexically_normal().generic_string().c_str()); +} + +inline std::filesystem::path GetSceneViewportResourceRepoRootPath() { +#ifdef XCUIEDITOR_REPO_ROOT + return std::filesystem::path(XCUIEDITOR_REPO_ROOT); +#else + return std::filesystem::path(__FILE__) + .parent_path() + .parent_path() + .parent_path() + .parent_path() + .parent_path(); +#endif +} + +inline ::XCEngine::Containers::String BuildSceneViewportEditorResourcePath( + const std::filesystem::path& relativePath) { + return NormalizeSceneViewportResourcePath( + GetSceneViewportResourceRepoRootPath() / + "editor" / + "resources" / + relativePath); +} + +} // namespace Internal + +inline ::XCEngine::Containers::String GetSceneViewportInfiniteGridShaderPath() { + return Internal::BuildSceneViewportEditorResourcePath( + std::filesystem::path("shaders") / + "scene-viewport" / + "infinite-grid" / + "infinite-grid.shader"); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/ViewportHostService.cpp b/new_editor/app/Rendering/Viewport/ViewportHostService.cpp new file mode 100644 index 00000000..39639822 --- /dev/null +++ b/new_editor/app/Rendering/Viewport/ViewportHostService.cpp @@ -0,0 +1,390 @@ +#include "ViewportHostService.h" + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::RHI::ResourceStates; + +void SetViewportStatusIfEmpty( + std::string& statusText, + std::string_view message) { + if (statusText.empty()) { + statusText = std::string(message); + } +} + +} // namespace + +ViewportHostService::ViewportHostService() = default; + +ViewportHostService::~ViewportHostService() = default; + +void ViewportHostService::AttachWindowRenderer( + Host::D3D12WindowRenderer& windowRenderer) { + if (m_windowRenderer == &windowRenderer) { + m_device = windowRenderer.GetRHIDevice(); + if (m_device != nullptr && !m_textureDescriptorAllocator.IsInitialized()) { + m_textureDescriptorAllocator.Initialize(*m_device); + } + return; + } + + Shutdown(); + m_windowRenderer = &windowRenderer; + m_device = windowRenderer.GetRHIDevice(); + if (m_device != nullptr) { + m_textureDescriptorAllocator.Initialize(*m_device); + } +} + +void ViewportHostService::DetachWindowRenderer() { + Shutdown(); +} + +void ViewportHostService::SetSurfacePresentationEnabled(bool enabled) { + m_surfacePresentationEnabled = enabled; +} + +void ViewportHostService::SetSceneViewportRenderRequest( + SceneViewportRenderRequest request) { + m_sceneViewportRenderRequest = request; +} + +void ViewportHostService::Shutdown() { + for (ViewportEntry& entry : m_entries) { + DestroyViewportEntry(entry); + } + + m_sceneViewportRenderPassBundle.Shutdown(); + m_textureDescriptorAllocator.Shutdown(); + m_windowRenderer = nullptr; + m_device = nullptr; + m_surfacePresentationEnabled = false; + m_sceneViewportRenderRequest = {}; + m_sceneRenderer.reset(); + m_sceneViewportLastRenderContext = {}; +} + +void ViewportHostService::BeginFrame() { + for (ViewportEntry& entry : m_entries) { + entry.requestedWidth = 0; + entry.requestedHeight = 0; + entry.requestedThisFrame = false; + entry.renderedThisFrame = false; + entry.kind = (&entry == &m_entries[0]) ? ViewportKind::Scene : ViewportKind::Game; + } +} + +ViewportHostService::ViewportEntry& ViewportHostService::GetEntry( + ViewportKind kind) { + const std::size_t index = kind == ViewportKind::Scene ? 0u : 1u; + ViewportEntry& entry = m_entries[index]; + entry.kind = kind; + return entry; +} + +const ViewportHostService::ViewportEntry& ViewportHostService::GetEntry( + ViewportKind kind) const { + const std::size_t index = kind == ViewportKind::Scene ? 0u : 1u; + return m_entries[index]; +} + +void ViewportHostService::DestroyViewportEntry(ViewportEntry& entry) { + m_renderTargetManager.DestroyTargets(&m_textureDescriptorAllocator, entry.renderTargets); + entry = {}; +} + +void ViewportHostService::EnsureSceneRenderer() { + if (!m_sceneRenderer) { + m_sceneRenderer = std::make_unique<::XCEngine::Rendering::SceneRenderer>(); + } +} + +ViewportFrame ViewportHostService::RequestViewport( + ViewportKind kind, + const ::XCEngine::UI::UISize& requestedSize) { + ViewportEntry& entry = GetEntry(kind); + entry.requestedThisFrame = requestedSize.width > 1.0f && requestedSize.height > 1.0f; + entry.requestedWidth = entry.requestedThisFrame + ? static_cast(requestedSize.width) + : 0u; + entry.requestedHeight = entry.requestedThisFrame + ? static_cast(requestedSize.height) + : 0u; + + if (!entry.requestedThisFrame) { + return BuildFrame(entry, requestedSize); + } + + if (m_windowRenderer == nullptr || m_device == nullptr) { + return BuildFrame(entry, requestedSize); + } + + if (!EnsureViewportResources(entry)) { + return BuildFrame(entry, requestedSize); + } + + return BuildFrame(entry, requestedSize); +} + +ViewportObjectIdPickResult ViewportHostService::PickSceneViewportObject( + const ::XCEngine::UI::UISize& viewportSize, + const ::XCEngine::UI::UIPoint& viewportMousePosition) { + if (!m_sceneViewportRenderRequest.IsValid()) { + return {}; + } + + ViewportEntry& entry = GetEntry(ViewportKind::Scene); + const ViewportObjectIdPickResult objectIdPick = + PickSceneViewportObjectWithObjectId( + entry, + viewportSize, + viewportMousePosition); + if (objectIdPick.status == ViewportObjectIdPickStatus::ReadbackFailed) { + SetViewportStatusIfEmpty( + entry.statusText, + "Scene object id readback failed"); + } + return objectIdPick; +} + +void ViewportHostService::RenderRequestedViewports( + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (m_windowRenderer == nullptr || m_device == nullptr || !renderContext.IsValid()) { + return; + } + + m_sceneViewportLastRenderContext = renderContext; + + for (ViewportEntry& entry : m_entries) { + if (!entry.requestedThisFrame || !EnsureViewportResources(entry)) { + continue; + } + + if (entry.kind == ViewportKind::Scene) { + RenderSceneViewport(entry, renderContext); + } else { + entry.statusText.clear(); + ClearViewport(entry, renderContext, 0.09f, 0.09f, 0.09f, 1.0f); + } + + entry.renderedThisFrame = true; + } +} + +bool ViewportHostService::EnsureViewportResources(ViewportEntry& entry) { + const ViewportResourceReuseQuery reuseQuery = + BuildViewportRenderTargetsReuseQuery( + entry.kind, + entry.renderTargets, + entry.requestedWidth, + entry.requestedHeight); + if (CanReuseViewportResources(reuseQuery)) { + return true; + } + + if (m_windowRenderer == nullptr || + m_device == nullptr || + entry.requestedWidth == 0u || + entry.requestedHeight == 0u) { + return false; + } + + return m_renderTargetManager.EnsureTargets( + entry.kind, + entry.requestedWidth, + entry.requestedHeight, + *m_device, + m_textureDescriptorAllocator, + entry.renderTargets); +} + +bool ViewportHostService::RenderSceneViewport( + ViewportEntry& entry, + const ::XCEngine::Rendering::RenderContext& renderContext) { + if (m_sceneViewportRenderRequest.camera == nullptr) { + ApplySceneViewportFallback( + entry, + renderContext, + "Scene view camera is unavailable", + 0.18f, + 0.07f, + 0.07f, + 1.0f); + return false; + } + + if (m_sceneViewportRenderRequest.scene == nullptr) { + ApplySceneViewportFallback( + entry, + renderContext, + "No active scene", + 0.07f, + 0.08f, + 0.10f, + 1.0f); + return false; + } + + EnsureSceneRenderer(); + entry.statusText.clear(); + + ::XCEngine::Rendering::RenderSurface surface = + BuildViewportColorSurface(entry.renderTargets); + std::vector<::XCEngine::Rendering::CameraFramePlan> plans = + m_sceneRenderer->BuildFramePlans( + *m_sceneViewportRenderRequest.scene, + m_sceneViewportRenderRequest.camera, + renderContext, + surface); + if (plans.empty()) { + ApplySceneViewportFallback( + entry, + renderContext, + "Scene renderer failed", + 0.18f, + 0.07f, + 0.07f, + 1.0f); + return false; + } + + SceneViewportRenderPlanBuildResult renderPlan = + m_sceneViewportRenderPassBundle.BuildRenderPlan( + entry.renderTargets, + m_sceneViewportRenderRequest); + ApplySceneViewportRenderPlan( + entry.renderTargets, + renderPlan.plan, + plans.front()); + if (renderPlan.warningStatusText != nullptr) { + SetViewportStatusIfEmpty(entry.statusText, renderPlan.warningStatusText); + } + + if (!m_sceneRenderer->Render(plans)) { + ApplySceneViewportFallback( + entry, + renderContext, + "Scene renderer failed", + 0.18f, + 0.07f, + 0.07f, + 1.0f); + return false; + } + + MarkSceneViewportRenderSuccess( + entry.renderTargets, + renderPlan.plan, + plans.front()); + return true; +} + +ViewportObjectIdPickResult ViewportHostService::PickSceneViewportObjectWithObjectId( + ViewportEntry& entry, + const ::XCEngine::UI::UISize& viewportSize, + const ::XCEngine::UI::UIPoint& viewportMousePosition) { + if (m_device == nullptr) { + return {}; + } + + ViewportObjectIdPickContext pickContext = {}; + pickContext.commandQueue = m_sceneViewportLastRenderContext.commandQueue; + pickContext.texture = entry.renderTargets.objectIdTexture; + pickContext.textureState = entry.renderTargets.objectIdState; + pickContext.textureWidth = entry.renderTargets.width; + pickContext.textureHeight = entry.renderTargets.height; + pickContext.hasValidFrame = entry.renderTargets.hasValidObjectIdFrame; + pickContext.viewportSize = viewportSize; + pickContext.viewportMousePosition = viewportMousePosition; + + return PickViewportObjectIdEntity( + pickContext, + [this]( + const ViewportObjectIdReadbackRequest& request, + std::array& outRgba) { + return m_device != nullptr && + m_device->ReadTexturePixelRGBA8( + request.commandQueue, + request.texture, + request.textureState, + request.pixelX, + request.pixelY, + outRgba); + }); +} + +void ViewportHostService::ApplySceneViewportFallback( + ViewportEntry& entry, + const ::XCEngine::Rendering::RenderContext& renderContext, + std::string statusText, + float r, + float g, + float b, + float a) { + entry.statusText = std::move(statusText); + entry.renderTargets.hasValidObjectIdFrame = false; + ClearViewport(entry, renderContext, r, g, b, a); +} + +void ViewportHostService::ClearViewport( + ViewportEntry& entry, + const ::XCEngine::Rendering::RenderContext& renderContext, + float r, + float g, + float b, + float a) { + if (renderContext.commandList == nullptr || + entry.renderTargets.colorView == nullptr || + entry.renderTargets.depthView == nullptr) { + return; + } + + auto* commandList = renderContext.commandList; + auto* colorView = entry.renderTargets.colorView; + const float clearColor[4] = { r, g, b, a }; + commandList->TransitionBarrier( + colorView, + entry.renderTargets.colorState, + ResourceStates::RenderTarget); + commandList->SetRenderTargets(1, &colorView, entry.renderTargets.depthView); + commandList->ClearRenderTarget(colorView, clearColor); + commandList->ClearDepthStencil(entry.renderTargets.depthView, 1.0f, 0); + commandList->TransitionBarrier( + colorView, + ResourceStates::RenderTarget, + ResourceStates::PixelShaderResource); + entry.renderTargets.colorState = ResourceStates::PixelShaderResource; + entry.renderTargets.objectIdState = ResourceStates::Common; + entry.renderTargets.selectionMaskState = ResourceStates::Common; + entry.renderTargets.hasValidObjectIdFrame = false; +} + +ViewportFrame ViewportHostService::BuildFrame( + const ViewportEntry& entry, + const ::XCEngine::UI::UISize& requestedSize) const { + ViewportFrame frame = {}; + frame.requestedSize = requestedSize; + frame.renderSize = ::XCEngine::UI::UISize( + static_cast(entry.renderTargets.width), + static_cast(entry.renderTargets.height)); + frame.wasRequested = entry.requestedThisFrame; + frame.statusText = entry.statusText; + + if (m_surfacePresentationEnabled && + entry.renderTargets.textureHandle.IsValid()) { + frame.texture = entry.renderTargets.textureHandle; + frame.hasTexture = true; + } + + return frame; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/ViewportHostService.h b/new_editor/app/Rendering/Viewport/ViewportHostService.h index f1f28042..ae97880b 100644 --- a/new_editor/app/Rendering/Viewport/ViewportHostService.h +++ b/new_editor/app/Rendering/Viewport/ViewportHostService.h @@ -1,24 +1,40 @@ #pragma once +#include "SceneViewportRenderPassBundle.h" +#include "ViewportObjectIdPicker.h" +#include "Scene/EditorSceneRuntime.h" #include "ViewportRenderTargets.h" #include #include +#include + #include #include #include #include +#include #include +namespace XCEngine::Rendering { + +class SceneRenderer; + +} // namespace XCEngine::Rendering + namespace XCEngine::UI::Editor::App { class ViewportHostService { public: + ViewportHostService(); + ~ViewportHostService(); + void AttachWindowRenderer(Host::D3D12WindowRenderer& windowRenderer); void DetachWindowRenderer(); void SetSurfacePresentationEnabled(bool enabled); + void SetSceneViewportRenderRequest(SceneViewportRenderRequest request); void Shutdown(); void BeginFrame(); @@ -26,6 +42,9 @@ public: ViewportFrame RequestViewport( ViewportKind kind, const ::XCEngine::UI::UISize& requestedSize); + ViewportObjectIdPickResult PickSceneViewportObject( + const ::XCEngine::UI::UISize& viewportSize, + const ::XCEngine::UI::UIPoint& viewportMousePosition); void RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext); @@ -44,7 +63,23 @@ private: ViewportEntry& GetEntry(ViewportKind kind); const ViewportEntry& GetEntry(ViewportKind kind) const; void DestroyViewportEntry(ViewportEntry& entry); + void EnsureSceneRenderer(); bool EnsureViewportResources(ViewportEntry& entry); + bool RenderSceneViewport( + ViewportEntry& entry, + const ::XCEngine::Rendering::RenderContext& renderContext); + ViewportObjectIdPickResult PickSceneViewportObjectWithObjectId( + ViewportEntry& entry, + const ::XCEngine::UI::UISize& viewportSize, + const ::XCEngine::UI::UIPoint& viewportMousePosition); + void ApplySceneViewportFallback( + ViewportEntry& entry, + const ::XCEngine::Rendering::RenderContext& renderContext, + std::string statusText, + float r, + float g, + float b, + float a); void ClearViewport( ViewportEntry& entry, const ::XCEngine::Rendering::RenderContext& renderContext, @@ -61,6 +96,10 @@ private: Host::D3D12ShaderResourceDescriptorAllocator m_textureDescriptorAllocator = {}; ViewportRenderTargetManager m_renderTargetManager = {}; bool m_surfacePresentationEnabled = false; + SceneViewportRenderRequest m_sceneViewportRenderRequest = {}; + std::unique_ptr<::XCEngine::Rendering::SceneRenderer> m_sceneRenderer = {}; + SceneViewportRenderPassBundle m_sceneViewportRenderPassBundle = {}; + ::XCEngine::Rendering::RenderContext m_sceneViewportLastRenderContext = {}; std::array m_entries = {}; }; diff --git a/new_editor/app/Rendering/Viewport/ViewportHostServiceFrame.cpp b/new_editor/app/Rendering/Viewport/ViewportHostServiceFrame.cpp deleted file mode 100644 index 224121f3..00000000 --- a/new_editor/app/Rendering/Viewport/ViewportHostServiceFrame.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "ViewportHostService.h" - -#include - -namespace XCEngine::UI::Editor::App { - -namespace { - -using ::XCEngine::RHI::ResourceStates; - -} // namespace - -ViewportFrame ViewportHostService::RequestViewport( - ViewportKind kind, - const ::XCEngine::UI::UISize& requestedSize) { - ViewportEntry& entry = GetEntry(kind); - entry.requestedThisFrame = requestedSize.width > 1.0f && requestedSize.height > 1.0f; - entry.requestedWidth = entry.requestedThisFrame - ? static_cast(requestedSize.width) - : 0u; - entry.requestedHeight = entry.requestedThisFrame - ? static_cast(requestedSize.height) - : 0u; - - if (!entry.requestedThisFrame) { - return BuildFrame(entry, requestedSize); - } - - if (m_windowRenderer == nullptr || m_device == nullptr) { - return BuildFrame(entry, requestedSize); - } - - if (!EnsureViewportResources(entry)) { - return BuildFrame(entry, requestedSize); - } - - return BuildFrame(entry, requestedSize); -} - -void ViewportHostService::RenderRequestedViewports( - const ::XCEngine::Rendering::RenderContext& renderContext) { - if (m_windowRenderer == nullptr || m_device == nullptr || !renderContext.IsValid()) { - return; - } - - for (ViewportEntry& entry : m_entries) { - if (!entry.requestedThisFrame || !EnsureViewportResources(entry)) { - continue; - } - - if (entry.kind == ViewportKind::Scene) { - ClearViewport(entry, renderContext, 0.09f, 0.09f, 0.09f, 1.0f); - } else { - ClearViewport(entry, renderContext, 0.09f, 0.09f, 0.09f, 1.0f); - } - - entry.renderedThisFrame = true; - } -} - -bool ViewportHostService::EnsureViewportResources(ViewportEntry& entry) { - const ViewportResourceReuseQuery reuseQuery = - BuildViewportRenderTargetsReuseQuery( - entry.kind, - entry.renderTargets, - entry.requestedWidth, - entry.requestedHeight); - if (CanReuseViewportResources(reuseQuery)) { - return true; - } - - if (m_windowRenderer == nullptr || - m_device == nullptr || - entry.requestedWidth == 0u || - entry.requestedHeight == 0u) { - return false; - } - - return m_renderTargetManager.EnsureTargets( - entry.kind, - entry.requestedWidth, - entry.requestedHeight, - *m_device, - m_textureDescriptorAllocator, - entry.renderTargets); -} - -void ViewportHostService::ClearViewport( - ViewportEntry& entry, - const ::XCEngine::Rendering::RenderContext& renderContext, - float r, - float g, - float b, - float a) { - if (renderContext.commandList == nullptr || - entry.renderTargets.colorView == nullptr || - entry.renderTargets.depthView == nullptr) { - return; - } - - auto* commandList = renderContext.commandList; - auto* colorView = entry.renderTargets.colorView; - const float clearColor[4] = { r, g, b, a }; - commandList->TransitionBarrier( - colorView, - entry.renderTargets.colorState, - ResourceStates::RenderTarget); - commandList->SetRenderTargets(1, &colorView, entry.renderTargets.depthView); - commandList->ClearRenderTarget(colorView, clearColor); - commandList->ClearDepthStencil(entry.renderTargets.depthView, 1.0f, 0); - commandList->TransitionBarrier( - colorView, - ResourceStates::RenderTarget, - ResourceStates::PixelShaderResource); - entry.renderTargets.colorState = ResourceStates::PixelShaderResource; - entry.renderTargets.hasValidObjectIdFrame = false; -} - -ViewportFrame ViewportHostService::BuildFrame( - const ViewportEntry& entry, - const ::XCEngine::UI::UISize& requestedSize) const { - ViewportFrame frame = {}; - frame.requestedSize = requestedSize; - frame.renderSize = ::XCEngine::UI::UISize( - static_cast(entry.renderTargets.width), - static_cast(entry.renderTargets.height)); - frame.wasRequested = entry.requestedThisFrame; - frame.statusText = entry.statusText; - - if (m_surfacePresentationEnabled && - entry.renderTargets.textureHandle.IsValid()) { - frame.texture = entry.renderTargets.textureHandle; - frame.hasTexture = true; - } - - return frame; -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/ViewportHostServiceLifecycle.cpp b/new_editor/app/Rendering/Viewport/ViewportHostServiceLifecycle.cpp deleted file mode 100644 index c22bb749..00000000 --- a/new_editor/app/Rendering/Viewport/ViewportHostServiceLifecycle.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "ViewportHostService.h" - -namespace XCEngine::UI::Editor::App { - -void ViewportHostService::AttachWindowRenderer( - Host::D3D12WindowRenderer& windowRenderer) { - if (m_windowRenderer == &windowRenderer) { - m_device = windowRenderer.GetRHIDevice(); - if (m_device != nullptr && !m_textureDescriptorAllocator.IsInitialized()) { - m_textureDescriptorAllocator.Initialize(*m_device); - } - return; - } - - Shutdown(); - m_windowRenderer = &windowRenderer; - m_device = windowRenderer.GetRHIDevice(); - if (m_device != nullptr) { - m_textureDescriptorAllocator.Initialize(*m_device); - } -} - -void ViewportHostService::DetachWindowRenderer() { - Shutdown(); -} - -void ViewportHostService::SetSurfacePresentationEnabled(bool enabled) { - m_surfacePresentationEnabled = enabled; -} - -void ViewportHostService::Shutdown() { - for (ViewportEntry& entry : m_entries) { - DestroyViewportEntry(entry); - } - - m_textureDescriptorAllocator.Shutdown(); - m_windowRenderer = nullptr; - m_device = nullptr; - m_surfacePresentationEnabled = false; -} - -void ViewportHostService::BeginFrame() { - for (ViewportEntry& entry : m_entries) { - entry.requestedWidth = 0; - entry.requestedHeight = 0; - entry.requestedThisFrame = false; - entry.renderedThisFrame = false; - entry.kind = (&entry == &m_entries[0]) ? ViewportKind::Scene : ViewportKind::Game; - } -} - -ViewportHostService::ViewportEntry& ViewportHostService::GetEntry( - ViewportKind kind) { - const std::size_t index = kind == ViewportKind::Scene ? 0u : 1u; - ViewportEntry& entry = m_entries[index]; - entry.kind = kind; - return entry; -} - -const ViewportHostService::ViewportEntry& ViewportHostService::GetEntry( - ViewportKind kind) const { - const std::size_t index = kind == ViewportKind::Scene ? 0u : 1u; - return m_entries[index]; -} - -void ViewportHostService::DestroyViewportEntry(ViewportEntry& entry) { - m_renderTargetManager.DestroyTargets(&m_textureDescriptorAllocator, entry.renderTargets); - entry = {}; -} - -} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/ViewportObjectIdPicker.h b/new_editor/app/Rendering/Viewport/ViewportObjectIdPicker.h new file mode 100644 index 00000000..06cc6c5d --- /dev/null +++ b/new_editor/app/Rendering/Viewport/ViewportObjectIdPicker.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +struct ViewportObjectIdPickContext { + ::XCEngine::RHI::RHICommandQueue* commandQueue = nullptr; + ::XCEngine::RHI::RHITexture* texture = nullptr; + ::XCEngine::RHI::ResourceStates textureState = + ::XCEngine::RHI::ResourceStates::Common; + std::uint32_t textureWidth = 0; + std::uint32_t textureHeight = 0; + bool hasValidFrame = false; + ::XCEngine::UI::UISize viewportSize = {}; + ::XCEngine::UI::UIPoint viewportMousePosition = {}; +}; + +struct ViewportObjectIdReadbackRequest { + ::XCEngine::RHI::RHICommandQueue* commandQueue = nullptr; + ::XCEngine::RHI::RHITexture* texture = nullptr; + ::XCEngine::RHI::ResourceStates textureState = + ::XCEngine::RHI::ResourceStates::Common; + std::uint32_t pixelX = 0; + std::uint32_t pixelY = 0; +}; + +enum class ViewportObjectIdPickStatus : std::uint8_t { + Unavailable = 0, + Success, + ReadbackFailed +}; + +struct ViewportObjectIdPickResult { + ViewportObjectIdPickStatus status = ViewportObjectIdPickStatus::Unavailable; + ::XCEngine::Rendering::RenderObjectId renderObjectId = + ::XCEngine::Rendering::kInvalidRenderObjectId; + std::uint64_t entityId = 0; + + bool HasResolvedSample() const { + return status == ViewportObjectIdPickStatus::Success; + } +}; + +inline bool CanPickViewportObjectId(const ViewportObjectIdPickContext& context) { + return context.commandQueue != nullptr && + context.texture != nullptr && + context.textureWidth > 0u && + context.textureHeight > 0u && + context.hasValidFrame && + context.viewportSize.width > 1.0f && + context.viewportSize.height > 1.0f && + context.viewportMousePosition.x >= 0.0f && + context.viewportMousePosition.y >= 0.0f && + context.viewportMousePosition.x <= context.viewportSize.width && + context.viewportMousePosition.y <= context.viewportSize.height; +} + +inline std::uint32_t ClampViewportObjectIdPixelCoordinate( + float value, + std::uint32_t extent) { + if (extent == 0u) { + return 0u; + } + + const float maxCoordinate = static_cast(extent - 1u); + const float clamped = (std::max)(0.0f, (std::min)(value, maxCoordinate)); + return static_cast(std::floor(clamped)); +} + +inline std::uint32_t ResolveViewportObjectIdPixelCoordinate( + float viewportCoordinate, + float viewportExtent, + std::uint32_t textureExtent) { + if (viewportExtent <= 0.0f || textureExtent == 0u) { + return 0u; + } + + const float normalized = viewportCoordinate / viewportExtent; + const float textureCoordinate = + normalized * static_cast(textureExtent); + return ClampViewportObjectIdPixelCoordinate(textureCoordinate, textureExtent); +} + +inline bool BuildViewportObjectIdReadbackRequest( + const ViewportObjectIdPickContext& context, + ViewportObjectIdReadbackRequest& outRequest) { + outRequest = {}; + if (!CanPickViewportObjectId(context)) { + return false; + } + + outRequest.commandQueue = context.commandQueue; + outRequest.texture = context.texture; + outRequest.textureState = context.textureState; + outRequest.pixelX = ResolveViewportObjectIdPixelCoordinate( + context.viewportMousePosition.x, + context.viewportSize.width, + context.textureWidth); + outRequest.pixelY = ResolveViewportObjectIdPixelCoordinate( + context.viewportMousePosition.y, + context.viewportSize.height, + context.textureHeight); + return true; +} + +template +ViewportObjectIdPickResult PickViewportObjectIdEntity( + const ViewportObjectIdPickContext& context, + ReadPixelFn&& readPixel) { + ViewportObjectIdPickResult result = {}; + + ViewportObjectIdReadbackRequest request = {}; + if (!BuildViewportObjectIdReadbackRequest(context, request)) { + return result; + } + + std::array rgba = {}; + if (!readPixel(request, rgba)) { + result.status = ViewportObjectIdPickStatus::ReadbackFailed; + return result; + } + + result.status = ViewportObjectIdPickStatus::Success; + result.renderObjectId = ::XCEngine::Rendering::DecodeRenderObjectIdFromColor( + rgba[0], + rgba[1], + rgba[2], + rgba[3]); + result.entityId = ::XCEngine::Rendering::ConvertRenderObjectIdToRuntimeObjectId( + result.renderObjectId); + return result; +} + +template +bool TryPickViewportObjectIdEntity( + const ViewportObjectIdPickContext& context, + ReadPixelFn&& readPixel, + std::uint64_t& outEntityId) { + const ViewportObjectIdPickResult result = + PickViewportObjectIdEntity(context, std::forward(readPixel)); + outEntityId = result.entityId; + return result.HasResolvedSample(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Rendering/Viewport/RenderTargetManager/Resources.cpp b/new_editor/app/Rendering/Viewport/ViewportRenderTargets.cpp similarity index 58% rename from new_editor/app/Rendering/Viewport/RenderTargetManager/Resources.cpp rename to new_editor/app/Rendering/Viewport/ViewportRenderTargets.cpp index 27820d1e..fc2022e8 100644 --- a/new_editor/app/Rendering/Viewport/RenderTargetManager/Resources.cpp +++ b/new_editor/app/Rendering/Viewport/ViewportRenderTargets.cpp @@ -129,8 +129,89 @@ bool CreateViewportTextureDescriptor( return true; } +template +void ShutdownAndDeleteViewportResource(ResourceType*& resource) { + if (resource == nullptr) { + return; + } + + resource->Shutdown(); + delete resource; + resource = nullptr; +} + +void ResetViewportTargetMetadata(ViewportRenderTargets& targets) { + targets.width = 0; + targets.height = 0; + targets.srvCpuHandle = {}; + targets.srvGpuHandle = {}; + targets.textureHandle = {}; + targets.colorState = ::XCEngine::RHI::ResourceStates::Common; + targets.objectIdState = ::XCEngine::RHI::ResourceStates::Common; + targets.selectionMaskState = ::XCEngine::RHI::ResourceStates::Common; + targets.hasValidObjectIdFrame = false; +} + } // namespace +ViewportResourceReuseQuery BuildViewportRenderTargetsReuseQuery( + ViewportKind kind, + const ViewportRenderTargets& targets, + std::uint32_t requestedWidth, + std::uint32_t requestedHeight) { + ViewportResourceReuseQuery query = {}; + query.kind = kind; + query.width = targets.width; + query.height = targets.height; + query.requestedWidth = requestedWidth; + query.requestedHeight = requestedHeight; + query.resources.hasColorTexture = targets.colorTexture != nullptr; + query.resources.hasColorView = targets.colorView != nullptr; + query.resources.hasDepthTexture = targets.depthTexture != nullptr; + query.resources.hasDepthView = targets.depthView != nullptr; + query.resources.hasDepthShaderView = targets.depthShaderView != nullptr; + query.resources.hasObjectIdTexture = targets.objectIdTexture != nullptr; + query.resources.hasObjectIdDepthTexture = targets.objectIdDepthTexture != nullptr; + query.resources.hasObjectIdDepthView = targets.objectIdDepthView != nullptr; + query.resources.hasObjectIdView = targets.objectIdView != nullptr; + query.resources.hasObjectIdShaderView = targets.objectIdShaderView != nullptr; + query.resources.hasSelectionMaskTexture = targets.selectionMaskTexture != nullptr; + query.resources.hasSelectionMaskView = targets.selectionMaskView != nullptr; + query.resources.hasSelectionMaskShaderView = targets.selectionMaskShaderView != nullptr; + query.resources.hasTextureDescriptor = targets.textureHandle.IsValid(); + return query; +} + +::XCEngine::Rendering::RenderSurface BuildViewportColorSurface( + const ViewportRenderTargets& targets) { + return BuildViewportRenderSurface( + targets.width, + targets.height, + targets.colorView, + targets.depthView, + targets.colorState); +} + +::XCEngine::Rendering::RenderSurface BuildViewportObjectIdSurface( + const ViewportRenderTargets& targets) { + return BuildViewportRenderSurface( + targets.width, + targets.height, + targets.objectIdView, + targets.objectIdDepthView, + targets.objectIdState); +} + +::XCEngine::Rendering::RenderSurface BuildViewportSelectionMaskSurface( + const ViewportRenderTargets& targets) { + return BuildViewportRenderSurface( + targets.width, + targets.height, + targets.selectionMaskView, + targets.depthView, + targets.selectionMaskState); +} + bool ViewportRenderTargetManager::EnsureTargets( ViewportKind kind, std::uint32_t width, @@ -163,4 +244,28 @@ bool ViewportRenderTargetManager::EnsureTargets( return true; } +void ViewportRenderTargetManager::DestroyTargets( + Host::D3D12ShaderResourceDescriptorAllocator* textureDescriptorAllocator, + ViewportRenderTargets& targets) const { + if (textureDescriptorAllocator != nullptr && targets.srvCpuHandle.ptr != 0) { + textureDescriptorAllocator->Free(targets.srvCpuHandle, targets.srvGpuHandle); + } + + ShutdownAndDeleteViewportResource(targets.objectIdView); + ShutdownAndDeleteViewportResource(targets.objectIdShaderView); + ShutdownAndDeleteViewportResource(targets.objectIdDepthView); + ShutdownAndDeleteViewportResource(targets.objectIdDepthTexture); + ShutdownAndDeleteViewportResource(targets.objectIdTexture); + ShutdownAndDeleteViewportResource(targets.selectionMaskView); + ShutdownAndDeleteViewportResource(targets.selectionMaskShaderView); + ShutdownAndDeleteViewportResource(targets.selectionMaskTexture); + ShutdownAndDeleteViewportResource(targets.depthShaderView); + ShutdownAndDeleteViewportResource(targets.depthView); + ShutdownAndDeleteViewportResource(targets.depthTexture); + ShutdownAndDeleteViewportResource(targets.colorView); + ShutdownAndDeleteViewportResource(targets.colorTexture); + + ResetViewportTargetMetadata(targets); +} + } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/State/EditorContext.h b/new_editor/app/State/EditorContext.h index be8a46fb..ca2e39e1 100644 --- a/new_editor/app/State/EditorContext.h +++ b/new_editor/app/State/EditorContext.h @@ -1,6 +1,7 @@ #pragma once #include "Composition/EditorShellVariant.h" +#include "Project/EditorProjectRuntime.h" #include "Scene/EditorSceneRuntime.h" #include @@ -25,7 +26,9 @@ public: void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer); void BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute); + EditorEditCommandRoute* projectRoute, + EditorEditCommandRoute* sceneRoute, + EditorEditCommandRoute* inspectorRoute = nullptr); void SetExitRequestHandler(std::function handler); void SyncSessionFromWorkspace(const UIEditorWorkspaceController& workspaceController); @@ -33,10 +36,13 @@ public: const std::string& GetValidationMessage() const; const EditorShellAsset& GetShellAsset() const; const EditorSession& GetSession() const; + EditorProjectRuntime& GetProjectRuntime(); + const EditorProjectRuntime& GetProjectRuntime() const; EditorSceneRuntime& GetSceneRuntime(); const EditorSceneRuntime& GetSceneRuntime() const; void SetSelection(EditorSelectionState selection); void ClearSelection(); + void SyncSessionFromProjectRuntime(); UIEditorWorkspaceController BuildWorkspaceController() const; const UIEditorShellInteractionServices& GetShellServices() const; @@ -63,6 +69,7 @@ private: UIEditorShortcutManager m_shortcutManager = {}; UIEditorShellInteractionServices m_shellServices = {}; EditorSession m_session = {}; + EditorProjectRuntime m_projectRuntime = {}; EditorSceneRuntime m_sceneRuntime = {}; EditorHostCommandBridge m_hostCommandBridge = {}; std::string m_lastStatus = {}; diff --git a/new_editor/app/State/EditorContextStatus.cpp b/new_editor/app/State/EditorContextStatus.cpp index 5e7abf33..fd1d9553 100644 --- a/new_editor/app/State/EditorContextStatus.cpp +++ b/new_editor/app/State/EditorContextStatus.cpp @@ -42,7 +42,7 @@ std::string ResolveViewportStatusMessage( } // namespace void EditorContext::SetReadyStatus() { - SetStatus("Ready", "Old editor shell baseline loaded."); + SetStatus("Ready", "Application shell loaded."); } void EditorContext::SetStatus( diff --git a/new_editor/include/XCEditor/App/EditorEditCommandRoute.h b/new_editor/include/XCEditor/App/EditorEditCommandRoute.h new file mode 100644 index 00000000..69fb2750 --- /dev/null +++ b/new_editor/include/XCEditor/App/EditorEditCommandRoute.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class EditorEditCommandRoute { +public: + virtual ~EditorEditCommandRoute() = default; + + virtual UIEditorHostCommandEvaluationResult EvaluateEditCommand( + std::string_view commandId) const = 0; + + virtual UIEditorHostCommandDispatchResult DispatchEditCommand( + std::string_view commandId) = 0; + + virtual UIEditorHostCommandEvaluationResult EvaluateAssetCommand( + std::string_view commandId) const { + (void)commandId; + UIEditorHostCommandEvaluationResult result = {}; + result.message = "Current panel does not expose asset commands."; + return result; + } + + virtual UIEditorHostCommandDispatchResult DispatchAssetCommand( + std::string_view commandId) { + UIEditorHostCommandDispatchResult result = {}; + result.message = EvaluateAssetCommand(commandId).message; + return result; + } +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorHostCommandBridge.h b/new_editor/include/XCEditor/App/EditorHostCommandBridge.h similarity index 80% rename from new_editor/app/Composition/EditorHostCommandBridge.h rename to new_editor/include/XCEditor/App/EditorHostCommandBridge.h index 1ea7c3f9..6d31f565 100644 --- a/new_editor/app/Composition/EditorHostCommandBridge.h +++ b/new_editor/include/XCEditor/App/EditorHostCommandBridge.h @@ -1,8 +1,7 @@ #pragma once -#include "Composition/EditorEditCommandRoute.h" -#include "State/EditorSession.h" - +#include +#include #include #include @@ -15,7 +14,9 @@ public: void BindSession(EditorSession& session); void BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute); + EditorEditCommandRoute* projectRoute, + EditorEditCommandRoute* sceneRoute = nullptr, + EditorEditCommandRoute* inspectorRoute = nullptr); void SetExitRequestHandler(std::function handler); UIEditorHostCommandEvaluationResult EvaluateHostCommand( @@ -38,6 +39,8 @@ private: std::string_view commandId) const; UIEditorHostCommandEvaluationResult EvaluateEditCommand( std::string_view commandId) const; + UIEditorHostCommandDispatchResult DispatchAssetCommand( + std::string_view commandId); UIEditorHostCommandDispatchResult DispatchEditCommand( std::string_view commandId); UIEditorHostCommandEvaluationResult EvaluateUnsupportedHostCommand( @@ -47,8 +50,9 @@ private: EditorSession* m_session = nullptr; EditorEditCommandRoute* m_hierarchyRoute = nullptr; EditorEditCommandRoute* m_projectRoute = nullptr; + EditorEditCommandRoute* m_sceneRoute = nullptr; + EditorEditCommandRoute* m_inspectorRoute = nullptr; std::function m_requestExit = {}; }; } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/include/XCEditor/App/EditorPanelIds.h b/new_editor/include/XCEditor/App/EditorPanelIds.h new file mode 100644 index 00000000..ca608769 --- /dev/null +++ b/new_editor/include/XCEditor/App/EditorPanelIds.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace XCEngine::UI::Editor::App { + +inline constexpr std::string_view kHierarchyPanelId = "hierarchy"; +inline constexpr std::string_view kScenePanelId = "scene"; +inline constexpr std::string_view kGamePanelId = "game"; +inline constexpr std::string_view kInspectorPanelId = "inspector"; +inline constexpr std::string_view kConsolePanelId = "console"; +inline constexpr std::string_view kProjectPanelId = "project"; + +inline constexpr std::string_view kHierarchyPanelTitle = "Hierarchy"; +inline constexpr std::string_view kScenePanelTitle = "Scene"; +inline constexpr std::string_view kGamePanelTitle = "Game"; +inline constexpr std::string_view kInspectorPanelTitle = "Inspector"; +inline constexpr std::string_view kConsolePanelTitle = "Console"; +inline constexpr std::string_view kProjectPanelTitle = "Project"; + +[[nodiscard]] constexpr bool IsEditorViewportPanelId(std::string_view panelId) { + return panelId == kScenePanelId || panelId == kGamePanelId; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/State/EditorSession.h b/new_editor/include/XCEditor/App/EditorSession.h similarity index 97% rename from new_editor/app/State/EditorSession.h rename to new_editor/include/XCEditor/App/EditorSession.h index 202f5f4d..75a11b24 100644 --- a/new_editor/app/State/EditorSession.h +++ b/new_editor/include/XCEditor/App/EditorSession.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace XCEngine::UI::Editor { @@ -41,6 +42,7 @@ struct EditorSelectionState { std::string displayName = {}; std::filesystem::path absolutePath = {}; bool directory = false; + std::uint64_t stamp = 0u; }; struct EditorConsoleEntry { @@ -69,4 +71,3 @@ void SyncEditorSessionFromWorkspace( const UIEditorWorkspaceController& controller); } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h index 8a734ee6..e1259754 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h +++ b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h @@ -6,6 +6,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -24,6 +28,7 @@ enum class UIEditorPropertyGridFieldKind : std::uint8_t { Bool, Number, Enum, + Asset, Color, Vector2, Vector3, @@ -60,6 +65,17 @@ struct UIEditorPropertyGridEnumFieldValue { std::size_t selectedIndex = 0u; }; +struct UIEditorPropertyGridAssetFieldValue { + std::string assetId = {}; + std::string displayName = {}; + std::string statusText = {}; + std::string emptyText = "None"; + ::XCEngine::UI::UIColor tint = ::XCEngine::UI::UIColor(0.28f, 0.50f, 0.83f, 1.0f); + bool showPickerButton = true; + bool allowClear = true; + bool showStatusBadge = true; +}; + struct UIEditorPropertyGridColorFieldValue { ::XCEngine::UI::UIColor value = {}; bool showAlpha = true; @@ -111,6 +127,7 @@ struct UIEditorPropertyGridField { bool boolValue = false; UIEditorPropertyGridNumberFieldValue numberValue = {}; UIEditorPropertyGridEnumFieldValue enumValue = {}; + UIEditorPropertyGridAssetFieldValue assetValue = {}; UIEditorPropertyGridColorFieldValue colorValue = {}; UIEditorPropertyGridVector2FieldValue vector2Value = {}; UIEditorPropertyGridVector3FieldValue vector3Value = {}; @@ -129,6 +146,26 @@ struct UIEditorPropertyGridColorFieldVisualState { UIEditorColorFieldState state = {}; }; +struct UIEditorPropertyGridAssetFieldVisualState { + std::string fieldId = {}; + UIEditorAssetFieldState state = {}; +}; + +struct UIEditorPropertyGridVector2FieldVisualState { + std::string fieldId = {}; + UIEditorVector2FieldState state = {}; +}; + +struct UIEditorPropertyGridVector3FieldVisualState { + std::string fieldId = {}; + UIEditorVector3FieldState state = {}; +}; + +struct UIEditorPropertyGridVector4FieldVisualState { + std::string fieldId = {}; + UIEditorVector4FieldState state = {}; +}; + struct UIEditorPropertyGridState { std::string hoveredSectionId = {}; std::string hoveredFieldId = {}; @@ -137,7 +174,11 @@ struct UIEditorPropertyGridState { std::string pressedFieldId = {}; std::string popupFieldId = {}; std::size_t popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + std::vector assetFieldStates = {}; std::vector colorFieldStates = {}; + std::vector vector2FieldStates = {}; + std::vector vector3FieldStates = {}; + std::vector vector4FieldStates = {}; }; struct UIEditorPropertyGridMetrics { diff --git a/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h b/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h index 93ecc1a6..072e9eaf 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h +++ b/new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h @@ -1,6 +1,10 @@ #pragma once +#include #include +#include +#include +#include #include #include @@ -14,6 +18,26 @@ namespace XCEngine::UI::Editor { +struct UIEditorPropertyGridAssetFieldInteractionEntry { + std::string fieldId = {}; + UIEditorAssetFieldInteractionState state = {}; +}; + +struct UIEditorPropertyGridVector2FieldInteractionEntry { + std::string fieldId = {}; + UIEditorVector2FieldInteractionState state = {}; +}; + +struct UIEditorPropertyGridVector3FieldInteractionEntry { + std::string fieldId = {}; + UIEditorVector3FieldInteractionState state = {}; +}; + +struct UIEditorPropertyGridVector4FieldInteractionEntry { + std::string fieldId = {}; + UIEditorVector4FieldInteractionState state = {}; +}; + struct UIEditorPropertyGridInteractionState { Widgets::UIEditorPropertyGridState propertyGridState = {}; ::XCEngine::UI::Widgets::UIKeyboardNavigationModel keyboardNavigation = {}; @@ -21,6 +45,10 @@ struct UIEditorPropertyGridInteractionState { ::XCEngine::UI::UIPoint pointerPosition = {}; std::size_t pressedPopupIndex = Widgets::UIEditorPropertyGridInvalidIndex; bool hasPointerPosition = false; + std::vector assetFieldInteractionStates = {}; + std::vector vector2FieldInteractionStates = {}; + std::vector vector3FieldInteractionStates = {}; + std::vector vector4FieldInteractionStates = {}; }; struct UIEditorPropertyGridInteractionResult { @@ -37,6 +65,8 @@ struct UIEditorPropertyGridInteractionResult { bool popupClosed = false; bool fieldValueChanged = false; bool secondaryClicked = false; + bool pickerRequested = false; + bool activateRequested = false; Widgets::UIEditorPropertyGridHitTarget hitTarget = {}; std::string toggledSectionId = {}; std::string selectedFieldId = {}; @@ -45,6 +75,7 @@ struct UIEditorPropertyGridInteractionResult { std::string committedValue = {}; std::string changedFieldId = {}; std::string changedValue = {}; + std::string requestedFieldId = {}; }; struct UIEditorPropertyGridInteractionFrame { diff --git a/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h b/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h index 8751b89b..cb52cb0b 100644 --- a/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h +++ b/new_editor/include/XCEditor/Panels/UIEditorPanelContentHost.h @@ -21,11 +21,6 @@ std::string_view GetUIEditorPanelContentHostEventKindName( bool IsUIEditorPanelPresentationExternallyHosted(UIEditorPanelPresentationKind kind); -struct UIEditorPanelContentHostBinding { - std::string panelId = {}; - UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; -}; - struct UIEditorPanelContentHostMountRequest { std::string panelId = {}; UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; @@ -73,14 +68,12 @@ const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( const Widgets::UIEditorDockHostLayout& dockHostLayout, - const UIEditorPanelRegistry& panelRegistry, - const std::vector& bindings); + const UIEditorPanelRegistry& panelRegistry); UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( UIEditorPanelContentHostState& state, const UIEditorPanelContentHostRequest& request, - const UIEditorPanelRegistry& panelRegistry, - const std::vector& bindings); + const UIEditorPanelRegistry& panelRegistry); std::vector CollectMountedUIEditorPanelContentHostPanelIds( const UIEditorPanelContentHostFrame& frame); diff --git a/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h b/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h index 37f9a3af..c32fc918 100644 --- a/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h +++ b/new_editor/include/XCEditor/Panels/UIEditorPanelRegistry.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -20,6 +22,7 @@ struct UIEditorPanelDescriptor { bool placeholder = true; bool canHide = true; bool canClose = true; + UIEditorViewportShellSpec viewportShellSpec = {}; }; struct UIEditorPanelRegistry { @@ -42,7 +45,7 @@ struct UIEditorPanelRegistryValidationResult { } }; -UIEditorPanelRegistry BuildDefaultEditorShellPanelRegistry(); +UIEditorPanelRegistry BuildEditorFoundationPanelRegistry(); const UIEditorPanelDescriptor* FindUIEditorPanelDescriptor( const UIEditorPanelRegistry& registry, diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellAsset.h b/new_editor/include/XCEditor/Shell/UIEditorShellAsset.h index 59df21e8..ce278d46 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellAsset.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellAsset.h @@ -55,7 +55,7 @@ struct EditorShellAssetValidationResult { } }; -EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot); +EditorShellAsset BuildEditorFoundationShellAsset(const std::filesystem::path& repoRoot); UIEditorShortcutManager BuildEditorShellShortcutManager(const EditorShellAsset& asset); EditorShellAssetValidationResult ValidateEditorShellAsset(const EditorShellAsset& asset); diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellCapturePolicy.h b/new_editor/include/XCEditor/Shell/UIEditorShellCapturePolicy.h new file mode 100644 index 00000000..14883c98 --- /dev/null +++ b/new_editor/include/XCEditor/Shell/UIEditorShellCapturePolicy.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace XCEngine::UI { +struct UIPoint; +} + +namespace XCEngine::UI::Editor { + +bool ShouldStartImmediateUIEditorShellPointerCapture( + const UIEditorShellInteractionFrame& shellFrame, + const ::XCEngine::UI::UIPoint& clientPoint); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Viewport/UIEditorViewportInputBridge.h b/new_editor/include/XCEditor/Viewport/UIEditorViewportInputBridge.h index 30b1d782..0734def7 100644 --- a/new_editor/include/XCEditor/Viewport/UIEditorViewportInputBridge.h +++ b/new_editor/include/XCEditor/Viewport/UIEditorViewportInputBridge.h @@ -27,7 +27,18 @@ struct UIEditorViewportInputBridgeState { std::uint8_t pointerButtonsDownMask = 0; }; +struct UIEditorViewportPointerButtonTransition { + ::XCEngine::UI::UIPointerButton button = + ::XCEngine::UI::UIPointerButton::None; + bool pressed = false; + bool inside = false; + ::XCEngine::UI::UIPoint screenPosition = {}; + ::XCEngine::UI::UIPoint localPointerPosition = {}; + ::XCEngine::UI::UIInputModifiers modifiers = {}; +}; + struct UIEditorViewportInputBridgeFrame { + bool hasPointerPosition = false; bool hovered = false; bool focused = false; bool captured = false; @@ -48,6 +59,7 @@ struct UIEditorViewportInputBridgeFrame { std::vector pressedKeyCodes = {}; std::vector releasedKeyCodes = {}; std::vector characters = {}; + std::vector pointerButtonTransitions = {}; }; bool IsUIEditorViewportInputBridgeKeyDown( @@ -64,4 +76,11 @@ UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( const std::vector<::XCEngine::UI::UIInputEvent>& events, const UIEditorViewportInputBridgeConfig& config = {}); +UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( + UIEditorViewportInputBridgeState& state, + const ::XCEngine::UI::UIRect& interactionRect, + const ::XCEngine::UI::UIRect& localRect, + const std::vector<::XCEngine::UI::UIInputEvent>& events, + const UIEditorViewportInputBridgeConfig& config = {}); + } // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h b/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h index 690af7cb..a0426c57 100644 --- a/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h +++ b/new_editor/include/XCEditor/Viewport/UIEditorViewportSlot.h @@ -39,8 +39,8 @@ struct UIEditorViewportSlotFrame { }; struct UIEditorViewportSlotChrome { - std::string_view title = {}; - std::string_view subtitle = {}; + std::string title = {}; + std::string subtitle = {}; bool showTopBar = true; bool showBottomBar = true; float topBarHeight = 24.0f; @@ -71,7 +71,7 @@ struct UIEditorViewportSlotMetrics { float outerBorderThickness = 1.0f; float focusedBorderThickness = 1.0f; float topBarPaddingX = 8.0f; - float topBarPaddingY = 4.0f; + float topBarPaddingY = 0.0f; float toolPaddingX = 8.0f; float toolGap = 4.0f; float toolCornerRounding = 2.0f; diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h index aaffad76..7153584a 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceModel.h @@ -57,7 +57,7 @@ struct UIEditorWorkspaceVisiblePanel { bool placeholder = false; }; -UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel(); +UIEditorWorkspaceModel BuildEditorFoundationWorkspaceModel(); UIEditorWorkspaceNode BuildUIEditorWorkspacePanel( std::string nodeId, diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceValidation.h b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceValidation.h index f5a68b1e..99a0585a 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceValidation.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWorkspaceValidation.h @@ -10,6 +10,7 @@ namespace XCEngine::UI::Editor { enum class UIEditorWorkspaceValidationCode : std::uint8_t { None = 0, EmptyNodeId, + DuplicateNodeId, InvalidSplitChildCount, InvalidSplitRatio, EmptyTabStack, diff --git a/new_editor/app/Composition/EditorHostCommandBridge.cpp b/new_editor/src/App/EditorHostCommandBridge.cpp similarity index 79% rename from new_editor/app/Composition/EditorHostCommandBridge.cpp rename to new_editor/src/App/EditorHostCommandBridge.cpp index 7a83e900..456a170b 100644 --- a/new_editor/app/Composition/EditorHostCommandBridge.cpp +++ b/new_editor/src/App/EditorHostCommandBridge.cpp @@ -1,4 +1,4 @@ -#include "EditorHostCommandBridge.h" +#include #include #include @@ -11,9 +11,13 @@ void EditorHostCommandBridge::BindSession(EditorSession& session) { void EditorHostCommandBridge::BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute) { + EditorEditCommandRoute* projectRoute, + EditorEditCommandRoute* sceneRoute, + EditorEditCommandRoute* inspectorRoute) { m_hierarchyRoute = hierarchyRoute; m_projectRoute = projectRoute; + m_sceneRoute = sceneRoute; + m_inspectorRoute = inspectorRoute; } void EditorHostCommandBridge::SetExitRequestHandler(std::function handler) { @@ -34,6 +38,10 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateHostCommand return EvaluateEditCommand(commandId); } + if (commandId.rfind("assets.", 0u) == 0u) { + return EvaluateAssetCommand(commandId); + } + return EvaluateUnsupportedHostCommand(commandId); } @@ -59,6 +67,10 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchHostCommand( return DispatchEditCommand(commandId); } + if (commandId.rfind("assets.", 0u) == 0u) { + return DispatchAssetCommand(commandId); + } + result.message = EvaluateHostCommand(commandId).message; return result; } @@ -91,9 +103,11 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateFileCommand UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateAssetCommand( std::string_view commandId) const { - (void)commandId; - return BuildDisabledResult( - "Asset commands have no bound Project/AssetDatabase owner in the current shell."); + if (m_projectRoute == nullptr) { + return BuildDisabledResult("Project command route is unavailable in the current window."); + } + + return m_projectRoute->EvaluateAssetCommand(commandId); } UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateRunCommand( @@ -128,10 +142,11 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateEditCommand case EditorActionRoute::Project: return BuildDisabledResult("Project command route is unavailable in the current window."); case EditorActionRoute::Inspector: - return BuildDisabledResult("Inspector does not expose edit commands yet."); + return BuildDisabledResult("Inspector command route is unavailable in the current window."); case EditorActionRoute::Console: return BuildDisabledResult("Console does not expose edit commands yet."); case EditorActionRoute::Scene: + return BuildDisabledResult("Scene command route is unavailable in the current window."); case EditorActionRoute::Game: return BuildDisabledResult("Viewport panels do not expose edit commands yet."); case EditorActionRoute::None: @@ -162,6 +177,23 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchEditCommand( return route->DispatchEditCommand(commandId); } +UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchAssetCommand( + std::string_view commandId) { + UIEditorHostCommandDispatchResult result = {}; + const UIEditorHostCommandEvaluationResult evaluation = EvaluateAssetCommand(commandId); + if (!evaluation.executable) { + result.message = evaluation.message; + return result; + } + + if (m_projectRoute == nullptr) { + result.message = "Project command route is unavailable."; + return result; + } + + return m_projectRoute->DispatchAssetCommand(commandId); +} + UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateUnsupportedHostCommand( std::string_view commandId) const { if (commandId.rfind("file.", 0u) == 0u) { @@ -190,10 +222,13 @@ EditorEditCommandRoute* EditorHostCommandBridge::ResolveEditCommandRoute( return m_hierarchyRoute; case EditorActionRoute::Project: return m_projectRoute; + case EditorActionRoute::Inspector: + return m_inspectorRoute; + case EditorActionRoute::Scene: + return m_sceneRoute; default: return nullptr; } } } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/State/EditorSession.cpp b/new_editor/src/App/EditorSession.cpp similarity index 86% rename from new_editor/app/State/EditorSession.cpp rename to new_editor/src/App/EditorSession.cpp index 85c36597..b5a2a2dc 100644 --- a/new_editor/app/State/EditorSession.cpp +++ b/new_editor/src/App/EditorSession.cpp @@ -1,4 +1,5 @@ -#include "EditorSession.h" +#include +#include #include @@ -50,22 +51,22 @@ std::string_view GetEditorSelectionKindName(EditorSelectionKind kind) { } EditorActionRoute ResolveEditorActionRoute(std::string_view panelId) { - if (panelId == "hierarchy") { + if (panelId == kHierarchyPanelId) { return EditorActionRoute::Hierarchy; } - if (panelId == "project") { + if (panelId == kProjectPanelId) { return EditorActionRoute::Project; } - if (panelId == "inspector") { + if (panelId == kInspectorPanelId) { return EditorActionRoute::Inspector; } - if (panelId == "console") { + if (panelId == kConsolePanelId) { return EditorActionRoute::Console; } - if (panelId == "scene") { + if (panelId == kScenePanelId) { return EditorActionRoute::Scene; } - if (panelId == "game") { + if (panelId == kGamePanelId) { return EditorActionRoute::Game; } return EditorActionRoute::None; @@ -79,4 +80,3 @@ void SyncEditorSessionFromWorkspace( } } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/src/Fields/PropertyGridInteractionAsset.cpp b/new_editor/src/Fields/PropertyGridInteractionAsset.cpp new file mode 100644 index 00000000..e820eb95 --- /dev/null +++ b/new_editor/src/Fields/PropertyGridInteractionAsset.cpp @@ -0,0 +1,300 @@ +#include "Fields/PropertyGridInteractionInternal.h" + +#include + +#include + +namespace XCEngine::UI::Editor::Internal { + +using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridHitTargetKind; +using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +namespace { + +template +Entry* FindMutableEntry( + std::vector& entries, + std::string_view fieldId) { + for (Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +const Entry* FindEntry( + const std::vector& entries, + std::string_view fieldId) { + for (const Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +UIEditorAssetFieldInteractionState BuildInteractionState( + const UIEditorPropertyGridInteractionState& state, + std::string_view fieldId) { + UIEditorAssetFieldInteractionState interactionState = {}; + if (const auto* entry = FindEntry(state.assetFieldInteractionStates, fieldId); + entry != nullptr) { + interactionState = entry->state; + } + + interactionState.pointerPosition = state.pointerPosition; + interactionState.hasPointerPosition = state.hasPointerPosition; + return interactionState; +} + +void StoreInteractionState( + UIEditorPropertyGridInteractionState& state, + std::string_view fieldId, + const UIEditorAssetFieldInteractionState& interactionState) { + if (auto* entry = FindMutableEntry(state.assetFieldInteractionStates, fieldId); + entry != nullptr) { + entry->state = interactionState; + } else { + UIEditorPropertyGridAssetFieldInteractionEntry newEntry = {}; + newEntry.fieldId = std::string(fieldId); + newEntry.state = interactionState; + state.assetFieldInteractionStates.push_back(std::move(newEntry)); + } + + if (auto* visualState = + FindMutableEntry(state.propertyGridState.assetFieldStates, fieldId); + visualState != nullptr) { + visualState->state = interactionState.fieldState; + } else { + Widgets::UIEditorPropertyGridAssetFieldVisualState newVisualState = {}; + newVisualState.fieldId = std::string(fieldId); + newVisualState.state = interactionState.fieldState; + state.propertyGridState.assetFieldStates.push_back(std::move(newVisualState)); + } +} + +bool IsVisibleAssetFieldId( + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + std::string_view fieldId) { + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + return sectionIndex < sections.size() && + fieldIndex < sections[sectionIndex].fields.size() && + sections[sectionIndex].fields[fieldIndex].kind == + UIEditorPropertyGridFieldKind::Asset; +} + +void CopySpecToField( + UIEditorPropertyGridField& field, + const Widgets::UIEditorAssetFieldSpec& spec) { + field.assetValue.assetId = spec.assetId; + field.assetValue.displayName = spec.displayName; + field.assetValue.statusText = spec.statusText; + field.assetValue.emptyText = spec.emptyText; + field.assetValue.tint = spec.tint; + field.assetValue.showPickerButton = spec.showPickerButton; + field.assetValue.allowClear = spec.allowClear; + field.assetValue.showStatusBadge = spec.showStatusBadge; +} + +bool HasMeaningfulResult( + const UIEditorAssetFieldInteractionResult& result, + const UIEditorAssetFieldInteractionState& interactionState, + bool hadFocus) { + return result.consumed || + result.focusChanged || + result.valueChanged || + result.activateRequested || + result.pickerRequested || + result.clearRequested || + result.hitTarget.kind != Widgets::UIEditorAssetFieldHitTargetKind::None || + hadFocus || + interactionState.fieldState.focused; +} + +void AssignPropertyGridHitTarget( + UIEditorPropertyGridInteractionResult& result, + std::size_t sectionIndex, + std::size_t fieldIndex, + std::size_t visibleFieldIndex, + Widgets::UIEditorAssetFieldHitTargetKind hitTargetKind) { + switch (hitTargetKind) { + case Widgets::UIEditorAssetFieldHitTargetKind::Row: + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Widgets::UIEditorAssetFieldHitTargetKind::ValueBox: + case Widgets::UIEditorAssetFieldHitTargetKind::PickerButton: + case Widgets::UIEditorAssetFieldHitTargetKind::ClearButton: + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + + case Widgets::UIEditorAssetFieldHitTargetKind::None: + default: + break; + } +} + +} // namespace + +void PruneAssetFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + state.assetFieldInteractionStates.erase( + std::remove_if( + state.assetFieldInteractionStates.begin(), + state.assetFieldInteractionStates.end(), + [&layout, §ions]( + const UIEditorPropertyGridAssetFieldInteractionEntry& entry) { + return !IsVisibleAssetFieldId(layout, sections, entry.fieldId); + }), + state.assetFieldInteractionStates.end()); + + state.propertyGridState.assetFieldStates.erase( + std::remove_if( + state.propertyGridState.assetFieldStates.begin(), + state.propertyGridState.assetFieldStates.end(), + [&layout, §ions]( + const Widgets::UIEditorPropertyGridAssetFieldVisualState& entry) { + return !IsVisibleAssetFieldId(layout, sections, entry.fieldId); + }), + state.propertyGridState.assetFieldStates.end()); +} + +bool ProcessAssetFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const Widgets::UIEditorAssetFieldMetrics assetMetrics = + BuildUIEditorPropertyGridAssetFieldMetrics(metrics); + bool handled = false; + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != UIEditorPropertyGridFieldKind::Asset) { + continue; + } + + UIEditorAssetFieldInteractionState assetState = + BuildInteractionState(state, field.fieldId); + const bool hadFocus = assetState.fieldState.focused; + Widgets::UIEditorAssetFieldSpec spec = + Widgets::Internal::BuildAssetFieldSpec(field); + const UIEditorAssetFieldInteractionFrame frame = + UpdateUIEditorAssetFieldInteraction( + assetState, + spec, + layout.fieldRowRects[visibleFieldIndex], + { event }, + assetMetrics); + + if (!HasMeaningfulResult(frame.result, assetState, hadFocus)) { + continue; + } + + handled = true; + CopySpecToField(field, spec); + StoreInteractionState(state, field.fieldId, assetState); + state.propertyGridState.focused = assetState.fieldState.focused; + state.propertyGridState.pressedFieldId.clear(); + + if (frame.result.consumed || + frame.result.focusChanged || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested || + hadFocus || + assetState.fieldState.focused) { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + result.activeFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId && + (frame.result.hitTarget.kind != + Widgets::UIEditorAssetFieldHitTargetKind::None || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested || + hadFocus || + assetState.fieldState.focused)) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + if (frame.result.hitTarget.kind != + Widgets::UIEditorAssetFieldHitTargetKind::None || + frame.result.valueChanged || + frame.result.activateRequested || + frame.result.pickerRequested) { + ClosePopup(state, result); + } + + result.consumed = result.consumed || frame.result.consumed; + if (frame.result.valueChanged) { + SetChangedValueResult(field, result); + } + if (frame.result.pickerRequested) { + result.pickerRequested = true; + result.requestedFieldId = field.fieldId; + } + if (frame.result.activateRequested) { + result.activateRequested = true; + if (result.requestedFieldId.empty()) { + result.requestedFieldId = field.fieldId; + } + } + + AssignPropertyGridHitTarget( + result, + sectionIndex, + fieldIndex, + visibleFieldIndex, + frame.result.hitTarget.kind); + } + + return handled; +} + +} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp b/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp index 461325e6..1ba3091c 100644 --- a/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp +++ b/new_editor/src/Fields/PropertyGridInteractionHelpers.cpp @@ -118,6 +118,10 @@ void MergeInteractionResult( accumulated.fieldValueChanged || current.fieldValueChanged; accumulated.secondaryClicked = accumulated.secondaryClicked || current.secondaryClicked; + accumulated.pickerRequested = + accumulated.pickerRequested || current.pickerRequested; + accumulated.activateRequested = + accumulated.activateRequested || current.activateRequested; if (current.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None) { accumulated.hitTarget = current.hitTarget; @@ -143,6 +147,9 @@ void MergeInteractionResult( if (!current.changedValue.empty()) { accumulated.changedValue = current.changedValue; } + if (!current.requestedFieldId.empty()) { + accumulated.requestedFieldId = current.requestedFieldId; + } } void SyncHoverTarget( diff --git a/new_editor/src/Fields/PropertyGridInteractionInternal.h b/new_editor/src/Fields/PropertyGridInteractionInternal.h index ce224175..42da9474 100644 --- a/new_editor/src/Fields/PropertyGridInteractionInternal.h +++ b/new_editor/src/Fields/PropertyGridInteractionInternal.h @@ -42,6 +42,10 @@ void StoreColorFieldVisualState( UIEditorPropertyGridInteractionState& state, std::string_view fieldId, const UIEditorColorFieldInteractionState& interactionState); +void PruneAssetFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const Widgets::UIEditorPropertyGridLayout& layout, + const std::vector& sections); void PruneColorFieldVisualStates( UIEditorPropertyGridInteractionState& state, const Widgets::UIEditorPropertyGridLayout& layout, @@ -127,5 +131,45 @@ bool ProcessColorFieldEvent( const Widgets::UIEditorPropertyGridMetrics& metrics, const ::XCEngine::UI::UIInputEvent& event, UIEditorPropertyGridInteractionResult& result); +bool ProcessAssetFieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); +void PruneVectorFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const Widgets::UIEditorPropertyGridLayout& layout, + const std::vector& sections); +bool ProcessVector2FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); +bool ProcessVector3FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); +bool ProcessVector4FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const Widgets::UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result); } // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInteractionVector.cpp b/new_editor/src/Fields/PropertyGridInteractionVector.cpp new file mode 100644 index 00000000..61c8ac26 --- /dev/null +++ b/new_editor/src/Fields/PropertyGridInteractionVector.cpp @@ -0,0 +1,627 @@ +#include "Fields/PropertyGridInteractionInternal.h" + +#include + +#include +#include + +namespace XCEngine::UI::Editor::Widgets::Internal { + +const UIEditorPropertyGridVector2FieldVisualState* FindVector2FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector2FieldVisualState& entry : + state.vector2FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridVector3FieldVisualState* FindVector3FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector3FieldVisualState& entry : + state.vector3FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +const UIEditorPropertyGridVector4FieldVisualState* FindVector4FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridVector4FieldVisualState& entry : + state.vector4FieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +} // namespace XCEngine::UI::Editor::Widgets::Internal + +namespace XCEngine::UI::Editor::Internal { + +using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; +using Widgets::UIEditorPropertyGridHitTargetKind; +using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; + +namespace { + +template +Entry* FindMutableEntry( + std::vector& entries, + std::string_view fieldId) { + for (Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +const Entry* FindEntry( + const std::vector& entries, + std::string_view fieldId) { + for (const Entry& entry : entries) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + +template +typename Traits::InteractionState BuildInteractionState( + const UIEditorPropertyGridInteractionState& state, + std::string_view fieldId) { + typename Traits::InteractionState interactionState = {}; + const auto& interactionEntries = Traits::InteractionEntries(state); + if (const auto* entry = FindEntry(interactionEntries, fieldId); + entry != nullptr) { + interactionState = entry->state; + } + + interactionState.pointerPosition = state.pointerPosition; + interactionState.hasPointerPosition = state.hasPointerPosition; + return interactionState; +} + +template +void StoreInteractionState( + UIEditorPropertyGridInteractionState& state, + std::string_view fieldId, + const typename Traits::InteractionState& interactionState) { + auto& interactionEntries = Traits::InteractionEntries(state); + if (auto* entry = FindMutableEntry(interactionEntries, fieldId); + entry != nullptr) { + entry->state = interactionState; + } else { + typename Traits::InteractionEntry interactionEntry = {}; + interactionEntry.fieldId = std::string(fieldId); + interactionEntry.state = interactionState; + interactionEntries.push_back(std::move(interactionEntry)); + } + + auto& visualStates = Traits::VisualStates(state.propertyGridState); + if (auto* visualState = + FindMutableEntry(visualStates, fieldId); + visualState != nullptr) { + visualState->state = Traits::FieldState(interactionState); + return; + } + + typename Traits::VisualStateEntry visualState = {}; + visualState.fieldId = std::string(fieldId); + visualState.state = Traits::FieldState(interactionState); + visualStates.push_back(std::move(visualState)); +} + +template +void PruneStateEntries( + std::vector& interactionEntries, + std::vector& visualStates, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + const auto isVisibleTypedFieldId = [&layout, §ions](std::string_view fieldId) { + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + return sectionIndex < sections.size() && + fieldIndex < sections[sectionIndex].fields.size() && + sections[sectionIndex].fields[fieldIndex].kind == Traits::kFieldKind; + }; + + interactionEntries.erase( + std::remove_if( + interactionEntries.begin(), + interactionEntries.end(), + [&isVisibleTypedFieldId](const typename Traits::InteractionEntry& entry) { + return !isVisibleTypedFieldId(entry.fieldId); + }), + interactionEntries.end()); + visualStates.erase( + std::remove_if( + visualStates.begin(), + visualStates.end(), + [&isVisibleTypedFieldId](const typename Traits::VisualStateEntry& entry) { + return !isVisibleTypedFieldId(entry.fieldId); + }), + visualStates.end()); +} + +template +bool HasMeaningfulResult( + const typename Traits::InteractionResult& result, + const typename Traits::InteractionState& interactionState, + bool hadFocus, + bool hadEditing) { + return result.consumed || + result.focusChanged || + result.valueChanged || + result.stepApplied || + result.selectionChanged || + result.editStarted || + result.editCommitted || + result.editCommitRejected || + result.editCanceled || + result.hitTarget.kind != Traits::kNoneHitTargetKind || + hadFocus || + hadEditing || + Traits::FieldState(interactionState).focused || + Traits::FieldState(interactionState).editing; +} + +template +void CopyValuesToField( + UIEditorPropertyGridField& field, + const typename Traits::Spec& spec); + +template +void MergeVectorInteractionResult( + UIEditorPropertyGridInteractionResult& result, + const typename Traits::InteractionResult& fieldResult) { + result.consumed = result.consumed || fieldResult.consumed; + result.selectionChanged = result.selectionChanged || fieldResult.selectionChanged; + result.editStarted = result.editStarted || fieldResult.editStarted; + result.editCommitted = result.editCommitted || fieldResult.editCommitted; + result.editCommitRejected = + result.editCommitRejected || fieldResult.editCommitRejected; + result.editCanceled = result.editCanceled || fieldResult.editCanceled; +} + +template +bool ProcessVectorFieldEventImpl( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + const typename Traits::Metrics vectorMetrics = + Traits::BuildMetrics(metrics); + bool handled = false; + + for (std::size_t visibleFieldIndex = 0u; + visibleFieldIndex < layout.visibleFieldIndices.size(); + ++visibleFieldIndex) { + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + continue; + } + + UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + if (field.kind != Traits::kFieldKind) { + continue; + } + + typename Traits::InteractionState vectorState = + BuildInteractionState(state, field.fieldId); + const bool hadFocus = Traits::FieldState(vectorState).focused; + const bool hadEditing = Traits::FieldState(vectorState).editing; + typename Traits::Spec spec = Traits::BuildSpec(field); + const typename Traits::InteractionFrame frame = + Traits::Update( + vectorState, + spec, + layout.fieldRowRects[visibleFieldIndex], + { event }, + vectorMetrics); + + if (!HasMeaningfulResult( + frame.result, + vectorState, + hadFocus, + hadEditing)) { + continue; + } + + handled = true; + CopyValuesToField(field, spec); + StoreInteractionState(state, field.fieldId, vectorState); + state.propertyGridState.focused = + Traits::FieldState(vectorState).focused; + state.propertyGridState.pressedFieldId.clear(); + + if (frame.result.consumed || + frame.result.selectionChanged || + frame.result.editStarted || + frame.result.valueChanged || + frame.result.stepApplied || + frame.result.editCommitted || + frame.result.editCanceled || + hadFocus || + Traits::FieldState(vectorState).focused) { + result.selectionChanged = + selectionModel.SetSelection(field.fieldId) || result.selectionChanged; + result.selectedFieldId = field.fieldId; + result.activeFieldId = field.fieldId; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + } + + if (propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() != field.fieldId && + (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.selectionChanged || + frame.result.editStarted)) { + CommitActiveEdit(state, propertyEditModel, sections, result); + } + + if (frame.result.hitTarget.kind != Traits::kNoneHitTargetKind || + frame.result.selectionChanged || + frame.result.editStarted) { + ClosePopup(state, result); + } + + MergeVectorInteractionResult(result, frame.result); + if (frame.result.valueChanged || frame.result.stepApplied) { + SetChangedValueResult(field, result); + } + + switch (frame.result.hitTarget.kind) { + case Traits::kRowHitTargetKind: + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + case Traits::kComponentHitTargetKind: + result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox; + result.hitTarget.sectionIndex = sectionIndex; + result.hitTarget.fieldIndex = fieldIndex; + result.hitTarget.visibleFieldIndex = visibleFieldIndex; + break; + default: + break; + } + } + + return handled; +} + +struct Vector2Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector2FieldVisualState; + using InteractionEntry = UIEditorPropertyGridVector2FieldInteractionEntry; + using InteractionState = UIEditorVector2FieldInteractionState; + using InteractionResult = UIEditorVector2FieldInteractionResult; + using InteractionFrame = UIEditorVector2FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector2FieldMetrics; + using Spec = Widgets::UIEditorVector2FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector2; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector2FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector2FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector2FieldStates; + } + + static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { + return state.vector2FieldInteractionStates; + } + + static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { + return state.vector2FieldInteractionStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector2FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector2FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector2FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector2FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector2FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +struct Vector3Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector3FieldVisualState; + using InteractionEntry = UIEditorPropertyGridVector3FieldInteractionEntry; + using InteractionState = UIEditorVector3FieldInteractionState; + using InteractionResult = UIEditorVector3FieldInteractionResult; + using InteractionFrame = UIEditorVector3FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector3FieldMetrics; + using Spec = Widgets::UIEditorVector3FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector3; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector3FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector3FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector3FieldStates; + } + + static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { + return state.vector3FieldInteractionStates; + } + + static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { + return state.vector3FieldInteractionStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector3FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector3FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector3FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector3FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector3FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +struct Vector4Traits { + using VisualStateEntry = Widgets::UIEditorPropertyGridVector4FieldVisualState; + using InteractionEntry = UIEditorPropertyGridVector4FieldInteractionEntry; + using InteractionState = UIEditorVector4FieldInteractionState; + using InteractionResult = UIEditorVector4FieldInteractionResult; + using InteractionFrame = UIEditorVector4FieldInteractionFrame; + using Metrics = Widgets::UIEditorVector4FieldMetrics; + using Spec = Widgets::UIEditorVector4FieldSpec; + + static constexpr UIEditorPropertyGridFieldKind kFieldKind = + UIEditorPropertyGridFieldKind::Vector4; + static constexpr auto kNoneHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::None; + static constexpr auto kRowHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::Row; + static constexpr auto kComponentHitTargetKind = + Widgets::UIEditorVector4FieldHitTargetKind::Component; + + static auto& VisualStates(Widgets::UIEditorPropertyGridState& state) { + return state.vector4FieldStates; + } + + static const auto& VisualStates(const Widgets::UIEditorPropertyGridState& state) { + return state.vector4FieldStates; + } + + static auto& InteractionEntries(UIEditorPropertyGridInteractionState& state) { + return state.vector4FieldInteractionStates; + } + + static const auto& InteractionEntries(const UIEditorPropertyGridInteractionState& state) { + return state.vector4FieldInteractionStates; + } + + static auto& FieldState(InteractionState& state) { + return state.vector4FieldState; + } + + static const auto& FieldState(const InteractionState& state) { + return state.vector4FieldState; + } + + static Spec BuildSpec(const UIEditorPropertyGridField& field) { + return Widgets::Internal::BuildVector4FieldSpec(field); + } + + static Metrics BuildMetrics(const Widgets::UIEditorPropertyGridMetrics& metrics) { + return BuildUIEditorPropertyGridVector4FieldMetrics(metrics); + } + + static InteractionFrame Update( + InteractionState& state, + Spec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Metrics& metrics) { + return UpdateUIEditorVector4FieldInteraction( + state, + spec, + bounds, + inputEvents, + metrics); + } +}; + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector2Traits::Spec& spec) { + field.vector2Value.values = spec.values; +} + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector3Traits::Spec& spec) { + field.vector3Value.values = spec.values; +} + +template <> +void CopyValuesToField( + UIEditorPropertyGridField& field, + const Vector4Traits::Spec& spec) { + field.vector4Value.values = spec.values; +} + +} // namespace + +void PruneVectorFieldVisualStates( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { + PruneStateEntries( + state.vector2FieldInteractionStates, + state.propertyGridState.vector2FieldStates, + layout, + sections); + PruneStateEntries( + state.vector3FieldInteractionStates, + state.propertyGridState.vector3FieldStates, + layout, + sections); + PruneStateEntries( + state.vector4FieldInteractionStates, + state.propertyGridState.vector4FieldStates, + layout, + sections); +} + +bool ProcessVector2FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return ProcessVectorFieldEventImpl( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ProcessVector3FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return ProcessVectorFieldEventImpl( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +bool ProcessVector4FieldEvent( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridLayout& layout, + std::vector& sections, + const Widgets::UIEditorPropertyGridMetrics& metrics, + const ::XCEngine::UI::UIInputEvent& event, + UIEditorPropertyGridInteractionResult& result) { + return ProcessVectorFieldEventImpl( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + result); +} + +} // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Fields/PropertyGridInternal.h b/new_editor/src/Fields/PropertyGridInternal.h index f05b5827..8048df63 100644 --- a/new_editor/src/Fields/PropertyGridInternal.h +++ b/new_editor/src/Fields/PropertyGridInternal.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,7 @@ float ResolveFieldRowHeight( UIEditorBoolFieldSpec BuildBoolFieldSpec(const UIEditorPropertyGridField& field); UIEditorNumberFieldSpec BuildNumberFieldSpec(const UIEditorPropertyGridField& field); UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field); +UIEditorAssetFieldSpec BuildAssetFieldSpec(const UIEditorPropertyGridField& field); UIEditorColorFieldSpec BuildColorFieldSpec(const UIEditorPropertyGridField& field); UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field); UIEditorVector2FieldSpec BuildVector2FieldSpec(const UIEditorPropertyGridField& field); @@ -55,6 +57,9 @@ UIEditorNumberFieldHitTargetKind ResolveNumberHoveredTarget( UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget( const UIEditorPropertyGridState& state, const UIEditorPropertyGridField& field); +UIEditorAssetFieldHitTargetKind ResolveAssetHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field); UIEditorColorFieldHitTargetKind ResolveColorHoveredTarget( const UIEditorPropertyGridState& state, const UIEditorPropertyGridField& field); @@ -73,9 +78,21 @@ UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget( std::vector BuildEnumPopupItems( const UIEditorPropertyGridField& field); +const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId); const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( const UIEditorPropertyGridState& state, std::string_view fieldId); +const UIEditorPropertyGridVector2FieldVisualState* FindVector2FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId); +const UIEditorPropertyGridVector3FieldVisualState* FindVector3FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId); +const UIEditorPropertyGridVector4FieldVisualState* FindVector4FieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId); ::XCEngine::UI::UIRect ResolvePopupViewportRect( const ::XCEngine::UI::UIRect& bounds); bool BuildEnumPopupRuntime( diff --git a/new_editor/src/Fields/PropertyGridRendering.cpp b/new_editor/src/Fields/PropertyGridRendering.cpp index 71a51a33..af3b2633 100644 --- a/new_editor/src/Fields/PropertyGridRendering.cpp +++ b/new_editor/src/Fields/PropertyGridRendering.cpp @@ -77,6 +77,22 @@ UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field) return spec; } +UIEditorAssetFieldSpec BuildAssetFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorAssetFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.assetId = field.assetValue.assetId; + spec.displayName = field.assetValue.displayName; + spec.statusText = field.assetValue.statusText; + spec.emptyText = field.assetValue.emptyText; + spec.tint = field.assetValue.tint; + spec.readOnly = field.readOnly; + spec.showPickerButton = field.assetValue.showPickerButton; + spec.allowClear = field.assetValue.allowClear; + spec.showStatusBadge = field.assetValue.showStatusBadge; + return spec; +} + UIEditorColorFieldSpec BuildColorFieldSpec(const UIEditorPropertyGridField& field) { UIEditorColorFieldSpec spec = {}; spec.fieldId = field.fieldId; @@ -167,6 +183,14 @@ UIEditorPropertyGridFieldRects ResolveFieldRects( return { fieldLayout.labelRect, fieldLayout.valueRect }; } + case UIEditorPropertyGridFieldKind::Asset: { + const UIEditorAssetFieldLayout fieldLayout = BuildUIEditorAssetFieldLayout( + rowRect, + BuildAssetFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + case UIEditorPropertyGridFieldKind::Color: { const UIEditorColorFieldLayout fieldLayout = BuildUIEditorColorFieldLayout( rowRect, @@ -258,6 +282,18 @@ UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget( : UIEditorEnumFieldHitTargetKind::Row; } +UIEditorAssetFieldHitTargetKind ResolveAssetHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorAssetFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorAssetFieldHitTargetKind::ValueBox + : UIEditorAssetFieldHitTargetKind::Row; +} + UIEditorColorFieldHitTargetKind ResolveColorHoveredTarget( const UIEditorPropertyGridState& state, const UIEditorPropertyGridField& field) { @@ -352,6 +388,19 @@ const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState( return nullptr; } +const UIEditorPropertyGridAssetFieldVisualState* FindAssetFieldVisualState( + const UIEditorPropertyGridState& state, + std::string_view fieldId) { + for (const UIEditorPropertyGridAssetFieldVisualState& entry : + state.assetFieldStates) { + if (entry.fieldId == fieldId) { + return &entry; + } + } + + return nullptr; +} + UIRect ResolvePopupViewportRect(const UIRect& bounds) { return UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); } @@ -533,6 +582,10 @@ void AppendUIEditorPropertyGridForeground( ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(metrics); const UIEditorNumberFieldPalette numberPalette = ::XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldPalette(palette); + const UIEditorAssetFieldMetrics assetMetrics = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics(metrics); + const UIEditorAssetFieldPalette assetPalette = + ::XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldPalette(palette); const UIEditorTextFieldMetrics textMetrics = ::XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(metrics); const UIEditorTextFieldPalette textPalette = @@ -624,6 +677,31 @@ void AppendUIEditorPropertyGridForeground( break; } + case UIEditorPropertyGridFieldKind::Asset: { + UIEditorAssetFieldState fieldState = {}; + if (const auto* visualState = + Internal::FindAssetFieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveAssetHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorAssetFieldHitTargetKind::ValueBox + : UIEditorAssetFieldHitTargetKind::None; + fieldState.focused = state.focused; + } + + AppendUIEditorAssetField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + Internal::BuildAssetFieldSpec(field), + fieldState, + assetPalette, + assetMetrics); + break; + } + case UIEditorPropertyGridFieldKind::Color: { UIEditorColorFieldState fieldState = {}; if (const auto* visualState = @@ -648,12 +726,18 @@ void AppendUIEditorPropertyGridForeground( case UIEditorPropertyGridFieldKind::Vector2: { UIEditorVector2FieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveVector2HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector2FieldHitTargetKind::Row - : UIEditorVector2FieldHitTargetKind::None; - fieldState.focused = state.focused; + if (const auto* visualState = + Internal::FindVector2FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector2HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector2FieldHitTargetKind::Row + : UIEditorVector2FieldHitTargetKind::None; + fieldState.focused = state.focused; + } AppendUIEditorVector2Field( drawList, layout.fieldRowRects[visibleFieldIndex], @@ -666,12 +750,18 @@ void AppendUIEditorPropertyGridForeground( case UIEditorPropertyGridFieldKind::Vector3: { UIEditorVector3FieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveVector3HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector3FieldHitTargetKind::Row - : UIEditorVector3FieldHitTargetKind::None; - fieldState.focused = state.focused; + if (const auto* visualState = + Internal::FindVector3FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector3HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector3FieldHitTargetKind::Row + : UIEditorVector3FieldHitTargetKind::None; + fieldState.focused = state.focused; + } AppendUIEditorVector3Field( drawList, layout.fieldRowRects[visibleFieldIndex], @@ -684,12 +774,18 @@ void AppendUIEditorPropertyGridForeground( case UIEditorPropertyGridFieldKind::Vector4: { UIEditorVector4FieldState fieldState = {}; - fieldState.hoveredTarget = Internal::ResolveVector4HoveredTarget(state, field); - fieldState.activeTarget = - state.pressedFieldId == field.fieldId - ? UIEditorVector4FieldHitTargetKind::Row - : UIEditorVector4FieldHitTargetKind::None; - fieldState.focused = state.focused; + if (const auto* visualState = + Internal::FindVector4FieldVisualState(state, field.fieldId); + visualState != nullptr) { + fieldState = visualState->state; + } else { + fieldState.hoveredTarget = Internal::ResolveVector4HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector4FieldHitTargetKind::Row + : UIEditorVector4FieldHitTargetKind::None; + fieldState.focused = state.focused; + } AppendUIEditorVector4Field( drawList, layout.fieldRowRects[visibleFieldIndex], diff --git a/new_editor/src/Fields/UIEditorPropertyGrid.cpp b/new_editor/src/Fields/UIEditorPropertyGrid.cpp index e3a52c7b..04103444 100644 --- a/new_editor/src/Fields/UIEditorPropertyGrid.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGrid.cpp @@ -52,6 +52,9 @@ std::string ResolveUIEditorPropertyGridFieldValueText( case UIEditorPropertyGridFieldKind::Enum: return ResolveUIEditorEnumFieldValueText(Internal::BuildEnumFieldSpec(field)); + case UIEditorPropertyGridFieldKind::Asset: + return ResolveUIEditorAssetFieldValueText(Internal::BuildAssetFieldSpec(field)); + case UIEditorPropertyGridFieldKind::Color: return Internal::FormatColorValueText(field); diff --git a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp index 0ba32a43..27008d05 100644 --- a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp @@ -40,7 +40,9 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { UIEditorPropertyGridLayout layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); Internal::SyncHoverTarget(state, layout, sections, popupMetrics); @@ -73,12 +75,90 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( eventResult)) { Internal::MergeInteractionResult(interactionResult, eventResult); layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); Internal::SyncHoverTarget(state, layout, sections, popupMetrics); continue; } + UIEditorPropertyGridInteractionResult vectorEventResult = {}; + const bool vectorHandled = + Internal::ProcessVector2FieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + vectorEventResult) || + Internal::ProcessVector3FieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + vectorEventResult) || + Internal::ProcessVector4FieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + vectorEventResult); + if (vectorHandled) { + Internal::MergeInteractionResult(eventResult, vectorEventResult); + layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); + Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); + Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); + Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + if (vectorEventResult.consumed || + vectorEventResult.hitTarget.kind != + UIEditorPropertyGridHitTargetKind::None || + vectorEventResult.selectionChanged || + vectorEventResult.editStarted) { + Internal::MergeInteractionResult(interactionResult, eventResult); + continue; + } + } + + UIEditorPropertyGridInteractionResult assetEventResult = {}; + if (Internal::ProcessAssetFieldEvent( + state, + selectionModel, + propertyEditModel, + layout, + sections, + metrics, + event, + assetEventResult)) { + Internal::MergeInteractionResult(eventResult, assetEventResult); + layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); + Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); + Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); + Internal::SyncHoverTarget(state, layout, sections, popupMetrics); + if (assetEventResult.consumed || + assetEventResult.hitTarget.kind != + UIEditorPropertyGridHitTargetKind::None || + assetEventResult.selectionChanged || + assetEventResult.fieldValueChanged || + assetEventResult.pickerRequested || + assetEventResult.activateRequested) { + Internal::MergeInteractionResult(interactionResult, eventResult); + continue; + } + } + switch (event.type) { case UIInputEventType::FocusGained: state.propertyGridState.focused = true; @@ -416,7 +496,9 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( } layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); Internal::SyncHoverTarget(state, layout, sections, popupMetrics); if (eventResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && @@ -438,18 +520,23 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( eventResult.popupClosed || eventResult.fieldValueChanged || eventResult.secondaryClicked || + eventResult.pickerRequested || + eventResult.activateRequested || eventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None || !eventResult.toggledSectionId.empty() || !eventResult.selectedFieldId.empty() || !eventResult.activeFieldId.empty() || !eventResult.committedFieldId.empty() || - !eventResult.changedFieldId.empty()) { + !eventResult.changedFieldId.empty() || + !eventResult.requestedFieldId.empty()) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); + Internal::PruneAssetFieldVisualStates(state, layout, sections); Internal::PruneColorFieldVisualStates(state, layout, sections); + Internal::PruneVectorFieldVisualStates(state, layout, sections); Internal::SyncKeyboardNavigation(state, selectionModel, layout, sections); Internal::SyncHoverTarget(state, layout, sections, popupMetrics); if (interactionResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && diff --git a/new_editor/src/Panels/UIEditorPanelContentHost.cpp b/new_editor/src/Panels/UIEditorPanelContentHost.cpp index 3c9cf2e5..66d01dc4 100644 --- a/new_editor/src/Panels/UIEditorPanelContentHost.cpp +++ b/new_editor/src/Panels/UIEditorPanelContentHost.cpp @@ -30,16 +30,8 @@ bool AreRectsEquivalent( lhs.height == rhs.height; } -bool SupportsBinding( - const UIEditorPanelRegistry& panelRegistry, - const UIEditorPanelContentHostBinding& binding) { - if (!IsUIEditorPanelPresentationExternallyHosted(binding.kind)) { - return false; - } - - const UIEditorPanelDescriptor* descriptor = - FindUIEditorPanelDescriptor(panelRegistry, binding.panelId); - return descriptor != nullptr && descriptor->presentationKind == binding.kind; +bool SupportsExternalHosting(const UIEditorPanelDescriptor& descriptor) { + return IsUIEditorPanelPresentationExternallyHosted(descriptor.presentationKind); } UIEditorPanelContentHostPanelState* FindMutablePanelState( @@ -130,23 +122,22 @@ const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( const Widgets::UIEditorDockHostLayout& dockHostLayout, - const UIEditorPanelRegistry& panelRegistry, - const std::vector& bindings) { + const UIEditorPanelRegistry& panelRegistry) { UIEditorPanelContentHostRequest request = {}; - for (const UIEditorPanelContentHostBinding& binding : bindings) { - if (!SupportsBinding(panelRegistry, binding)) { + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + if (!SupportsExternalHosting(descriptor)) { continue; } const ::XCEngine::UI::UIRect* bodyRect = - FindVisiblePanelBodyRect(dockHostLayout, binding.panelId); + FindVisiblePanelBodyRect(dockHostLayout, descriptor.panelId); if (bodyRect == nullptr) { continue; } UIEditorPanelContentHostMountRequest mountRequest = {}; - mountRequest.panelId = binding.panelId; - mountRequest.kind = binding.kind; + mountRequest.panelId = descriptor.panelId; + mountRequest.kind = descriptor.presentationKind; mountRequest.bounds = *bodyRect; request.mountRequests.push_back(std::move(mountRequest)); } @@ -157,17 +148,16 @@ UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( UIEditorPanelContentHostState& state, const UIEditorPanelContentHostRequest& request, - const UIEditorPanelRegistry& panelRegistry, - const std::vector& bindings) { + const UIEditorPanelRegistry& panelRegistry) { UIEditorPanelContentHostFrame frame = {}; std::unordered_set supportedPanelIds = {}; - for (const UIEditorPanelContentHostBinding& binding : bindings) { - if (!SupportsBinding(panelRegistry, binding)) { + for (const UIEditorPanelDescriptor& descriptor : panelRegistry.panels) { + if (!SupportsExternalHosting(descriptor)) { continue; } - supportedPanelIds.insert(binding.panelId); - EnsurePanelState(state, binding.panelId, binding.kind); + supportedPanelIds.insert(descriptor.panelId); + EnsurePanelState(state, descriptor.panelId, descriptor.presentationKind); } state.panelStates.erase( diff --git a/new_editor/src/Panels/UIEditorPanelRegistry.cpp b/new_editor/src/Panels/UIEditorPanelRegistry.cpp index c40906bd..c907efb7 100644 --- a/new_editor/src/Panels/UIEditorPanelRegistry.cpp +++ b/new_editor/src/Panels/UIEditorPanelRegistry.cpp @@ -18,7 +18,7 @@ UIEditorPanelRegistryValidationResult MakeValidationError( } // namespace -UIEditorPanelRegistry BuildDefaultEditorShellPanelRegistry() { +UIEditorPanelRegistry BuildEditorFoundationPanelRegistry() { UIEditorPanelRegistry registry = {}; registry.panels = { { diff --git a/new_editor/src/Shell/UIEditorShellAsset.cpp b/new_editor/src/Shell/UIEditorShellAsset.cpp index 5dec3bd7..c677db05 100644 --- a/new_editor/src/Shell/UIEditorShellAsset.cpp +++ b/new_editor/src/Shell/UIEditorShellAsset.cpp @@ -128,16 +128,19 @@ UIEditorWorkspacePanelPresentationModel BuildShellPresentation( presentation.panelId = descriptor.panelId; presentation.kind = descriptor.presentationKind; if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { - presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; - presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell"; - presentation.viewportShellModel.spec.chrome.showTopBar = true; - presentation.viewportShellModel.spec.chrome.showBottomBar = true; + presentation.viewportShellModel.spec = descriptor.viewportShellSpec; + if (presentation.viewportShellModel.spec.chrome.title.empty()) { + presentation.viewportShellModel.spec.chrome.title = descriptor.defaultTitle; + } + if (presentation.viewportShellModel.spec.chrome.subtitle.empty()) { + presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell"; + } presentation.viewportShellModel.frame.statusText = descriptor.defaultTitle; } return presentation; } -UIEditorShellInteractionDefinition BuildDefaultShellDefinition( +UIEditorShellInteractionDefinition BuildFoundationShellDefinition( const UIEditorPanelRegistry& panelRegistry, const UIEditorWorkspaceModel& workspace) { UIEditorShellInteractionDefinition definition = {}; @@ -154,13 +157,13 @@ UIEditorShellInteractionDefinition BuildDefaultShellDefinition( } // namespace -EditorShellAsset BuildDefaultEditorShellAsset(const std::filesystem::path& repoRoot) { +EditorShellAsset BuildEditorFoundationShellAsset(const std::filesystem::path& repoRoot) { EditorShellAsset asset = {}; asset.captureRootPath = (repoRoot / "new_editor/captures").lexically_normal(); - asset.panelRegistry = BuildDefaultEditorShellPanelRegistry(); - asset.workspace = BuildDefaultEditorShellWorkspaceModel(); + asset.panelRegistry = BuildEditorFoundationPanelRegistry(); + asset.workspace = BuildEditorFoundationWorkspaceModel(); asset.workspaceSession = BuildDefaultUIEditorWorkspaceSession(asset.panelRegistry, asset.workspace); - asset.shellDefinition = BuildDefaultShellDefinition(asset.panelRegistry, asset.workspace); + asset.shellDefinition = BuildFoundationShellDefinition(asset.panelRegistry, asset.workspace); return asset; } diff --git a/new_editor/src/Shell/UIEditorShellCapturePolicy.cpp b/new_editor/src/Shell/UIEditorShellCapturePolicy.cpp new file mode 100644 index 00000000..b542935a --- /dev/null +++ b/new_editor/src/Shell/UIEditorShellCapturePolicy.cpp @@ -0,0 +1,50 @@ +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using Widgets::UIEditorDockHostHitTargetKind; + +bool ContainsPoint( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +} // namespace + +bool ShouldStartImmediateUIEditorShellPointerCapture( + const UIEditorShellInteractionFrame& shellFrame, + const ::XCEngine::UI::UIPoint& clientPoint) { + const Widgets::UIEditorDockHostHitTarget dockHitTarget = + Widgets::HitTestUIEditorDockHost( + shellFrame.workspaceInteractionFrame.dockHostFrame.layout, + clientPoint); + if (dockHitTarget.kind == UIEditorDockHostHitTargetKind::SplitterHandle) { + return true; + } + + for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : + shellFrame.workspaceInteractionFrame.composeFrame.viewportFrames) { + if (!viewportFrame.viewportShellModel.spec.inputBridgeConfig + .capturePointerOnPointerDownInside) { + continue; + } + + if (ContainsPoint( + viewportFrame.viewportShellFrame.slotLayout.bounds, + clientPoint)) { + return true; + } + } + + return false; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Viewport/UIEditorViewportInputBridge.cpp b/new_editor/src/Viewport/UIEditorViewportInputBridge.cpp index ebeccf1a..e7cdf114 100644 --- a/new_editor/src/Viewport/UIEditorViewportInputBridge.cpp +++ b/new_editor/src/Viewport/UIEditorViewportInputBridge.cpp @@ -1,6 +1,7 @@ #include #include +#include namespace XCEngine::UI::Editor { @@ -36,6 +37,26 @@ std::uint8_t ButtonMask(UIPointerButton button) { } } +std::uint8_t ButtonMaskFromModifiers(const ::XCEngine::UI::UIInputModifiers& modifiers) { + std::uint8_t mask = 0u; + if (modifiers.leftMouse) { + mask |= ButtonMask(UIPointerButton::Left); + } + if (modifiers.rightMouse) { + mask |= ButtonMask(UIPointerButton::Right); + } + if (modifiers.middleMouse) { + mask |= ButtonMask(UIPointerButton::Middle); + } + if (modifiers.x1Mouse) { + mask |= ButtonMask(UIPointerButton::X1); + } + if (modifiers.x2Mouse) { + mask |= ButtonMask(UIPointerButton::X2); + } + return mask; +} + bool AnyPointerButtonsDown(const UIEditorViewportInputBridgeState& state) { return state.pointerButtonsDownMask != 0u; } @@ -55,7 +76,7 @@ void ClearInputState(UIEditorViewportInputBridgeState& state) { void UpdatePointerPosition( UIEditorViewportInputBridgeState& state, UIEditorViewportInputBridgeFrame& frame, - const UIRect& inputRect, + const UIRect& localRect, const UIPoint& screenPosition, const ::XCEngine::UI::UIInputModifiers& modifiers) { if (state.hasPointerPosition) { @@ -68,7 +89,7 @@ void UpdatePointerPosition( } state.lastScreenPointerPosition = screenPosition; - state.lastLocalPointerPosition = ToLocalPoint(inputRect, screenPosition); + state.lastLocalPointerPosition = ToLocalPoint(localRect, screenPosition); state.hasPointerPosition = true; state.modifiers = modifiers; @@ -77,6 +98,39 @@ void UpdatePointerPosition( frame.modifiers = state.modifiers; } +void ReconcilePointerCapture( + UIEditorViewportInputBridgeState& state, + UIEditorViewportInputBridgeFrame& frame) { + if (!state.captured) { + return; + } + + const std::uint8_t captureMask = ButtonMask(state.captureButton); + if (captureMask == 0u || + (state.pointerButtonsDownMask & captureMask) == 0u) { + ClearCapture(state); + frame.captureEnded = true; + } +} + +void AppendPointerButtonTransition( + UIEditorViewportInputBridgeFrame& frame, + UIPointerButton button, + bool pressed, + bool inside, + const UIPoint& screenPosition, + const UIPoint& localPointerPosition, + const ::XCEngine::UI::UIInputModifiers& modifiers) { + UIEditorViewportPointerButtonTransition transition = {}; + transition.button = button; + transition.pressed = pressed; + transition.inside = inside; + transition.screenPosition = screenPosition; + transition.localPointerPosition = localPointerPosition; + transition.modifiers = modifiers; + frame.pointerButtonTransitions.push_back(std::move(transition)); +} + } // namespace bool IsUIEditorViewportInputBridgeKeyDown( @@ -97,31 +151,51 @@ UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( const UIRect& inputRect, const std::vector& events, const UIEditorViewportInputBridgeConfig& config) { + return UpdateUIEditorViewportInputBridge( + state, + inputRect, + inputRect, + events, + config); +} + +UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( + UIEditorViewportInputBridgeState& state, + const UIRect& interactionRect, + const UIRect& localRect, + const std::vector& events, + const UIEditorViewportInputBridgeConfig& config) { UIEditorViewportInputBridgeFrame frame = {}; + frame.hasPointerPosition = state.hasPointerPosition; frame.screenPointerPosition = state.lastScreenPointerPosition; frame.localPointerPosition = state.lastLocalPointerPosition; frame.modifiers = state.modifiers; for (const UIInputEvent& event : events) { - const bool inside = ContainsPoint(inputRect, event.position); + const bool inside = ContainsPoint(interactionRect, event.position); switch (event.type) { case UIInputEventType::PointerEnter: case UIInputEventType::PointerMove: - UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + UpdatePointerPosition(state, frame, localRect, event.position, event.modifiers); state.hovered = inside; + state.pointerButtonsDownMask = ButtonMaskFromModifiers(event.modifiers); + ReconcilePointerCapture(state, frame); break; case UIInputEventType::PointerLeave: state.hovered = false; state.lastScreenPointerPosition = event.position; - state.lastLocalPointerPosition = ToLocalPoint(inputRect, event.position); + state.lastLocalPointerPosition = ToLocalPoint(localRect, event.position); frame.screenPointerPosition = state.lastScreenPointerPosition; frame.localPointerPosition = state.lastLocalPointerPosition; frame.modifiers = state.modifiers; break; case UIInputEventType::PointerButtonDown: - UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + UpdatePointerPosition(state, frame, localRect, event.position, event.modifiers); state.hovered = inside; - state.pointerButtonsDownMask |= ButtonMask(event.pointerButton); + state.pointerButtonsDownMask = + static_cast( + ButtonMaskFromModifiers(event.modifiers) | + ButtonMask(event.pointerButton)); frame.changedPointerButton = event.pointerButton; if (inside) { frame.pointerPressedInside = true; @@ -144,25 +218,38 @@ UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( frame.captureEnded = true; } } + AppendPointerButtonTransition( + frame, + event.pointerButton, + true, + inside, + state.lastScreenPointerPosition, + state.lastLocalPointerPosition, + state.modifiers); break; case UIInputEventType::PointerButtonUp: - UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + UpdatePointerPosition(state, frame, localRect, event.position, event.modifiers); state.hovered = inside; - state.pointerButtonsDownMask &= static_cast(~ButtonMask(event.pointerButton)); + state.pointerButtonsDownMask = ButtonMaskFromModifiers(event.modifiers); frame.changedPointerButton = event.pointerButton; if (inside) { frame.pointerReleasedInside = true; } - if (state.captured && - state.captureButton == event.pointerButton && - !AnyPointerButtonsDown(state)) { - ClearCapture(state); - frame.captureEnded = true; - } + AppendPointerButtonTransition( + frame, + event.pointerButton, + false, + inside, + state.lastScreenPointerPosition, + state.lastLocalPointerPosition, + state.modifiers); + ReconcilePointerCapture(state, frame); break; case UIInputEventType::PointerWheel: - UpdatePointerPosition(state, frame, inputRect, event.position, event.modifiers); + UpdatePointerPosition(state, frame, localRect, event.position, event.modifiers); state.hovered = inside; + state.pointerButtonsDownMask = ButtonMaskFromModifiers(event.modifiers); + ReconcilePointerCapture(state, frame); if (inside || state.captured) { frame.wheelDelta += event.wheelDelta; } @@ -208,7 +295,10 @@ UIEditorViewportInputBridgeFrame UpdateUIEditorViewportInputBridge( } } - frame.pointerInside = ContainsPoint(inputRect, state.lastScreenPointerPosition); + frame.hasPointerPosition = state.hasPointerPosition; + frame.pointerInside = + state.hasPointerPosition && + ContainsPoint(interactionRect, state.lastScreenPointerPosition); frame.hovered = state.hovered; frame.focused = state.focused; frame.captured = state.captured; diff --git a/new_editor/src/Viewport/UIEditorViewportShell.cpp b/new_editor/src/Viewport/UIEditorViewportShell.cpp index dfe1f6ed..1078b6db 100644 --- a/new_editor/src/Viewport/UIEditorViewportShell.cpp +++ b/new_editor/src/Viewport/UIEditorViewportShell.cpp @@ -9,6 +9,15 @@ using Widgets::UIEditorViewportSlotFrame; using Widgets::UIEditorViewportSlotLayout; using Widgets::UIEditorViewportSlotState; +bool ContainsPoint( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + UIEditorViewportSlotLayout BuildViewportShellLayout( const ::XCEngine::UI::UIRect& bounds, const UIEditorViewportShellSpec& spec, @@ -25,10 +34,13 @@ UIEditorViewportSlotLayout BuildViewportShellLayout( UIEditorViewportSlotState BuildViewportShellSlotState( const UIEditorViewportShellVisualState& visualState, - const UIEditorViewportInputBridgeFrame& inputFrame) { + const UIEditorViewportInputBridgeFrame& inputFrame, + const UIEditorViewportSlotLayout& slotLayout) { UIEditorViewportSlotState slotState = {}; slotState.focused = inputFrame.focused; - slotState.surfaceHovered = inputFrame.hovered; + slotState.surfaceHovered = + inputFrame.hasPointerPosition && + ContainsPoint(slotLayout.inputRect, inputFrame.screenPointerPosition); slotState.surfaceActive = inputFrame.focused || inputFrame.captured; slotState.inputCaptured = inputFrame.captured; slotState.hoveredToolIndex = visualState.hoveredToolIndex; @@ -62,10 +74,15 @@ UIEditorViewportShellFrame UpdateUIEditorViewportShell( frame.requestedViewportSize = frame.slotLayout.requestedSurfaceSize; frame.inputFrame = UpdateUIEditorViewportInputBridge( state.inputBridgeState, + frame.slotLayout.bounds, frame.slotLayout.inputRect, inputEvents, model.spec.inputBridgeConfig); - frame.slotState = BuildViewportShellSlotState(model.spec.visualState, frame.inputFrame); + frame.slotState = + BuildViewportShellSlotState( + model.spec.visualState, + frame.inputFrame, + frame.slotLayout); return frame; } diff --git a/new_editor/src/Viewport/UIEditorViewportSlot.cpp b/new_editor/src/Viewport/UIEditorViewportSlot.cpp index 4295da40..a11e10da 100644 --- a/new_editor/src/Viewport/UIEditorViewportSlot.cpp +++ b/new_editor/src/Viewport/UIEditorViewportSlot.cpp @@ -377,7 +377,7 @@ void AppendUIEditorViewportSlotForeground( if (!chrome.title.empty()) { drawList.AddText( UIPoint(layout.titleRect.x, layout.topBarRect.y + metrics.titleInsetY), - std::string(chrome.title), + chrome.title, palette.textPrimary, 13.0f); } @@ -385,7 +385,7 @@ void AppendUIEditorViewportSlotForeground( if (!chrome.subtitle.empty()) { drawList.AddText( UIPoint(layout.subtitleRect.x, layout.topBarRect.y + metrics.subtitleInsetY), - std::string(chrome.subtitle), + chrome.subtitle, palette.textSecondary, 10.0f); } diff --git a/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp b/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp index 43568ea1..ddafc2e1 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceCompose.cpp @@ -51,28 +51,6 @@ bool SupportsExternalViewportPresentation( descriptor->presentationKind == UIEditorPanelPresentationKind::ViewportShell; } -std::vector BuildContentHostBindings( - const UIEditorPanelRegistry& panelRegistry, - const std::vector& presentations) { - std::vector bindings = {}; - bindings.reserve(presentations.size()); - for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { - const UIEditorPanelDescriptor* descriptor = - FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId); - if (descriptor == nullptr || - descriptor->presentationKind != presentation.kind || - !IsUIEditorPanelPresentationExternallyHosted(presentation.kind)) { - continue; - } - - UIEditorPanelContentHostBinding binding = {}; - binding.panelId = presentation.panelId; - binding.kind = presentation.kind; - bindings.push_back(std::move(binding)); - } - return bindings; -} - UIEditorWorkspacePanelPresentationState& EnsurePanelState( UIEditorWorkspaceComposeState& state, std::string_view panelId) { @@ -182,12 +160,9 @@ UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( session, dockHostState, dockHostMetrics); - const std::vector contentHostBindings = - BuildContentHostBindings(panelRegistry, presentations); request.contentHostRequest = ResolveUIEditorPanelContentHostRequest( request.dockHostLayout, - panelRegistry, - contentHostBindings); + panelRegistry); for (const UIEditorPanelContentHostMountRequest& mountRequest : request.contentHostRequest.mountRequests) { @@ -234,18 +209,14 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( session, dockHostState, dockHostMetrics); - const std::vector contentHostBindings = - BuildContentHostBindings(panelRegistry, presentations); const UIEditorPanelContentHostRequest contentHostRequest = ResolveUIEditorPanelContentHostRequest( frame.dockHostLayout, - panelRegistry, - contentHostBindings); + panelRegistry); frame.contentHostFrame = UpdateUIEditorPanelContentHost( state.contentHostState, contentHostRequest, - panelRegistry, - contentHostBindings); + panelRegistry); TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations); for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp index 70be3607..f4353780 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp @@ -459,6 +459,7 @@ void CollectVisiblePanelsRecursive( UIEditorWorkspaceValidationResult ValidateNodeRecursive( const UIEditorWorkspaceNode& node, + std::unordered_set& nodeIds, std::unordered_set& panelIds) { if (node.nodeId.empty()) { return MakeValidationError( @@ -466,6 +467,13 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( "Workspace node id must not be empty."); } + if (!nodeIds.insert(node.nodeId).second) { + return MakeValidationError( + UIEditorWorkspaceValidationCode::DuplicateNodeId, + "Workspace node id '" + node.nodeId + + "' is duplicated in the workspace tree."); + } + switch (node.kind) { case UIEditorWorkspaceNodeKind::Panel: if (!node.children.empty()) { @@ -517,7 +525,7 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( } if (UIEditorWorkspaceValidationResult result = - ValidateNodeRecursive(child, panelIds); + ValidateNodeRecursive(child, nodeIds, panelIds); !result.IsValid()) { return result; } @@ -542,7 +550,7 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( for (const UIEditorWorkspaceNode& child : node.children) { if (UIEditorWorkspaceValidationResult result = - ValidateNodeRecursive(child, panelIds); + ValidateNodeRecursive(child, nodeIds, panelIds); !result.IsValid()) { return result; } @@ -591,8 +599,8 @@ bool AreUIEditorWorkspaceModelsEquivalent( AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root); } -UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() { - const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry(); +UIEditorWorkspaceModel BuildEditorFoundationWorkspaceModel() { + const UIEditorPanelRegistry registry = BuildEditorFoundationPanelRegistry(); const UIEditorPanelDescriptor& rootPanel = Internal::RequirePanelDescriptor(registry, "editor-foundation-root"); diff --git a/new_editor/src/Workspace/WorkspaceModelInternal.h b/new_editor/src/Workspace/WorkspaceModelInternal.h index 2ee47ee4..85bab399 100644 --- a/new_editor/src/Workspace/WorkspaceModelInternal.h +++ b/new_editor/src/Workspace/WorkspaceModelInternal.h @@ -91,6 +91,7 @@ void CollectVisiblePanelsRecursive( UIEditorWorkspaceValidationResult ValidateNodeRecursive( const UIEditorWorkspaceNode& node, + std::unordered_set& nodeIds, std::unordered_set& panelIds); } // namespace XCEngine::UI::Editor::Internal diff --git a/new_editor/src/Workspace/WorkspaceModelValidation.cpp b/new_editor/src/Workspace/WorkspaceModelValidation.cpp index 2d5ddb4d..62e6d951 100644 --- a/new_editor/src/Workspace/WorkspaceModelValidation.cpp +++ b/new_editor/src/Workspace/WorkspaceModelValidation.cpp @@ -4,9 +4,10 @@ namespace XCEngine::UI::Editor { UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( const UIEditorWorkspaceModel& workspace) { + std::unordered_set nodeIds = {}; std::unordered_set panelIds = {}; UIEditorWorkspaceValidationResult result = - Internal::ValidateNodeRecursive(workspace.root, panelIds); + Internal::ValidateNodeRecursive(workspace.root, nodeIds, panelIds); if (!result.IsValid()) { return result; } diff --git a/tests/NewEditor/CMakeLists.txt b/tests/NewEditor/CMakeLists.txt deleted file mode 100644 index 09093ddc..00000000 --- a/tests/NewEditor/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -set(NEW_EDITOR_TEST_SOURCES - test_sandbox_frame_builder.cpp - test_structured_editor_shell.cpp -) - -add_executable(new_editor_tests ${NEW_EDITOR_TEST_SOURCES}) - -target_link_libraries(new_editor_tests PRIVATE - XCNewEditorLib - GTest::gtest_main -) - -target_include_directories(new_editor_tests PRIVATE - ${CMAKE_SOURCE_DIR}/new_editor/src - ${CMAKE_SOURCE_DIR}/engine/include -) - -file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCNEWEDITOR_REPO_ROOT_PATH) - -target_compile_definitions(new_editor_tests PRIVATE - XCNEWEDITOR_REPO_ROOT="${XCNEWEDITOR_REPO_ROOT_PATH}" -) - -if(MSVC) - target_compile_options(new_editor_tests PRIVATE /utf-8 /FS) - set_property(TARGET new_editor_tests PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -endif() - -add_test(NAME new_editor_tests COMMAND new_editor_tests) diff --git a/tests/NewEditor/test_sandbox_frame_builder.cpp b/tests/NewEditor/test_sandbox_frame_builder.cpp deleted file mode 100644 index 9063c092..00000000 --- a/tests/NewEditor/test_sandbox_frame_builder.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include - -#include "SandboxFrameBuilder.h" - -#include - -namespace { - -bool DrawDataContainsText( - const XCEngine::UI::UIDrawData& drawData, - const std::string& text) { - for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) { - for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) { - if (command.type == XCEngine::UI::UIDrawCommandType::Text && - command.text == text) { - return true; - } - } - } - - return false; -} - -} // namespace - -TEST(NewEditorSandboxFrameTest, BuildsNativeEditorSandboxPacket) { - XCEngine::NewEditor::SandboxFrameOptions options = {}; - options.width = 1440.0f; - options.height = 900.0f; - options.timeSeconds = 1.0; - - const XCEngine::UI::UIDrawData drawData = - XCEngine::NewEditor::BuildSandboxFrame(options); - - EXPECT_FALSE(drawData.Empty()); - EXPECT_GT(drawData.GetTotalCommandCount(), 40u); - EXPECT_TRUE(DrawDataContainsText(drawData, "XCUI Native Sandbox")); - EXPECT_TRUE(DrawDataContainsText(drawData, "Hierarchy")); - EXPECT_TRUE(DrawDataContainsText(drawData, "Scene")); - EXPECT_TRUE(DrawDataContainsText(drawData, "Inspector")); - EXPECT_TRUE(DrawDataContainsText(drawData, "No ImGui Host")); -} - -TEST(NewEditorSandboxFrameTest, ClampsInvalidSizeToSafeFallback) { - XCEngine::NewEditor::SandboxFrameOptions options = {}; - options.width = 0.0f; - options.height = -10.0f; - - const XCEngine::UI::UIDrawData drawData = - XCEngine::NewEditor::BuildSandboxFrame(options); - - EXPECT_FALSE(drawData.Empty()); - EXPECT_TRUE(DrawDataContainsText(drawData, "XCUI Native Sandbox")); -} diff --git a/tests/NewEditor/test_structured_editor_shell.cpp b/tests/NewEditor/test_structured_editor_shell.cpp deleted file mode 100644 index 7ceb6116..00000000 --- a/tests/NewEditor/test_structured_editor_shell.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include - -#include -#include - -#include -#include -#include - -#ifndef XCNEWEDITOR_REPO_ROOT -#define XCNEWEDITOR_REPO_ROOT "." -#endif - -namespace { - -using XCEngine::UI::UIDrawCommand; -using XCEngine::UI::UIDrawCommandType; -using XCEngine::UI::UIDrawData; -using XCEngine::UI::Runtime::UIScreenAsset; -using XCEngine::UI::Runtime::UIScreenFrameInput; -using XCEngine::UI::Runtime::UIScreenPlayer; -using XCEngine::UI::Runtime::UIDocumentScreenHost; - -std::filesystem::path RepoRelative(const char* relativePath) { - return (std::filesystem::path(XCNEWEDITOR_REPO_ROOT) / relativePath).lexically_normal(); -} - -bool DrawDataContainsText(const UIDrawData& drawData, const std::string& text) { - for (const auto& drawList : drawData.GetDrawLists()) { - for (const UIDrawCommand& command : drawList.GetCommands()) { - if (command.type == UIDrawCommandType::Text && command.text == text) { - return true; - } - } - } - - return false; -} - -bool ContainsPathWithFilename( - const std::vector& paths, - const char* expectedFileName) { - for (const std::string& path : paths) { - if (std::filesystem::path(path).filename() == expectedFileName) { - return true; - } - } - - return false; -} - -} // namespace - -TEST(NewEditorStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResources) { - const std::filesystem::path viewPath = RepoRelative("new_editor/ui/views/editor_shell.xcui"); - const std::filesystem::path themePath = RepoRelative("new_editor/ui/themes/editor_shell.xctheme"); - - ASSERT_TRUE(std::filesystem::exists(viewPath)); - ASSERT_TRUE(std::filesystem::exists(themePath)); - - UIScreenAsset asset = {}; - asset.screenId = "new_editor.editor_shell"; - asset.documentPath = viewPath.string(); - asset.themePath = themePath.string(); - - UIDocumentScreenHost host = {}; - UIScreenPlayer player(host); - - ASSERT_TRUE(player.Load(asset)) << player.GetLastError(); - ASSERT_NE(player.GetDocument(), nullptr); - EXPECT_TRUE(player.GetDocument()->hasThemeDocument); - EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_shell.xctheme")); - - UIScreenFrameInput input = {}; - input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 1440.0f, 900.0f); - input.frameIndex = 1u; - input.focused = true; - - const auto& frame = player.Update(input); - EXPECT_TRUE(frame.stats.documentLoaded); - EXPECT_GT(frame.stats.nodeCount, 6u); - EXPECT_GT(frame.stats.commandCount, 12u); - EXPECT_TRUE(DrawDataContainsText(frame.drawData, "XCUI Core Validation")); - EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Input Core")); - EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Hover / Focus")); - EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pointer Capture")); - EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Route Target")); -} diff --git a/tests/UI/Core/integration/shared/src/InputModifierTracker.h b/tests/UI/Core/integration/shared/src/InputModifierTracker.h index 27c12cda..95903669 100644 --- a/tests/UI/Core/integration/shared/src/InputModifierTracker.h +++ b/tests/UI/Core/integration/shared/src/InputModifierTracker.h @@ -24,6 +24,11 @@ public: m_rightAlt = false; m_leftSuper = false; m_rightSuper = false; + m_leftMouse = false; + m_rightMouse = false; + m_middleMouse = false; + m_x1Mouse = false; + m_x2Mouse = false; } void SyncFromSystemState() { @@ -35,6 +40,11 @@ public: m_rightAlt = (GetKeyState(VK_RMENU) & 0x8000) != 0; m_leftSuper = (GetKeyState(VK_LWIN) & 0x8000) != 0; m_rightSuper = (GetKeyState(VK_RWIN) & 0x8000) != 0; + m_leftMouse = (GetKeyState(VK_LBUTTON) & 0x8000) != 0; + m_rightMouse = (GetKeyState(VK_RBUTTON) & 0x8000) != 0; + m_middleMouse = (GetKeyState(VK_MBUTTON) & 0x8000) != 0; + m_x1Mouse = (GetKeyState(VK_XBUTTON1) & 0x8000) != 0; + m_x2Mouse = (GetKeyState(VK_XBUTTON2) & 0x8000) != 0; } ::XCEngine::UI::UIInputModifiers GetCurrentModifiers() const { @@ -43,11 +53,25 @@ public: ::XCEngine::UI::UIInputModifiers BuildPointerModifiers(std::size_t wParam) const { ::XCEngine::UI::UIInputModifiers modifiers = BuildModifiers(); - modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0; - modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0; + ApplyPointerWParam(modifiers, wParam); return modifiers; } + ::XCEngine::UI::UIInputModifiers ApplyPointerMessage( + ::XCEngine::UI::UIInputEventType type, + ::XCEngine::UI::UIPointerButton button, + std::size_t wParam) { + ::XCEngine::UI::UIInputModifiers modifiers = BuildPointerModifiers(wParam); + if (type == ::XCEngine::UI::UIInputEventType::PointerButtonDown) { + SetPointerButton(modifiers, button, true); + } else if (type == ::XCEngine::UI::UIInputEventType::PointerButtonUp) { + SetPointerButton(modifiers, button, false); + } + + ApplyPointerState(modifiers); + return BuildModifiers(); + } + ::XCEngine::UI::UIInputModifiers ApplyKeyMessage( ::XCEngine::UI::UIInputEventType type, WPARAM wParam, @@ -119,6 +143,52 @@ private: } } + static void ApplyPointerWParam( + ::XCEngine::UI::UIInputModifiers& modifiers, + std::size_t wParam) { + modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0; + modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0; + modifiers.leftMouse = (wParam & MK_LBUTTON) != 0; + modifiers.rightMouse = (wParam & MK_RBUTTON) != 0; + modifiers.middleMouse = (wParam & MK_MBUTTON) != 0; + modifiers.x1Mouse = (wParam & MK_XBUTTON1) != 0; + modifiers.x2Mouse = (wParam & MK_XBUTTON2) != 0; + } + + static void SetPointerButton( + ::XCEngine::UI::UIInputModifiers& modifiers, + ::XCEngine::UI::UIPointerButton button, + bool pressed) { + switch (button) { + case ::XCEngine::UI::UIPointerButton::Left: + modifiers.leftMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::Right: + modifiers.rightMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::Middle: + modifiers.middleMouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::X1: + modifiers.x1Mouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::X2: + modifiers.x2Mouse = pressed; + break; + case ::XCEngine::UI::UIPointerButton::None: + default: + break; + } + } + + void ApplyPointerState(const ::XCEngine::UI::UIInputModifiers& modifiers) { + m_leftMouse = modifiers.leftMouse; + m_rightMouse = modifiers.rightMouse; + m_middleMouse = modifiers.middleMouse; + m_x1Mouse = modifiers.x1Mouse; + m_x2Mouse = modifiers.x2Mouse; + } + void SetModifierState(ModifierKey key, bool pressed) { switch (key) { case ModifierKey::LeftShift: @@ -157,6 +227,11 @@ private: modifiers.control = m_leftControl || m_rightControl; modifiers.alt = m_leftAlt || m_rightAlt; modifiers.super = m_leftSuper || m_rightSuper; + modifiers.leftMouse = m_leftMouse; + modifiers.rightMouse = m_rightMouse; + modifiers.middleMouse = m_middleMouse; + modifiers.x1Mouse = m_x1Mouse; + modifiers.x2Mouse = m_x2Mouse; return modifiers; } @@ -168,6 +243,11 @@ private: bool m_rightAlt = false; bool m_leftSuper = false; bool m_rightSuper = false; + bool m_leftMouse = false; + bool m_rightMouse = false; + bool m_middleMouse = false; + bool m_x1Mouse = false; + bool m_x2Mouse = false; }; } // namespace XCEngine::Tests::CoreUI::Host diff --git a/tests/UI/Core/unit/test_input_modifier_tracker.cpp b/tests/UI/Core/unit/test_input_modifier_tracker.cpp index f03123c3..27ad5ff5 100644 --- a/tests/UI/Core/unit/test_input_modifier_tracker.cpp +++ b/tests/UI/Core/unit/test_input_modifier_tracker.cpp @@ -14,6 +14,7 @@ namespace { using XCEngine::Tests::CoreUI::Host::InputModifierTracker; using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPointerButton; TEST(CoreInputModifierTrackerTest, ControlStatePersistsAcrossChordKeyDownAndClearsOnKeyUp) { InputModifierTracker tracker = {}; @@ -52,11 +53,31 @@ TEST(CoreInputModifierTrackerTest, PointerModifiersMergeMouseFlagsWithTrackedKey VK_MENU, 0x00380001); - const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT); + const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT | MK_RBUTTON); EXPECT_TRUE(modifiers.shift); EXPECT_TRUE(modifiers.alt); EXPECT_FALSE(modifiers.control); EXPECT_FALSE(modifiers.super); + EXPECT_FALSE(modifiers.leftMouse); + EXPECT_TRUE(modifiers.rightMouse); +} + +TEST(CoreInputModifierTrackerTest, PointerMessagesUpdateTrackedMouseButtonState) { + InputModifierTracker tracker = {}; + + const auto leftDown = tracker.ApplyPointerMessage( + UIInputEventType::PointerButtonDown, + UIPointerButton::Left, + 0u); + EXPECT_TRUE(leftDown.leftMouse); + EXPECT_TRUE(tracker.GetCurrentModifiers().leftMouse); + + const auto leftUp = tracker.ApplyPointerMessage( + UIInputEventType::PointerButtonUp, + UIPointerButton::Left, + 0u); + EXPECT_FALSE(leftUp.leftMouse); + EXPECT_FALSE(tracker.GetCurrentModifiers().leftMouse); } TEST(CoreInputModifierTrackerTest, RightControlIsTrackedIndependentlyFromLeftControl) { diff --git a/tests/UI/Editor/CMakeLists.txt b/tests/UI/Editor/CMakeLists.txt index 18cf640e..3183222f 100644 --- a/tests/UI/Editor/CMakeLists.txt +++ b/tests/UI/Editor/CMakeLists.txt @@ -18,9 +18,18 @@ include_directories("${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/app") add_subdirectory(unit) add_subdirectory(integration) +set(EDITOR_UI_UNIT_TEST_TARGETS + editor_ui_tests +) +if(TARGET editor_app_feature_tests) + list(APPEND EDITOR_UI_UNIT_TEST_TARGETS + editor_app_feature_tests + ) +endif() + add_custom_target(editor_ui_unit_tests DEPENDS - editor_ui_tests + ${EDITOR_UI_UNIT_TEST_TARGETS} ) add_custom_target(editor_ui_all_tests diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 6035b826..d169fcec 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -16,6 +16,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_property_grid.cpp test_ui_editor_property_grid_interaction.cpp test_ui_editor_shell_compose.cpp + test_editor_window_input_routing.cpp test_ui_editor_shell_interaction.cpp test_ui_editor_collection_primitives.cpp test_ui_editor_field_row_layout.cpp @@ -52,6 +53,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_tab_strip_interaction.cpp test_ui_editor_tree_view.cpp test_ui_editor_tree_view_interaction.cpp + test_viewport_object_id_picker.cpp test_ui_editor_viewport_input_bridge.cpp test_ui_editor_viewport_shell.cpp test_ui_editor_viewport_slot.cpp @@ -102,9 +104,23 @@ gtest_discover_tests(editor_ui_tests ) if(TARGET XCUIEditorAppLib) - add_executable(editor_app_feature_tests + set(EDITOR_APP_FEATURE_TEST_SOURCES + test_editor_project_runtime.cpp test_project_browser_model.cpp + test_project_panel.cpp test_hierarchy_scene_binding.cpp + test_scene_viewport_render_plan.cpp + test_scene_viewport_runtime.cpp + ) + + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_inspector_presentation.cpp") + list(APPEND EDITOR_APP_FEATURE_TEST_SOURCES + test_inspector_presentation.cpp + ) + endif() + + add_executable(editor_app_feature_tests + ${EDITOR_APP_FEATURE_TEST_SOURCES} ) target_link_libraries(editor_app_feature_tests diff --git a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp index 3c1ffa2d..cc0b7967 100644 --- a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp +++ b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp @@ -1,8 +1,8 @@ #include -#include "Composition/EditorEditCommandRoute.h" -#include "Composition/EditorHostCommandBridge.h" -#include "State/EditorSession.h" +#include +#include +#include namespace { @@ -15,6 +15,18 @@ using XCEngine::UI::Editor::UIEditorHostCommandEvaluationResult; class StubEditCommandRoute final : public EditorEditCommandRoute { public: + UIEditorHostCommandEvaluationResult EvaluateAssetCommand( + std::string_view commandId) const override { + lastEvaluatedAssetCommandId = std::string(commandId); + return assetEvaluationResult; + } + + UIEditorHostCommandDispatchResult DispatchAssetCommand( + std::string_view commandId) override { + lastDispatchedAssetCommandId = std::string(commandId); + return assetDispatchResult; + } + UIEditorHostCommandEvaluationResult EvaluateEditCommand( std::string_view commandId) const override { lastEvaluatedCommandId = std::string(commandId); @@ -27,8 +39,12 @@ public: return dispatchResult; } + mutable std::string lastEvaluatedAssetCommandId = {}; + std::string lastDispatchedAssetCommandId = {}; mutable std::string lastEvaluatedCommandId = {}; std::string lastDispatchedCommandId = {}; + UIEditorHostCommandEvaluationResult assetEvaluationResult = {}; + UIEditorHostCommandDispatchResult assetDispatchResult = {}; UIEditorHostCommandEvaluationResult evaluationResult = {}; UIEditorHostCommandDispatchResult dispatchResult = {}; }; @@ -45,7 +61,7 @@ TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { EditorHostCommandBridge bridge = {}; bridge.BindSession(session); - bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr); + bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr); const UIEditorHostCommandEvaluationResult evaluation = bridge.EvaluateHostCommand("edit.rename"); @@ -82,4 +98,85 @@ TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { "Only file.exit has a bound host owner in the current shell."); } +TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { + EditorSession session = {}; + session.activeRoute = EditorActionRoute::Hierarchy; + + StubEditCommandRoute projectRoute = {}; + projectRoute.assetEvaluationResult.executable = true; + projectRoute.assetEvaluationResult.message = "Project route owns create folder."; + projectRoute.assetDispatchResult.commandExecuted = true; + projectRoute.assetDispatchResult.message = "Project create folder dispatched."; + + EditorHostCommandBridge bridge = {}; + bridge.BindSession(session); + bridge.BindEditCommandRoutes(nullptr, &projectRoute, nullptr); + + const UIEditorHostCommandEvaluationResult evaluation = + bridge.EvaluateHostCommand("assets.create_folder"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Project route owns create folder."); + EXPECT_EQ(projectRoute.lastEvaluatedAssetCommandId, "assets.create_folder"); + + const UIEditorHostCommandDispatchResult dispatch = + bridge.DispatchHostCommand("assets.create_folder"); + EXPECT_TRUE(dispatch.commandExecuted); + EXPECT_EQ(dispatch.message, "Project create folder dispatched."); + EXPECT_EQ(projectRoute.lastDispatchedAssetCommandId, "assets.create_folder"); +} + +TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { + EditorSession session = {}; + session.activeRoute = EditorActionRoute::Scene; + + StubEditCommandRoute sceneRoute = {}; + sceneRoute.evaluationResult.executable = true; + sceneRoute.evaluationResult.message = "Scene route owns undo."; + sceneRoute.dispatchResult.commandExecuted = true; + sceneRoute.dispatchResult.message = "Scene undo dispatched."; + + EditorHostCommandBridge bridge = {}; + bridge.BindSession(session); + bridge.BindEditCommandRoutes(nullptr, nullptr, &sceneRoute); + + const UIEditorHostCommandEvaluationResult evaluation = + bridge.EvaluateHostCommand("edit.undo"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Scene route owns undo."); + EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo"); + + const UIEditorHostCommandDispatchResult dispatch = + bridge.DispatchHostCommand("edit.undo"); + EXPECT_TRUE(dispatch.commandExecuted); + EXPECT_EQ(dispatch.message, "Scene undo dispatched."); + EXPECT_EQ(sceneRoute.lastDispatchedCommandId, "edit.undo"); +} + +TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorRoute) { + EditorSession session = {}; + session.activeRoute = EditorActionRoute::Inspector; + + StubEditCommandRoute inspectorRoute = {}; + inspectorRoute.evaluationResult.executable = true; + inspectorRoute.evaluationResult.message = "Inspector route owns delete."; + inspectorRoute.dispatchResult.commandExecuted = true; + inspectorRoute.dispatchResult.message = "Inspector delete dispatched."; + + EditorHostCommandBridge bridge = {}; + bridge.BindSession(session); + bridge.BindEditCommandRoutes(nullptr, nullptr, nullptr, &inspectorRoute); + + const UIEditorHostCommandEvaluationResult evaluation = + bridge.EvaluateHostCommand("edit.delete"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Inspector route owns delete."); + EXPECT_EQ(inspectorRoute.lastEvaluatedCommandId, "edit.delete"); + + const UIEditorHostCommandDispatchResult dispatch = + bridge.DispatchHostCommand("edit.delete"); + EXPECT_TRUE(dispatch.commandExecuted); + EXPECT_EQ(dispatch.message, "Inspector delete dispatched."); + EXPECT_EQ(inspectorRoute.lastDispatchedCommandId, "edit.delete"); +} + } // namespace diff --git a/tests/UI/Editor/unit/test_editor_project_runtime.cpp b/tests/UI/Editor/unit/test_editor_project_runtime.cpp new file mode 100644 index 00000000..f9a32990 --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_project_runtime.cpp @@ -0,0 +1,172 @@ +#include "Project/EditorProjectRuntime.h" + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +class TemporaryRepo final { +public: + TemporaryRepo() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcengine_editor_project_runtime_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryRepo() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + + bool CreateDirectory(const std::filesystem::path& relativePath) const { + std::error_code errorCode = {}; + std::filesystem::create_directories(m_root / relativePath, errorCode); + return !errorCode; + } + + bool WriteFile( + const std::filesystem::path& relativePath, + std::string_view contents = "test") const { + const std::filesystem::path absolutePath = m_root / relativePath; + std::error_code errorCode = {}; + std::filesystem::create_directories(absolutePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + std::ofstream stream(absolutePath, std::ios::binary); + if (!stream.is_open()) { + return false; + } + + stream << contents; + return stream.good(); + } + +private: + std::filesystem::path m_root = {}; +}; + +TEST(EditorProjectRuntimeTests, NavigateToFolderClearsCurrentProjectSelection) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); + ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc")); + + ASSERT_TRUE(runtime.NavigateToFolder("Assets")); + EXPECT_FALSE(runtime.HasSelection()); + EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None); +} + +TEST(EditorProjectRuntimeTests, OpenSceneItemQueuesSceneOpenRequest) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); + + ASSERT_TRUE(runtime.OpenItem("Assets/Scenes/Main.xc")); + const std::optional openedScene = + runtime.ConsumePendingSceneOpenPath(); + ASSERT_TRUE(openedScene.has_value()); + EXPECT_EQ(openedScene.value(), (repo.Root() / "project/Assets/Scenes/Main.xc")); + EXPECT_FALSE(runtime.ConsumePendingSceneOpenPath().has_value()); +} + +TEST(EditorProjectRuntimeTests, ResolveCommandTargetsFollowRuntimeSelectionAndCurrentFolder) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes")); + ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc")); + + const EditorProjectRuntime::AssetCommandTarget assetTarget = + runtime.ResolveAssetCommandTarget(); + EXPECT_EQ(assetTarget.subjectItemId, "Assets/Scenes/Main.xc"); + EXPECT_EQ(assetTarget.subjectRelativePath, "Assets/Scenes/Main.xc"); + ASSERT_NE(assetTarget.containerFolder, nullptr); + EXPECT_EQ(assetTarget.containerFolder->itemId, "Assets/Scenes"); + + const std::optional editTarget = + runtime.ResolveEditCommandTarget(); + ASSERT_TRUE(editTarget.has_value()); + EXPECT_EQ(editTarget->itemId, "Assets/Scenes/Main.xc"); + EXPECT_FALSE(editTarget->directory); + + runtime.ClearSelection(); + + const EditorProjectRuntime::AssetCommandTarget folderTarget = + runtime.ResolveAssetCommandTarget(); + EXPECT_EQ(folderTarget.subjectItemId, "Assets/Scenes"); + EXPECT_EQ(folderTarget.subjectRelativePath, "Assets/Scenes"); + EXPECT_FALSE(folderTarget.showInExplorerSelectTarget); + + const std::optional folderEditTarget = + runtime.ResolveEditCommandTarget(); + ASSERT_TRUE(folderEditTarget.has_value()); + EXPECT_EQ(folderEditTarget->itemId, "Assets/Scenes"); + EXPECT_TRUE(folderEditTarget->directory); + EXPECT_FALSE(folderEditTarget->assetsRoot); +} + +TEST(EditorProjectRuntimeTests, ResolveEditCommandTargetMarksAssetsRootAndIgnoresBackgroundFallback) { + TemporaryRepo repo = {}; + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + + const std::optional rootTarget = + runtime.ResolveEditCommandTarget(); + ASSERT_TRUE(rootTarget.has_value()); + EXPECT_EQ(rootTarget->itemId, "Assets"); + EXPECT_TRUE(rootTarget->directory); + EXPECT_TRUE(rootTarget->assetsRoot); + + EXPECT_FALSE(runtime.ResolveEditCommandTarget({}, true).has_value()); +} + +TEST(EditorProjectRuntimeTests, RenameSelectedItemRemapsSelectionAndDeleteClearsIt) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts")); + ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs")); + + std::string renamedItemId = {}; + ASSERT_TRUE(runtime.RenameItem("Assets/Scripts/Player.cs", "Hero", &renamedItemId)); + EXPECT_EQ(renamedItemId, "Assets/Scripts/Hero.cs"); + EXPECT_TRUE(runtime.HasSelection()); + EXPECT_EQ(runtime.GetSelection().itemId, "Assets/Scripts/Hero.cs"); + EXPECT_EQ(runtime.GetSelection().displayName, "Hero"); + + ASSERT_TRUE(runtime.DeleteItem("Assets/Scripts/Hero.cs")); + EXPECT_FALSE(runtime.HasSelection()); + EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp index 66112dcd..9917704a 100644 --- a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp +++ b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp @@ -9,7 +9,7 @@ namespace { using XCEngine::Input::KeyCode; -using XCEngine::UI::Editor::BuildDefaultEditorShellAsset; +using XCEngine::UI::Editor::BuildEditorFoundationShellAsset; using XCEngine::UI::Editor::EditorShellAssetValidationCode; using XCEngine::UI::Editor::FindUIEditorPanelDescriptor; using XCEngine::UI::Editor::UIEditorCommandPanelSource; @@ -31,7 +31,7 @@ UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) { } TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) { - const auto shellAsset = BuildDefaultEditorShellAsset("."); + const auto shellAsset = BuildEditorFoundationShellAsset("."); const auto validation = ValidateEditorShellAsset(shellAsset); EXPECT_TRUE(validation.IsValid()) << validation.message; @@ -51,7 +51,7 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) { } TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); auto* documentPanel = const_cast( @@ -65,7 +65,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromR } TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromRegistry) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); shellAsset.workspace.activePanelId = "editor-foundation-root"; ASSERT_EQ( shellAsset.workspace.root.kind, @@ -81,7 +81,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromReg } TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionState) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); ASSERT_EQ(shellAsset.workspaceSession.panelStates.size(), 1u); shellAsset.workspaceSession.panelStates.front().open = false; @@ -90,7 +90,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionSta } TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFromRegistry) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); ASSERT_EQ( shellAsset.shellDefinition.workspacePresentations.size(), shellAsset.panelRegistry.panels.size()); @@ -104,7 +104,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFr } TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentationPanelId) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); ASSERT_EQ( shellAsset.shellDefinition.workspacePresentations.size(), shellAsset.panelRegistry.panels.size()); @@ -118,7 +118,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentation } TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresentation) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); shellAsset.shellDefinition.workspacePresentations.clear(); const auto validation = ValidateEditorShellAsset(shellAsset); @@ -128,7 +128,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresen } TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShellMenuModel) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); shellAsset.shellDefinition.menuModel.menus = { { "window", @@ -153,7 +153,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShellMenuModel) { } TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShortcutConfiguration) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); shellAsset.shortcutAsset.commandRegistry.commands = { { "workspace.reset_layout", @@ -172,7 +172,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShortcutConfigurati } TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationKindMismatch) { - auto shellAsset = BuildDefaultEditorShellAsset("."); + auto shellAsset = BuildEditorFoundationShellAsset("."); ASSERT_EQ( shellAsset.shellDefinition.workspacePresentations.size(), shellAsset.panelRegistry.panels.size()); diff --git a/tests/UI/Editor/unit/test_editor_window_input_routing.cpp b/tests/UI/Editor/unit/test_editor_window_input_routing.cpp new file mode 100644 index 00000000..994cba09 --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_window_input_routing.cpp @@ -0,0 +1,125 @@ +#include +#include +#include + +#include "app/Platform/Win32/EditorWindowPointerCapture.h" + +#include + +#include + +namespace { + +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::ShouldStartImmediateUIEditorShellPointerCapture; +using XCEngine::UI::Editor::UIEditorShellInteractionFrame; +using XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame; +using XCEngine::UI::Editor::Widgets::UIEditorDockHostSplitterLayout; +using XCEngine::UI::Editor::App::CanConsumeEditorWindowChromeHover; +using XCEngine::UI::Editor::App::CanRouteEditorWindowBorderlessChromePointerMessages; +using XCEngine::UI::Editor::App::CanRouteEditorWindowBorderlessResizePointerMessages; +using XCEngine::UI::Editor::App::CanRouteEditorWindowGlobalTabDragPointerMessages; +using XCEngine::UI::Editor::App::EditorWindowPointerCaptureOwner; + +UIEditorShellInteractionFrame BuildFrameWithViewportInputRect( + const UIRect& inputRect, + bool captureOnPointerDownInside = true) { + UIEditorShellInteractionFrame frame = {}; + + UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; + viewportFrame.panelId = "scene"; + viewportFrame.viewportShellModel.spec.inputBridgeConfig.capturePointerOnPointerDownInside = + captureOnPointerDownInside; + viewportFrame.viewportShellFrame.slotLayout.bounds = inputRect; + viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect; + frame.workspaceInteractionFrame.composeFrame.viewportFrames.push_back(std::move(viewportFrame)); + return frame; +} + +TEST(EditorWindowInputRoutingTests, ViewportInputRectStartsImmediateCaptureWhenEnabled) { + const UIEditorShellInteractionFrame frame = + BuildFrameWithViewportInputRect(UIRect(100.0f, 80.0f, 640.0f, 360.0f)); + + EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 180.0f))); + EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(80.0f, 60.0f))); +} + +TEST(EditorWindowInputRoutingTests, ViewportInputRectRespectsCaptureConfig) { + const UIEditorShellInteractionFrame frame = + BuildFrameWithViewportInputRect( + UIRect(100.0f, 80.0f, 640.0f, 360.0f), + false); + + EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 180.0f))); +} + +TEST(EditorWindowInputRoutingTests, ViewportTopBarStartsImmediateCaptureWhenInsideShellBounds) { + UIEditorShellInteractionFrame frame = {}; + + UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; + viewportFrame.panelId = "scene"; + viewportFrame.viewportShellModel.spec.inputBridgeConfig.capturePointerOnPointerDownInside = + true; + viewportFrame.viewportShellFrame.slotLayout.bounds = + UIRect(100.0f, 80.0f, 640.0f, 406.0f); + viewportFrame.viewportShellFrame.slotLayout.inputRect = + UIRect(100.0f, 104.0f, 640.0f, 382.0f); + frame.workspaceInteractionFrame.composeFrame.viewportFrames.push_back(std::move(viewportFrame)); + + EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(240.0f, 92.0f))); +} + +TEST(EditorWindowInputRoutingTests, SplitterHandleStartsImmediateCapture) { + UIEditorShellInteractionFrame frame = {}; + + UIEditorDockHostSplitterLayout splitter = {}; + splitter.nodeId = "root-split"; + splitter.handleHitRect = UIRect(400.0f, 0.0f, 12.0f, 720.0f); + frame.workspaceInteractionFrame.dockHostFrame.layout.splitters.push_back(std::move(splitter)); + + EXPECT_TRUE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(405.0f, 220.0f))); + EXPECT_FALSE(ShouldStartImmediateUIEditorShellPointerCapture(frame, UIPoint(520.0f, 220.0f))); +} + +TEST(EditorWindowInputRoutingTests, PointerCaptureOwnerRoutesPointerStreamToMatchingSubsystem) { + EXPECT_TRUE(CanRouteEditorWindowGlobalTabDragPointerMessages( + EditorWindowPointerCaptureOwner::GlobalTabDrag, + true)); + EXPECT_FALSE(CanRouteEditorWindowGlobalTabDragPointerMessages( + EditorWindowPointerCaptureOwner::Shell, + true)); + EXPECT_TRUE(CanRouteEditorWindowBorderlessResizePointerMessages( + EditorWindowPointerCaptureOwner::BorderlessResize)); + EXPECT_FALSE(CanRouteEditorWindowBorderlessResizePointerMessages( + EditorWindowPointerCaptureOwner::Shell)); + EXPECT_TRUE(CanRouteEditorWindowBorderlessChromePointerMessages( + EditorWindowPointerCaptureOwner::BorderlessChrome)); + EXPECT_FALSE(CanRouteEditorWindowBorderlessChromePointerMessages( + EditorWindowPointerCaptureOwner::HostedContent)); +} + +TEST(EditorWindowInputRoutingTests, ChromeHoverConsumptionStopsWhileShellOrHostedCaptureOwnsPointerStream) { + EXPECT_TRUE(CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner::None, + false, + false)); + EXPECT_FALSE(CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner::Shell, + false, + false)); + EXPECT_FALSE(CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner::HostedContent, + false, + false)); + EXPECT_FALSE(CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner::None, + true, + false)); + EXPECT_FALSE(CanConsumeEditorWindowChromeHover( + EditorWindowPointerCaptureOwner::None, + false, + true)); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp new file mode 100644 index 00000000..cf019d91 --- /dev/null +++ b/tests/UI/Editor/unit/test_hierarchy_scene_binding.cpp @@ -0,0 +1,204 @@ +#include "Features/Hierarchy/HierarchyModel.h" +#include "Scene/EditorSceneBridge.h" + +#include +#include +#include +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::Scene; +using ::XCEngine::Components::SceneManager; + +class ScopedSceneManagerReset final { +public: + ScopedSceneManagerReset() { + Reset(); + } + + ~ScopedSceneManagerReset() { + Reset(); + } + +private: + static void Reset() { + SceneManager& manager = SceneManager::Get(); + const auto scenes = manager.GetAllScenes(); + for (Scene* scene : scenes) { + manager.UnloadScene(scene); + } + } +}; + +class TemporaryProjectRoot final { +public: + TemporaryProjectRoot() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcui_hierarchy_scene_bridge_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryProjectRoot() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + +private: + std::filesystem::path m_root = {}; +}; + +TEST(HierarchySceneBindingTests, BuildFromSceneUsesRealGameObjectIds) { + ScopedSceneManagerReset reset = {}; + + Scene* scene = SceneManager::Get().CreateScene("Main"); + ASSERT_NE(scene, nullptr); + SceneManager::Get().SetActiveScene(scene); + + GameObject* root = scene->CreateGameObject("Root"); + ASSERT_NE(root, nullptr); + GameObject* child = scene->CreateGameObject("Child", root); + ASSERT_NE(child, nullptr); + + const HierarchyModel model = HierarchyModel::BuildFromScene(scene); + const HierarchyNode* rootNode = + model.FindNode(MakeEditorGameObjectItemId(root->GetID())); + ASSERT_NE(rootNode, nullptr); + EXPECT_EQ(rootNode->label, "Root"); + ASSERT_EQ(rootNode->children.size(), 1u); + EXPECT_EQ( + rootNode->children.front().nodeId, + MakeEditorGameObjectItemId(child->GetID())); + EXPECT_EQ(rootNode->children.front().label, "Child"); +} + +TEST(HierarchySceneBindingTests, DuplicateGameObjectClonesHierarchyIntoScene) { + ScopedSceneManagerReset reset = {}; + + Scene* scene = SceneManager::Get().CreateScene("Main"); + ASSERT_NE(scene, nullptr); + SceneManager::Get().SetActiveScene(scene); + + GameObject* root = scene->CreateGameObject("Root"); + ASSERT_NE(root, nullptr); + GameObject* child = scene->CreateGameObject("Child", root); + ASSERT_NE(child, nullptr); + + const std::string duplicateId = + DuplicateEditorGameObject(MakeEditorGameObjectItemId(root->GetID())); + ASSERT_FALSE(duplicateId.empty()); + + const HierarchyModel model = HierarchyModel::BuildFromScene(scene); + const HierarchyNode* duplicateNode = model.FindNode(duplicateId); + ASSERT_NE(duplicateNode, nullptr); + EXPECT_EQ(duplicateNode->label, "Root"); + ASSERT_EQ(duplicateNode->children.size(), 1u); + EXPECT_EQ(duplicateNode->children.front().label, "Child"); + + const auto roots = scene->GetRootGameObjects(); + EXPECT_EQ(roots.size(), 2u); +} + +TEST(HierarchySceneBindingTests, RenameGameObjectUpdatesRealSceneAndProjection) { + ScopedSceneManagerReset reset = {}; + + Scene* scene = SceneManager::Get().CreateScene("Main"); + ASSERT_NE(scene, nullptr); + SceneManager::Get().SetActiveScene(scene); + + GameObject* gameObject = scene->CreateGameObject("Camera"); + ASSERT_NE(gameObject, nullptr); + + const std::string itemId = + MakeEditorGameObjectItemId(gameObject->GetID()); + ASSERT_TRUE(RenameEditorGameObject(itemId, "PlayerCamera")); + EXPECT_EQ(gameObject->GetName(), "PlayerCamera"); + + const HierarchyModel model = HierarchyModel::BuildFromScene(scene); + const HierarchyNode* node = model.FindNode(itemId); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->label, "PlayerCamera"); +} + +TEST(HierarchySceneBindingTests, ReparentAndMoveToRootOperateOnRealScene) { + ScopedSceneManagerReset reset = {}; + + Scene* scene = SceneManager::Get().CreateScene("Main"); + ASSERT_NE(scene, nullptr); + SceneManager::Get().SetActiveScene(scene); + + GameObject* parentA = scene->CreateGameObject("ParentA"); + ASSERT_NE(parentA, nullptr); + GameObject* parentB = scene->CreateGameObject("ParentB"); + ASSERT_NE(parentB, nullptr); + GameObject* child = scene->CreateGameObject("Child", parentA); + ASSERT_NE(child, nullptr); + + ASSERT_TRUE(ReparentEditorGameObject( + MakeEditorGameObjectItemId(child->GetID()), + MakeEditorGameObjectItemId(parentB->GetID()))); + EXPECT_EQ(child->GetParent(), parentB); + + HierarchyModel model = HierarchyModel::BuildFromScene(scene); + const HierarchyNode* parentBNode = + model.FindNode(MakeEditorGameObjectItemId(parentB->GetID())); + ASSERT_NE(parentBNode, nullptr); + ASSERT_EQ(parentBNode->children.size(), 1u); + EXPECT_EQ(parentBNode->children.front().label, "Child"); + + ASSERT_TRUE(MoveEditorGameObjectToRoot( + MakeEditorGameObjectItemId(child->GetID()))); + EXPECT_EQ(child->GetParent(), nullptr); + + model = HierarchyModel::BuildFromScene(scene); + const auto roots = scene->GetRootGameObjects(); + EXPECT_EQ(roots.size(), 3u); +} + +TEST(HierarchySceneBindingTests, EnsureStartupSceneLoadsMainSceneAndSetsActive) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + + const std::filesystem::path scenePath = + projectRoot.Root() / "Assets" / "Scenes" / "Main.xc"; + std::filesystem::create_directories(scenePath.parent_path()); + { + Scene scene("Main"); + scene.CreateGameObject("Camera"); + scene.Save(scenePath.string()); + } + + const EditorStartupSceneResult result = + EnsureEditorStartupScene(projectRoot.Root()); + EXPECT_TRUE(result.ready); + EXPECT_TRUE(result.loadedFromDisk); + ASSERT_NE(GetActiveEditorScene(), nullptr); + EXPECT_EQ(GetActiveEditorScene()->GetName(), "Main"); + + const HierarchyModel model = + HierarchyModel::BuildFromScene(GetActiveEditorScene()); + EXPECT_FALSE(model.Empty()); + + SceneManager& sceneManager = SceneManager::Get(); + const auto scenes = sceneManager.GetAllScenes(); + for (Scene* scene : scenes) { + sceneManager.UnloadScene(scene); + } + ::XCEngine::Resources::ResourceManager::Get().Shutdown(); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_input_modifier_tracker.cpp b/tests/UI/Editor/unit/test_input_modifier_tracker.cpp index 90f50c89..5dbe5650 100644 --- a/tests/UI/Editor/unit/test_input_modifier_tracker.cpp +++ b/tests/UI/Editor/unit/test_input_modifier_tracker.cpp @@ -14,6 +14,7 @@ namespace { using XCEngine::UI::Editor::Host::InputModifierTracker; using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPointerButton; TEST(InputModifierTrackerTest, ControlStatePersistsAcrossChordKeyDownAndClearsOnKeyUp) { InputModifierTracker tracker = {}; @@ -52,11 +53,31 @@ TEST(InputModifierTrackerTest, PointerModifiersMergeMouseFlagsWithTrackedKeyboar VK_MENU, 0x00380001); - const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT); + const auto modifiers = tracker.BuildPointerModifiers(MK_SHIFT | MK_RBUTTON); EXPECT_TRUE(modifiers.shift); EXPECT_TRUE(modifiers.alt); EXPECT_FALSE(modifiers.control); EXPECT_FALSE(modifiers.super); + EXPECT_FALSE(modifiers.leftMouse); + EXPECT_TRUE(modifiers.rightMouse); +} + +TEST(InputModifierTrackerTest, PointerMessagesUpdateTrackedMouseButtonState) { + InputModifierTracker tracker = {}; + + const auto leftDown = tracker.ApplyPointerMessage( + UIInputEventType::PointerButtonDown, + UIPointerButton::Left, + 0u); + EXPECT_TRUE(leftDown.leftMouse); + EXPECT_TRUE(tracker.GetCurrentModifiers().leftMouse); + + const auto leftUp = tracker.ApplyPointerMessage( + UIInputEventType::PointerButtonUp, + UIPointerButton::Left, + 0u); + EXPECT_FALSE(leftUp.leftMouse); + EXPECT_FALSE(tracker.GetCurrentModifiers().leftMouse); } TEST(InputModifierTrackerTest, RightControlIsTrackedIndependentlyFromLeftControl) { diff --git a/tests/UI/Editor/unit/test_inspector_presentation.cpp b/tests/UI/Editor/unit/test_inspector_presentation.cpp new file mode 100644 index 00000000..80e67c33 --- /dev/null +++ b/tests/UI/Editor/unit/test_inspector_presentation.cpp @@ -0,0 +1,316 @@ +#include "Features/Inspector/Components/IInspectorComponentEditor.h" +#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" +#include "Features/Inspector/InspectorPresentationModel.h" +#include "Features/Inspector/InspectorSubject.h" +#include "Scene/EditorSceneRuntime.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::Scene; +using ::XCEngine::Components::SceneManager; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridSection; + +class ScopedSceneManagerReset final { +public: + ScopedSceneManagerReset() { + Reset(); + } + + ~ScopedSceneManagerReset() { + Reset(); + ::XCEngine::Resources::ResourceManager::Get().Shutdown(); + } + +private: + static void Reset() { + SceneManager& manager = SceneManager::Get(); + const auto scenes = manager.GetAllScenes(); + for (Scene* scene : scenes) { + manager.UnloadScene(scene); + } + } +}; + +class TemporaryProjectRoot final { +public: + TemporaryProjectRoot() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcui_inspector_presentation_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryProjectRoot() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + + std::filesystem::path MainScenePath() const { + return m_root / "Assets" / "Scenes" / "Main.xc"; + } + +private: + std::filesystem::path m_root = {}; +}; + +void SaveMainScene(const TemporaryProjectRoot& projectRoot) { + const std::filesystem::path scenePath = projectRoot.MainScenePath(); + std::filesystem::create_directories(scenePath.parent_path()); + + Scene scene("Main"); + GameObject* parent = scene.CreateGameObject("Parent"); + ASSERT_NE(parent, nullptr); + ASSERT_NE(parent->GetTransform(), nullptr); + parent->GetTransform()->SetLocalPosition(::XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + parent->GetTransform()->SetLocalScale(::XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f)); + ASSERT_NE(scene.CreateGameObject("Child", parent), nullptr); + ASSERT_NE(parent->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr); + scene.Save(scenePath.string()); +} + +const UIEditorPropertyGridSection* FindSection( + const InspectorPresentationModel& model, + std::string_view title) { + for (const UIEditorPropertyGridSection& section : model.sections) { + if (section.title == title) { + return §ion; + } + } + + return nullptr; +} + +const UIEditorPropertyGridField* FindField( + const UIEditorPropertyGridSection& section, + std::string_view label) { + for (const UIEditorPropertyGridField& field : section.fields) { + if (field.label == label) { + return &field; + } + } + + return nullptr; +} + +const InspectorPresentationComponentBinding* FindBinding( + const InspectorPresentationModel& model, + std::string_view typeName) { + for (const InspectorPresentationComponentBinding& binding : + model.componentBindings) { + if (binding.typeName == typeName) { + return &binding; + } + } + + return nullptr; +} + +TEST(InspectorPresentationModelTests, EmptySubjectBuildsDefaultEmptyState) { + EditorSceneRuntime runtime = {}; + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + {}, + runtime, + InspectorComponentEditorRegistry::Get()); + + EXPECT_FALSE(model.hasSelection); + EXPECT_EQ(model.title, "Nothing selected"); + EXPECT_EQ(model.subtitle, "Select a hierarchy item or project asset."); + EXPECT_TRUE(model.sections.empty()); + EXPECT_TRUE(model.componentBindings.empty()); +} + +TEST(InspectorPresentationModelTests, ProjectAssetSubjectBuildsIdentityAndLocationSections) { + InspectorSubject subject = {}; + subject.kind = InspectorSubjectKind::ProjectAsset; + subject.source = InspectorSelectionSource::Project; + subject.projectAsset.selection.kind = EditorSelectionKind::ProjectItem; + subject.projectAsset.selection.itemId = "asset:materials/test"; + subject.projectAsset.selection.displayName = "TestMaterial"; + subject.projectAsset.selection.absolutePath = + std::filesystem::path("D:/Xuanchi/Main/XCEngine/project/Assets/Materials/Test.mat"); + + EditorSceneRuntime runtime = {}; + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + subject, + runtime, + InspectorComponentEditorRegistry::Get()); + + ASSERT_TRUE(model.hasSelection); + EXPECT_EQ(model.title, "TestMaterial"); + EXPECT_EQ(model.subtitle, "Asset"); + ASSERT_EQ(model.sections.size(), 2u); + + const auto* identity = FindSection(model, "Identity"); + ASSERT_NE(identity, nullptr); + ASSERT_EQ(identity->fields.size(), 3u); + const auto* typeField = FindField(*identity, "Type"); + const auto* nameField = FindField(*identity, "Name"); + const auto* idField = FindField(*identity, "Id"); + ASSERT_NE(typeField, nullptr); + ASSERT_NE(nameField, nullptr); + ASSERT_NE(idField, nullptr); + EXPECT_EQ(typeField->valueText, "Asset"); + EXPECT_EQ(nameField->valueText, "TestMaterial"); + EXPECT_EQ(idField->valueText, "asset:materials/test"); + + const auto* location = FindSection(model, "Location"); + ASSERT_NE(location, nullptr); + ASSERT_EQ(location->fields.size(), 1u); + const auto* pathField = FindField(*location, "Path"); + ASSERT_NE(pathField, nullptr); + EXPECT_NE( + pathField->valueText.find("Assets/Materials/Test.mat"), + std::string::npos); +} + +TEST(InspectorPresentationModelTests, SceneObjectSubjectBuildsRegisteredComponentSections) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* parent = scene->Find("Parent"); + ASSERT_NE(parent, nullptr); + ASSERT_TRUE(runtime.SetSelection(parent->GetID())); + + const InspectorSubject subject = + BuildInspectorSubject(EditorSession{}, runtime); + ASSERT_EQ(subject.kind, InspectorSubjectKind::SceneObject); + + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + subject, + runtime, + InspectorComponentEditorRegistry::Get()); + + ASSERT_TRUE(model.hasSelection); + EXPECT_EQ(model.title, "Parent"); + EXPECT_EQ(model.subtitle, "GameObject"); + ASSERT_EQ(model.sections.size(), 4u); + ASSERT_EQ(model.componentBindings.size(), 2u); + + const auto* identity = FindSection(model, "Identity"); + ASSERT_NE(identity, nullptr); + const auto* sceneTypeField = FindField(*identity, "Type"); + const auto* sceneNameField = FindField(*identity, "Name"); + const auto* sceneIdField = FindField(*identity, "Id"); + ASSERT_NE(sceneTypeField, nullptr); + ASSERT_NE(sceneNameField, nullptr); + ASSERT_NE(sceneIdField, nullptr); + EXPECT_EQ(sceneTypeField->valueText, "GameObject"); + EXPECT_EQ(sceneNameField->valueText, "Parent"); + EXPECT_EQ(sceneIdField->valueText, runtime.GetSelectedItemId()); + + const auto* hierarchy = FindSection(model, "Hierarchy"); + ASSERT_NE(hierarchy, nullptr); + const auto* childrenField = FindField(*hierarchy, "Children"); + const auto* parentField = FindField(*hierarchy, "Parent"); + ASSERT_NE(childrenField, nullptr); + ASSERT_NE(parentField, nullptr); + EXPECT_EQ(childrenField->valueText, "1"); + EXPECT_EQ(parentField->valueText, "Scene Root"); + + const auto* transform = FindSection(model, "Transform"); + ASSERT_NE(transform, nullptr); + const auto* positionField = FindField(*transform, "Position"); + const auto* rotationField = FindField(*transform, "Rotation"); + const auto* scaleField = FindField(*transform, "Scale"); + ASSERT_NE(positionField, nullptr); + ASSERT_NE(rotationField, nullptr); + ASSERT_NE(scaleField, nullptr); + EXPECT_EQ(positionField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); + EXPECT_DOUBLE_EQ(positionField->vector3Value.values[0], 1.0); + EXPECT_DOUBLE_EQ(positionField->vector3Value.values[1], 2.0); + EXPECT_DOUBLE_EQ(positionField->vector3Value.values[2], 3.0); + EXPECT_EQ(rotationField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); + EXPECT_EQ(scaleField->kind, Widgets::UIEditorPropertyGridFieldKind::Vector3); + EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[0], 4.0); + EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[1], 5.0); + EXPECT_DOUBLE_EQ(scaleField->vector3Value.values[2], 6.0); + + const auto* camera = FindSection(model, "Camera"); + ASSERT_NE(camera, nullptr); + const auto* projectionField = FindField(*camera, "Projection"); + const auto* primaryField = FindField(*camera, "Primary"); + const auto* clearColorField = FindField(*camera, "Clear Color"); + ASSERT_NE(projectionField, nullptr); + ASSERT_NE(primaryField, nullptr); + ASSERT_NE(clearColorField, nullptr); + EXPECT_EQ(projectionField->kind, Widgets::UIEditorPropertyGridFieldKind::Enum); + EXPECT_EQ(projectionField->enumValue.selectedIndex, 0u); + EXPECT_EQ(primaryField->kind, Widgets::UIEditorPropertyGridFieldKind::Bool); + EXPECT_TRUE(primaryField->boolValue); + EXPECT_EQ(clearColorField->kind, Widgets::UIEditorPropertyGridFieldKind::Color); + + const auto* transformBinding = FindBinding(model, "Transform"); + const auto* cameraBinding = FindBinding(model, "Camera"); + ASSERT_NE(transformBinding, nullptr); + ASSERT_NE(cameraBinding, nullptr); + EXPECT_FALSE(transformBinding->removable); + EXPECT_TRUE(cameraBinding->removable); + EXPECT_EQ(transformBinding->fieldIds.size(), 3u); + EXPECT_GE(cameraBinding->fieldIds.size(), 8u); +} + +TEST(InspectorPresentationModelTests, CameraSkyboxMaterialBuildsAssetField) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* parent = scene->Find("Parent"); + ASSERT_NE(parent, nullptr); + auto* camera = parent->GetComponent<::XCEngine::Components::CameraComponent>(); + ASSERT_NE(camera, nullptr); + camera->SetSkyboxEnabled(true); + camera->SetSkyboxMaterialPath("Assets/Materials/Skybox.mat"); + ASSERT_TRUE(runtime.SetSelection(parent->GetID())); + + const InspectorPresentationModel model = + BuildInspectorPresentationModel( + BuildInspectorSubject(EditorSession{}, runtime), + runtime, + InspectorComponentEditorRegistry::Get()); + + const auto* cameraSection = FindSection(model, "Camera"); + ASSERT_NE(cameraSection, nullptr); + const auto* skyboxMaterialField = FindField(*cameraSection, "Skybox Material"); + ASSERT_NE(skyboxMaterialField, nullptr); + EXPECT_EQ(skyboxMaterialField->kind, Widgets::UIEditorPropertyGridFieldKind::Asset); + EXPECT_EQ( + skyboxMaterialField->assetValue.assetId, + "Assets/Materials/Skybox.mat"); + EXPECT_EQ( + skyboxMaterialField->assetValue.displayName, + "Skybox.mat"); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_project_browser_model.cpp b/tests/UI/Editor/unit/test_project_browser_model.cpp new file mode 100644 index 00000000..25956201 --- /dev/null +++ b/tests/UI/Editor/unit/test_project_browser_model.cpp @@ -0,0 +1,262 @@ +#include "Features/Project/ProjectBrowserModel.h" + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +class TemporaryRepo final { +public: + TemporaryRepo() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcengine_project_browser_model_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryRepo() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + + bool CreateDirectory(const std::filesystem::path& relativePath) const { + std::error_code errorCode = {}; + std::filesystem::create_directories(m_root / relativePath, errorCode); + return !errorCode; + } + + bool WriteFile( + const std::filesystem::path& relativePath, + std::string_view contents = "test") const { + const std::filesystem::path absolutePath = m_root / relativePath; + std::error_code errorCode = {}; + std::filesystem::create_directories(absolutePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + std::ofstream stream(absolutePath, std::ios::binary); + if (!stream.is_open()) { + return false; + } + + stream << contents; + return stream.good(); + } + +private: + std::filesystem::path m_root = {}; +}; + +TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFolder) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/A/Child")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/B")); + ASSERT_TRUE(repo.WriteFile("project/Assets/A.meta")); + ASSERT_TRUE(repo.WriteFile("project/Assets/A/Child.meta")); + ASSERT_TRUE(repo.WriteFile("project/Assets/B.meta")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child")); + + std::string movedFolderId = {}; + ASSERT_TRUE(model.ReparentFolder("Assets/A", "Assets/B", &movedFolderId)); + + EXPECT_EQ(movedFolderId, "Assets/B/A"); + EXPECT_EQ(model.GetCurrentFolderId(), "Assets/B/A/Child"); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A")); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/B/A.meta")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/A.meta")); +} + +TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFolder) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); + + std::string movedFolderId = {}; + ASSERT_TRUE(model.MoveFolderToRoot("Assets/Parent/Nested", &movedFolderId)); + + EXPECT_EQ(movedFolderId, "Assets/Nested"); + EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Nested"); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested")); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Nested.meta")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent/Nested.meta")); +} + +TEST(ProjectBrowserModelTests, CreateFolderCreatesUniqueDirectoryUnderTargetFolder) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes/New Folder")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + std::string createdFolderId = {}; + ASSERT_TRUE(model.CreateFolder("Assets/Scenes", "New Folder", &createdFolderId)); + + EXPECT_EQ(createdFolderId, "Assets/Scenes/New Folder 1"); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/New Folder 1")); +} + +TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposesRelativePath) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Materials")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Materials/New Material.mat")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + std::string createdItemId = {}; + ASSERT_TRUE(model.CreateMaterial("Assets/Materials", "New Material", &createdItemId)); + + EXPECT_EQ(createdItemId, "Assets/Materials/New Material 1.mat"); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Materials/New Material 1.mat")); + EXPECT_EQ( + model.BuildProjectRelativePath(createdItemId), + "Assets/Materials/New Material 1.mat"); + + ASSERT_TRUE(model.NavigateToFolder("Assets/Materials")); + const ProjectBrowserModel::AssetEntry* createdEntry = + model.FindAssetEntry(createdItemId); + ASSERT_NE(createdEntry, nullptr); + EXPECT_EQ(createdEntry->kind, ProjectBrowserModel::ItemKind::Material); + EXPECT_EQ(createdEntry->displayName, "New Material 1"); + EXPECT_EQ(createdEntry->nameWithExtension, "New Material 1.mat"); +} + +TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA/Nested")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderB")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + EXPECT_FALSE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderA/Nested")); + EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB")); +} + +TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentListing) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Archive")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts")); + + std::string movedItemId = {}; + ASSERT_TRUE(model.MoveItemToFolder( + "Assets/Scripts/Player.cs", + "Assets/Archive", + &movedItemId)); + + EXPECT_EQ(movedItemId, "Assets/Archive/Player.cs"); + EXPECT_EQ(model.GetCurrentFolderId(), "Assets/Scripts"); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scripts/Player.cs.meta")); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs")); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Archive/Player.cs.meta")); + EXPECT_EQ(model.FindAssetEntry("Assets/Scripts/Player.cs"), nullptr); + + ASSERT_TRUE(model.NavigateToFolder("Assets/Archive")); + EXPECT_NE(model.FindAssetEntry("Assets/Archive/Player.cs"), nullptr); +} + +TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentListing) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc.meta")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); + + std::string renamedItemId = {}; + ASSERT_TRUE(model.RenameItem("Assets/Scenes/Main.xc", "Gameplay", &renamedItemId)); + + EXPECT_EQ(renamedItemId, "Assets/Scenes/Gameplay.xc"); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc")); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Gameplay.xc.meta")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Scenes/Main.xc.meta")); + + const ProjectBrowserModel::AssetEntry* renamedEntry = + model.FindAssetEntry("Assets/Scenes/Gameplay.xc"); + ASSERT_NE(renamedEntry, nullptr); + EXPECT_EQ(renamedEntry->displayName, "Gameplay"); + EXPECT_EQ(renamedEntry->nameWithExtension, "Gameplay.xc"); +} + +TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Parent/Nested")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Parent.meta")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested")); + + ASSERT_TRUE(model.DeleteItem("Assets/Parent")); + + EXPECT_EQ(model.GetCurrentFolderId(), "Assets"); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent")); + EXPECT_FALSE(std::filesystem::exists(repo.Root() / "project/Assets/Parent.meta")); +} + +TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc")); + ASSERT_TRUE(repo.WriteFile("project/Assets/mesh.fbx")); + ASSERT_TRUE(repo.WriteFile("project/Assets/readme.txt")); + + ProjectBrowserModel model = {}; + model.Initialize(repo.Root()); + + const ProjectBrowserModel::AssetEntry* sceneEntry = + model.FindAssetEntry("Assets/mesh.fbx"); + ASSERT_NE(sceneEntry, nullptr); + EXPECT_EQ(sceneEntry->kind, ProjectBrowserModel::ItemKind::Model); + EXPECT_FALSE(sceneEntry->canOpen); + + const ProjectBrowserModel::AssetEntry* fileEntry = + model.FindAssetEntry("Assets/readme.txt"); + ASSERT_NE(fileEntry, nullptr); + EXPECT_EQ(fileEntry->kind, ProjectBrowserModel::ItemKind::File); + EXPECT_FALSE(fileEntry->canOpen); + + ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes")); + const ProjectBrowserModel::AssetEntry* openedSceneEntry = + model.FindAssetEntry("Assets/Scenes/Main.xc"); + ASSERT_NE(openedSceneEntry, nullptr); + EXPECT_EQ(openedSceneEntry->kind, ProjectBrowserModel::ItemKind::Scene); + EXPECT_TRUE(openedSceneEntry->canOpen); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_project_panel.cpp b/tests/UI/Editor/unit/test_project_panel.cpp new file mode 100644 index 00000000..064acb03 --- /dev/null +++ b/tests/UI/Editor/unit/test_project_panel.cpp @@ -0,0 +1,245 @@ +#include "Features/Project/ProjectPanel.h" +#include "Rendering/Assets/BuiltInIcons.h" + +#include + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +class TemporaryRepo final { +public: + TemporaryRepo() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcengine_project_panel_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryRepo() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + + bool CreateDirectory(const std::filesystem::path& relativePath) const { + std::error_code errorCode = {}; + std::filesystem::create_directories(m_root / relativePath, errorCode); + return !errorCode; + } + + bool WriteFile( + const std::filesystem::path& relativePath, + std::string_view contents = "test") const { + const std::filesystem::path absolutePath = m_root / relativePath; + std::error_code errorCode = {}; + std::filesystem::create_directories(absolutePath.parent_path(), errorCode); + if (errorCode) { + return false; + } + + std::ofstream stream(absolutePath, std::ios::binary); + if (!stream.is_open()) { + return false; + } + + stream << contents; + return stream.good(); + } + +private: + std::filesystem::path m_root = {}; +}; + +UIEditorPanelContentHostFrame MakeProjectHostFrame() { + UIEditorPanelContentHostFrame frame = {}; + UIEditorPanelContentHostPanelState panelState = {}; + panelState.panelId = std::string(kProjectPanelId); + panelState.kind = UIEditorPanelPresentationKind::HostedContent; + panelState.mounted = true; + panelState.bounds = ::XCEngine::UI::UIRect(0.0f, 0.0f, 640.0f, 360.0f); + frame.panelStates.push_back(std::move(panelState)); + return frame; +} + +::XCEngine::UI::UIInputEvent MakePointerButtonDown( + float x, + float y, + ::XCEngine::UI::UIPointerButton button) { + ::XCEngine::UI::UIInputEvent event = {}; + event.type = ::XCEngine::UI::UIInputEventType::PointerButtonDown; + event.position = ::XCEngine::UI::UIPoint(x, y); + event.pointerButton = button; + return event; +} + +TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) { + TemporaryRepo repo = {}; + + ProjectPanel panel = {}; + panel.Initialize(repo.Root()); + + const UIEditorHostCommandEvaluationResult evaluation = + panel.EvaluateAssetCommand("assets.create_folder"); + EXPECT_TRUE(evaluation.executable); + + const UIEditorHostCommandDispatchResult dispatch = + panel.DispatchAssetCommand("assets.create_folder"); + EXPECT_TRUE(dispatch.commandExecuted); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder")); + + const auto& events = panel.GetFrameEvents(); + ASSERT_EQ(events.size(), 2u); + EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected); + EXPECT_EQ(events[0].source, ProjectPanel::EventSource::Command); + EXPECT_EQ(events[0].itemId, "Assets/New Folder"); + EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested); + EXPECT_EQ(events[1].source, ProjectPanel::EventSource::Command); + EXPECT_EQ(events[1].itemId, "Assets/New Folder"); +} + +TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) { + TemporaryRepo repo = {}; + + ProjectPanel panel = {}; + panel.Initialize(repo.Root()); + + const UIEditorHostCommandEvaluationResult evaluation = + panel.EvaluateAssetCommand("assets.create_material"); + EXPECT_TRUE(evaluation.executable); + + const UIEditorHostCommandDispatchResult dispatch = + panel.DispatchAssetCommand("assets.create_material"); + EXPECT_TRUE(dispatch.commandExecuted); + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Material.mat")); + + const auto& events = panel.GetFrameEvents(); + ASSERT_EQ(events.size(), 2u); + EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::AssetSelected); + EXPECT_EQ(events[0].itemId, "Assets/New Material.mat"); + EXPECT_EQ(events[1].kind, ProjectPanel::EventKind::RenameRequested); + EXPECT_EQ(events[1].itemId, "Assets/New Material.mat"); +} + +TEST(ProjectPanelTests, BackgroundContextMenuCreateFolderUsesCurrentFolder) { + TemporaryRepo repo = {}; + + ProjectPanel panel = {}; + panel.Initialize(repo.Root()); + + const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame(); + panel.Update( + hostFrame, + { MakePointerButtonDown(520.0f, 180.0f, ::XCEngine::UI::UIPointerButton::Right) }, + true, + true); + panel.Update( + hostFrame, + { MakePointerButtonDown(520.0f, 194.0f, ::XCEngine::UI::UIPointerButton::Left) }, + true, + true); + + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/New Folder")); + const UIEditorHostCommandEvaluationResult renameEvaluation = + panel.EvaluateEditCommand("edit.rename"); + EXPECT_TRUE(renameEvaluation.executable); + EXPECT_EQ(renameEvaluation.message, "Rename project item 'New Folder'."); + + const auto& events = panel.GetFrameEvents(); + ASSERT_FALSE(events.empty()); + EXPECT_EQ(events.back().kind, ProjectPanel::EventKind::RenameRequested); + EXPECT_EQ(events.back().itemId, "Assets/New Folder"); +} + +TEST(ProjectPanelTests, FolderContextMenuCreateFolderUsesFolderTarget) { + TemporaryRepo repo = {}; + ASSERT_TRUE(std::filesystem::create_directories(repo.Root() / "project/Assets/FolderA")); + + ProjectPanel panel = {}; + panel.Initialize(repo.Root()); + + const UIEditorPanelContentHostFrame hostFrame = MakeProjectHostFrame(); + panel.Update( + hostFrame, + { MakePointerButtonDown(300.0f, 80.0f, ::XCEngine::UI::UIPointerButton::Right) }, + true, + true); + panel.Update( + hostFrame, + { MakePointerButtonDown(320.0f, 120.0f, ::XCEngine::UI::UIPointerButton::Left) }, + true, + true); + + EXPECT_TRUE(std::filesystem::exists(repo.Root() / "project/Assets/FolderA/New Folder")); +} + +TEST(ProjectPanelTests, InjectedRuntimeSelectionDrivesRenameWithoutPanelSelectionSync) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + + ProjectPanel panel = {}; + panel.SetProjectRuntime(&runtime); + + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts")); + ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs")); + + const UIEditorHostCommandDispatchResult dispatch = + panel.DispatchEditCommand("edit.rename"); + EXPECT_TRUE(dispatch.commandExecuted); + + const auto& events = panel.GetFrameEvents(); + ASSERT_EQ(events.size(), 1u); + EXPECT_EQ(events[0].kind, ProjectPanel::EventKind::RenameRequested); + EXPECT_EQ(events[0].source, ProjectPanel::EventSource::GridPrimary); + EXPECT_EQ(events[0].itemId, "Assets/Scripts/Player.cs"); +} + +TEST(ProjectPanelTests, InjectedRuntimeCurrentFolderDrivesRenameFallbackWithoutTreeSync) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA")); + + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + + ProjectPanel panel = {}; + panel.SetProjectRuntime(&runtime); + + ASSERT_TRUE(runtime.NavigateToFolder("Assets/FolderA")); + + const UIEditorHostCommandEvaluationResult evaluation = + panel.EvaluateEditCommand("edit.rename"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Rename project item 'FolderA'."); +} + +TEST(ProjectPanelTests, BuiltInIconsCanBeConfiguredBeforeRuntimeInitialization) { + TemporaryRepo repo = {}; + + ProjectPanel panel = {}; + BuiltInIcons icons = {}; + + panel.SetBuiltInIcons(&icons); + panel.Initialize(repo.Root()); + + const UIEditorHostCommandEvaluationResult evaluation = + panel.EvaluateAssetCommand("assets.create_folder"); + EXPECT_TRUE(evaluation.executable); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp b/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp new file mode 100644 index 00000000..7388c2d0 --- /dev/null +++ b/tests/UI/Editor/unit/test_scene_viewport_render_plan.cpp @@ -0,0 +1,246 @@ +#include "Rendering/Viewport/SceneViewportRenderPlan.h" + +#include +#include + +#include + +namespace { + +using XCEngine::RHI::Format; +using XCEngine::RHI::RHIResourceView; +using XCEngine::RHI::ResourceStates; +using XCEngine::RHI::ResourceViewDimension; +using XCEngine::RHI::ResourceViewType; +using XCEngine::Rendering::RenderPass; +using XCEngine::Rendering::RenderPassContext; +using XCEngine::Rendering::RenderSurface; +using XCEngine::UI::Editor::App::BuildSceneViewportGridPassData; +using XCEngine::UI::Editor::App::ApplySceneViewportRenderPlan; +using XCEngine::UI::Editor::App::BuildSceneViewportRenderPlan; +using XCEngine::UI::Editor::App::MarkSceneViewportRenderSuccess; +using XCEngine::UI::Editor::App::SceneViewportGridPassData; +using XCEngine::UI::Editor::App::SceneViewportRenderRequest; +using XCEngine::UI::Editor::App::SceneViewportSelectionOutlineStyle; +using XCEngine::UI::Editor::App::ViewportRenderTargets; + +class DummyResourceView final : public RHIResourceView { +public: + explicit DummyResourceView( + ResourceViewType viewType = ResourceViewType::RenderTarget, + Format format = Format::R8G8B8A8_UNorm) + : m_viewType(viewType) + , m_format(format) { + } + + void Shutdown() override { + } + + void* GetNativeHandle() override { + return nullptr; + } + + bool IsValid() const override { + return true; + } + + ResourceViewType GetViewType() const override { + return m_viewType; + } + + ResourceViewDimension GetDimension() const override { + return ResourceViewDimension::Texture2D; + } + + Format GetFormat() const override { + return m_format; + } + +private: + ResourceViewType m_viewType = ResourceViewType::RenderTarget; + Format m_format = Format::R8G8B8A8_UNorm; +}; + +class NoopRenderPass final : public RenderPass { +public: + const char* GetName() const override { + return "NoopRenderPass"; + } + + bool Execute(const RenderPassContext&) override { + return true; + } +}; + +SceneViewportRenderRequest CreateValidRequest( + XCEngine::Components::GameObject& cameraObject) { + auto* camera = + cameraObject.AddComponent(); + EXPECT_NE(camera, nullptr); + EXPECT_NE(cameraObject.GetTransform(), nullptr); + cameraObject.GetTransform()->SetPosition( + XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + SceneViewportRenderRequest request = {}; + request.camera = camera; + request.orbitDistance = 9.0f; + return request; +} + +TEST(SceneViewportRenderPlanTests, BuildRenderPlanCreatesOutlinePassWhenSelectionResourcesExist) { + DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); + DummyResourceView depthShaderView(ResourceViewType::ShaderResource, Format::D24_UNorm_S8_UInt); + DummyResourceView selectionMaskView(ResourceViewType::RenderTarget); + DummyResourceView selectionMaskShaderView(ResourceViewType::ShaderResource); + + ViewportRenderTargets targets = {}; + targets.depthView = &depthView; + targets.depthShaderView = &depthShaderView; + targets.selectionMaskView = &selectionMaskView; + targets.selectionMaskShaderView = &selectionMaskShaderView; + + XCEngine::Components::GameObject cameraObject("SceneCamera"); + SceneViewportRenderRequest request = CreateValidRequest(cameraObject); + request.selectedObjectIds = { 7u, 11u }; + + std::size_t gridFactoryCallCount = 0u; + std::size_t outlineFactoryCallCount = 0u; + const auto result = BuildSceneViewportRenderPlan( + targets, + request, + [&gridFactoryCallCount](const SceneViewportGridPassData& data) { + ++gridFactoryCallCount; + EXPECT_TRUE(data.valid); + EXPECT_FLOAT_EQ(data.orbitDistance, 9.0f); + return std::make_unique(); + }, + [&outlineFactoryCallCount]( + ViewportRenderTargets* outlineTargets, + const std::vector& selectedObjectIds, + const SceneViewportSelectionOutlineStyle& style) { + ++outlineFactoryCallCount; + EXPECT_NE(outlineTargets, nullptr); + EXPECT_EQ(selectedObjectIds.size(), 2u); + EXPECT_FLOAT_EQ(style.outlineWidthPixels, 2.0f); + EXPECT_FALSE(style.debugSelectionMask); + return std::make_unique(); + }); + + EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 2u); + EXPECT_TRUE(result.plan.usesGridPass); + EXPECT_TRUE(result.plan.usesSelectionOutline); + EXPECT_EQ(gridFactoryCallCount, 1u); + EXPECT_EQ(outlineFactoryCallCount, 1u); + EXPECT_EQ(result.warningStatusText, nullptr); +} + +TEST(SceneViewportRenderPlanTests, BuildRenderPlanWarnsWhenSelectionResourcesAreUnavailable) { + ViewportRenderTargets targets = {}; + XCEngine::Components::GameObject cameraObject("SceneCamera"); + SceneViewportRenderRequest request = CreateValidRequest(cameraObject); + request.selectedObjectIds = { 42u }; + + std::size_t gridFactoryCallCount = 0u; + const auto result = BuildSceneViewportRenderPlan( + targets, + request, + [&gridFactoryCallCount](const SceneViewportGridPassData& data) { + ++gridFactoryCallCount; + EXPECT_TRUE(data.valid); + return std::make_unique(); + }, + []( + ViewportRenderTargets*, + const std::vector&, + const SceneViewportSelectionOutlineStyle&) { + return std::make_unique(); + }); + + EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 1u); + EXPECT_TRUE(result.plan.usesGridPass); + EXPECT_FALSE(result.plan.usesSelectionOutline); + EXPECT_EQ(gridFactoryCallCount, 1u); + EXPECT_STREQ( + result.warningStatusText, + "Scene selection outline resources are unavailable"); +} + +TEST(SceneViewportRenderPlanTests, BuildSceneViewportGridPassDataCopiesCameraTransformAndLens) { + XCEngine::Components::GameObject cameraObject("SceneCamera"); + SceneViewportRenderRequest request = CreateValidRequest(cameraObject); + + const SceneViewportGridPassData gridData = + BuildSceneViewportGridPassData(request); + + ASSERT_TRUE(gridData.valid); + EXPECT_FLOAT_EQ(gridData.cameraPosition.x, 1.0f); + EXPECT_FLOAT_EQ(gridData.cameraPosition.y, 2.0f); + EXPECT_FLOAT_EQ(gridData.cameraPosition.z, 3.0f); + EXPECT_FLOAT_EQ(gridData.verticalFovDegrees, request.camera->GetFieldOfView()); + EXPECT_FLOAT_EQ(gridData.nearClipPlane, request.camera->GetNearClipPlane()); + EXPECT_FLOAT_EQ(gridData.farClipPlane, request.camera->GetFarClipPlane()); + EXPECT_FLOAT_EQ(gridData.orbitDistance, 9.0f); +} + +TEST(SceneViewportRenderPlanTests, ApplyRenderPlanAttachesPassesAndMarksRenderStates) { + DummyResourceView depthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); + DummyResourceView depthShaderView(ResourceViewType::ShaderResource, Format::D24_UNorm_S8_UInt); + DummyResourceView selectionMaskView(ResourceViewType::RenderTarget); + DummyResourceView selectionMaskShaderView(ResourceViewType::ShaderResource); + DummyResourceView objectIdDepthView(ResourceViewType::DepthStencil, Format::D24_UNorm_S8_UInt); + DummyResourceView objectIdView(ResourceViewType::RenderTarget); + + ViewportRenderTargets targets = {}; + targets.width = 800u; + targets.height = 600u; + targets.depthView = &depthView; + targets.depthShaderView = &depthShaderView; + targets.selectionMaskView = &selectionMaskView; + targets.selectionMaskShaderView = &selectionMaskShaderView; + targets.objectIdDepthView = &objectIdDepthView; + targets.objectIdView = &objectIdView; + targets.colorState = ResourceStates::Common; + targets.objectIdState = ResourceStates::Common; + targets.selectionMaskState = ResourceStates::Common; + + XCEngine::Components::GameObject cameraObject("SceneCamera"); + SceneViewportRenderRequest request = CreateValidRequest(cameraObject); + request.selectedObjectIds = { 24u }; + + auto result = BuildSceneViewportRenderPlan( + targets, + request, + [](const SceneViewportGridPassData& data) { + EXPECT_TRUE(data.valid); + return std::make_unique(); + }, + []( + ViewportRenderTargets*, + const std::vector&, + const SceneViewportSelectionOutlineStyle&) { + return std::make_unique(); + }); + + XCEngine::Rendering::CameraFramePlan framePlan = {}; + framePlan.request.surface = RenderSurface(800u, 600u); + framePlan.request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200)); + + ApplySceneViewportRenderPlan(targets, result.plan, framePlan); + + EXPECT_EQ(framePlan.postScenePasses, &result.plan.postScenePasses); + EXPECT_TRUE(framePlan.request.objectId.IsRequested()); + EXPECT_TRUE(framePlan.request.hasClearColorOverride); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.r, 0.27f); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.g, 0.27f); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.b, 0.27f); + ASSERT_EQ(framePlan.request.objectId.surface.GetColorAttachments().size(), 1u); + EXPECT_EQ(framePlan.request.objectId.surface.GetColorAttachments()[0], &objectIdView); + EXPECT_EQ(framePlan.request.objectId.surface.GetDepthAttachment(), &objectIdDepthView); + + MarkSceneViewportRenderSuccess(targets, result.plan, framePlan); + EXPECT_EQ(targets.colorState, ResourceStates::PixelShaderResource); + EXPECT_EQ(targets.objectIdState, ResourceStates::PixelShaderResource); + EXPECT_EQ(targets.selectionMaskState, ResourceStates::PixelShaderResource); + EXPECT_TRUE(targets.hasValidObjectIdFrame); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_structured_editor_shell.cpp b/tests/UI/Editor/unit/test_structured_editor_shell.cpp index ae23d375..20285f0a 100644 --- a/tests/UI/Editor/unit/test_structured_editor_shell.cpp +++ b/tests/UI/Editor/unit/test_structured_editor_shell.cpp @@ -17,7 +17,7 @@ namespace { using XCEngine::Input::KeyCode; -using XCEngine::UI::Editor::BuildDefaultEditorShellAsset; +using XCEngine::UI::Editor::BuildEditorFoundationShellAsset; using XCEngine::UI::Editor::BuildStructuredEditorShellBinding; using XCEngine::UI::Editor::BuildStructuredEditorShellServices; using XCEngine::UI::Editor::ResolveUIEditorShellInteractionModel; @@ -124,7 +124,7 @@ UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) { } // namespace TEST(EditorUIStructuredShellTest, StructuredEditorShellDoesNotRequireRepositoryXCUIDocument) { - const auto shell = BuildDefaultEditorShellAsset(RepoRootPath()); + const auto shell = BuildEditorFoundationShellAsset(RepoRootPath()); const auto binding = BuildStructuredEditorShellBinding(shell); ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message; @@ -134,7 +134,7 @@ TEST(EditorUIStructuredShellTest, StructuredEditorShellDoesNotRequireRepositoryX } TEST(EditorUIStructuredShellTest, StructuredShellBindingUsesEditorShellAssetAsSingleSource) { - auto shell = BuildDefaultEditorShellAsset(RepoRootPath()); + auto shell = BuildEditorFoundationShellAsset(RepoRootPath()); shell.shellDefinition.statusSegments.front().label = "Asset Contract"; shell.shortcutAsset.commandRegistry.commands = { { diff --git a/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp index 4b99ec3b..99184a28 100644 --- a/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp @@ -17,7 +17,6 @@ using XCEngine::UI::Editor::FindUIEditorPanelContentHostMountRequest; using XCEngine::UI::Editor::FindUIEditorPanelContentHostPanelState; using XCEngine::UI::Editor::GetUIEditorPanelContentHostEventKindName; using XCEngine::UI::Editor::ResolveUIEditorPanelContentHostRequest; -using XCEngine::UI::Editor::UIEditorPanelContentHostBinding; using XCEngine::UI::Editor::UIEditorPanelContentHostState; using XCEngine::UI::Editor::UIEditorPanelPresentationKind; using XCEngine::UI::Editor::UIEditorPanelRegistry; @@ -56,15 +55,6 @@ UIEditorWorkspaceModel BuildWorkspace() { return workspace; } -std::vector BuildBindings() { - return { - { "doc-a", UIEditorPanelPresentationKind::HostedContent }, - { "doc-b", UIEditorPanelPresentationKind::HostedContent }, - { "console", UIEditorPanelPresentationKind::Placeholder }, - { "inspector", UIEditorPanelPresentationKind::HostedContent } - }; -} - std::vector FormatEvents( const std::vector& events) { std::vector formatted = {}; @@ -90,8 +80,7 @@ TEST(UIEditorPanelContentHostTest, ResolveRequestMountsSelectedHostedTabAndStand const auto request = ResolveUIEditorPanelContentHostRequest( layout, - registry, - BuildBindings()); + registry); ASSERT_EQ(request.mountRequests.size(), 2u); EXPECT_NE(FindUIEditorPanelContentHostMountRequest(request, "doc-b"), nullptr); @@ -111,12 +100,11 @@ TEST(UIEditorPanelContentHostTest, UpdateEmitsMountedAndUnmountedWhenHostedTabSe registry, workspace, session); - auto request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + auto request = ResolveUIEditorPanelContentHostRequest(layout, registry); const auto initialFrame = UpdateUIEditorPanelContentHost( state, request, - registry, - BuildBindings()); + registry); EXPECT_EQ( FormatEvents(initialFrame.events), std::vector({ "Mounted:doc-b", "Mounted:inspector" })); @@ -131,12 +119,11 @@ TEST(UIEditorPanelContentHostTest, UpdateEmitsMountedAndUnmountedWhenHostedTabSe registry, workspace, session); - request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + request = ResolveUIEditorPanelContentHostRequest(layout, registry); const auto switchedFrame = UpdateUIEditorPanelContentHost( state, request, - registry, - BuildBindings()); + registry); EXPECT_EQ( FormatEvents(switchedFrame.events), @@ -161,20 +148,19 @@ TEST(UIEditorPanelContentHostTest, UpdateEmitsBoundsChangedForMountedHostedPanel registry, workspace, session); - auto request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); - UpdateUIEditorPanelContentHost(state, request, registry, BuildBindings()); + auto request = ResolveUIEditorPanelContentHostRequest(layout, registry); + UpdateUIEditorPanelContentHost(state, request, registry); layout = BuildUIEditorDockHostLayout( UIRect(0.0f, 0.0f, 1440.0f, 800.0f), registry, workspace, session); - request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + request = ResolveUIEditorPanelContentHostRequest(layout, registry); const auto resizedFrame = UpdateUIEditorPanelContentHost( state, request, - registry, - BuildBindings()); + registry); EXPECT_EQ( FormatEvents(resizedFrame.events), diff --git a/tests/UI/Editor/unit/test_ui_editor_panel_registry.cpp b/tests/UI/Editor/unit/test_ui_editor_panel_registry.cpp index ae43ab0d..22efe20a 100644 --- a/tests/UI/Editor/unit/test_ui_editor_panel_registry.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_panel_registry.cpp @@ -4,7 +4,7 @@ namespace { -using XCEngine::UI::Editor::BuildDefaultEditorShellPanelRegistry; +using XCEngine::UI::Editor::BuildEditorFoundationPanelRegistry; using XCEngine::UI::Editor::FindUIEditorPanelDescriptor; using XCEngine::UI::Editor::UIEditorPanelDescriptor; using XCEngine::UI::Editor::UIEditorPanelRegistry; @@ -12,7 +12,7 @@ using XCEngine::UI::Editor::UIEditorPanelRegistryValidationCode; using XCEngine::UI::Editor::ValidateUIEditorPanelRegistry; TEST(UIEditorPanelRegistryTest, DefaultRegistryContainsShellDescriptors) { - const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry(); + const UIEditorPanelRegistry registry = BuildEditorFoundationPanelRegistry(); ASSERT_EQ(registry.panels.size(), 1u); const UIEditorPanelDescriptor* descriptor = diff --git a/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp index 2931b1cc..5ccfe156 100644 --- a/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include +#include #include @@ -18,12 +20,15 @@ using XCEngine::UI::Widgets::UIExpansionModel; using XCEngine::UI::Widgets::UIPropertyEditModel; using XCEngine::UI::Widgets::UISelectionModel; using XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics; +using XCEngine::UI::Editor::BuildUIEditorPropertyGridAssetFieldMetrics; using XCEngine::UI::Editor::UIEditorPropertyGridInteractionState; using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction; +using XCEngine::UI::Editor::Widgets::BuildUIEditorAssetFieldLayout; using XCEngine::UI::Editor::Widgets::BuildUIEditorColorFieldLayout; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection; +using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout; UIEditorPropertyGridField MakeTextField( std::string id, @@ -80,6 +85,22 @@ UIEditorPropertyGridField MakeEnumField( return field; } +UIEditorPropertyGridField MakeAssetField( + std::string id, + std::string label, + std::string assetId, + std::string displayName, + std::string statusText = "MAT") { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Asset; + field.assetValue.assetId = std::move(assetId); + field.assetValue.displayName = std::move(displayName); + field.assetValue.statusText = std::move(statusText); + return field; +} + UIEditorPropertyGridField MakeColorField( std::string id, std::string label, @@ -94,6 +115,23 @@ UIEditorPropertyGridField MakeColorField( return field; } +UIEditorPropertyGridField MakeVector3Field( + std::string id, + std::string label, + const std::array& values, + bool integerMode = false) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Vector3; + field.vector3Value.values = values; + field.vector3Value.step = integerMode ? 1.0 : 0.1; + field.vector3Value.minValue = -1000000.0; + field.vector3Value.maxValue = 1000000.0; + field.vector3Value.integerMode = integerMode; + return field; +} + std::vector BuildSections() { return { { @@ -605,3 +643,169 @@ TEST(UIEditorPropertyGridInteractionTest, ColorFieldPopupCanOpenAndDragAlphaThro EXPECT_EQ(frame.result.changedFieldId, "tint"); EXPECT_LT(sections[0].fields[0].colorValue.value.a, 0.5f); } + +TEST(UIEditorPropertyGridInteractionTest, Vector3FieldCanStartEditAndCommitInsidePropertyGridHost) { + std::vector sections = { + { + "transform", + "Transform", + { + MakeVector3Field("position", "Position", { 1.0, 2.0, 3.0 }, true) + }, + 0.0f + } + }; + UISelectionModel selectionModel = {}; + UIExpansionModel expansionModel = {}; + expansionModel.Expand("transform"); + UIPropertyEditModel propertyEditModel = {}; + UIEditorPropertyGridInteractionState state = {}; + + auto frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + {}); + + XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec vectorSpec = {}; + vectorSpec.fieldId = "position"; + vectorSpec.label = "Position"; + vectorSpec.values = sections[0].fields[0].vector3Value.values; + vectorSpec.componentLabels = sections[0].fields[0].vector3Value.componentLabels; + vectorSpec.step = sections[0].fields[0].vector3Value.step; + vectorSpec.minValue = sections[0].fields[0].vector3Value.minValue; + vectorSpec.maxValue = sections[0].fields[0].vector3Value.maxValue; + vectorSpec.integerMode = sections[0].fields[0].vector3Value.integerMode; + const auto vectorLayout = BuildUIEditorVector3FieldLayout( + frame.layout.fieldRowRects[0], + vectorSpec, + XCEngine::UI::Editor::BuildUIEditorPropertyGridVector3FieldMetrics({})); + const UIPoint componentCenter = RectCenter(vectorLayout.componentRects[2]); + + frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + { + MakePointerDown(componentCenter.x, componentCenter.y), + MakePointerUp(componentCenter.x, componentCenter.y) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_EQ(frame.result.selectedFieldId, "position"); + ASSERT_EQ(state.vector3FieldInteractionStates.size(), 1u); + EXPECT_EQ(state.vector3FieldInteractionStates[0].fieldId, "position"); + EXPECT_TRUE(state.vector3FieldInteractionStates[0].state.vector3FieldState.editing); + EXPECT_EQ( + state.vector3FieldInteractionStates[0].state.vector3FieldState.selectedComponentIndex, + 2u); + + frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + { + MakeKeyDown(KeyCode::Backspace), + MakeCharacter('9'), + MakeKeyDown(KeyCode::Enter) + }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_TRUE(frame.result.fieldValueChanged); + EXPECT_EQ(frame.result.changedFieldId, "position"); + EXPECT_EQ(frame.result.changedValue, "1, 2, 9"); + EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[0], 1.0); + EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[1], 2.0); + EXPECT_DOUBLE_EQ(sections[0].fields[0].vector3Value.values[2], 9.0); +} + +TEST(UIEditorPropertyGridInteractionTest, AssetFieldCanRequestPickerAndClearInsidePropertyGridHost) { + std::vector sections = { + { + "renderer", + "Renderer", + { + MakeAssetField( + "material", + "Material", + "Assets/Materials/Stone.mat", + "Stone.mat") + }, + 0.0f + } + }; + UISelectionModel selectionModel = {}; + UIExpansionModel expansionModel = {}; + expansionModel.Expand("renderer"); + UIPropertyEditModel propertyEditModel = {}; + UIEditorPropertyGridInteractionState state = {}; + + auto frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + {}); + + XCEngine::UI::Editor::Widgets::UIEditorAssetFieldSpec assetSpec = {}; + assetSpec.fieldId = "material"; + assetSpec.label = "Material"; + assetSpec.assetId = sections[0].fields[0].assetValue.assetId; + assetSpec.displayName = sections[0].fields[0].assetValue.displayName; + assetSpec.statusText = sections[0].fields[0].assetValue.statusText; + assetSpec.emptyText = sections[0].fields[0].assetValue.emptyText; + assetSpec.tint = sections[0].fields[0].assetValue.tint; + assetSpec.showPickerButton = sections[0].fields[0].assetValue.showPickerButton; + assetSpec.allowClear = sections[0].fields[0].assetValue.allowClear; + assetSpec.showStatusBadge = sections[0].fields[0].assetValue.showStatusBadge; + const auto assetLayout = BuildUIEditorAssetFieldLayout( + frame.layout.fieldRowRects[0], + assetSpec, + BuildUIEditorPropertyGridAssetFieldMetrics({})); + const UIPoint pickerCenter = RectCenter(assetLayout.pickerRect); + const UIPoint clearCenter = RectCenter(assetLayout.clearRect); + + frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + { + MakePointerDown(pickerCenter.x, pickerCenter.y), + MakePointerUp(pickerCenter.x, pickerCenter.y) + }); + + EXPECT_TRUE(frame.result.pickerRequested); + EXPECT_EQ(frame.result.requestedFieldId, "material"); + EXPECT_TRUE(selectionModel.IsSelected("material")); + + frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 520.0f, 360.0f), + sections, + { + MakePointerDown(clearCenter.x, clearCenter.y), + MakePointerUp(clearCenter.x, clearCenter.y) + }); + + EXPECT_TRUE(frame.result.fieldValueChanged); + EXPECT_EQ(frame.result.changedFieldId, "material"); + EXPECT_EQ(frame.result.changedValue, "None"); + EXPECT_TRUE(sections[0].fields[0].assetValue.assetId.empty()); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp index ece8b1ba..3c519a2b 100644 --- a/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_input_bridge.cpp @@ -6,6 +6,7 @@ namespace { using XCEngine::UI::UIInputEvent; using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIInputModifiers; using XCEngine::UI::UIPoint; using XCEngine::UI::UIPointerButton; using XCEngine::UI::UIRect; @@ -26,6 +27,45 @@ UIInputEvent MakePointerEvent( return event; } +UIInputEvent MakePointerEventWithModifiers( + UIInputEventType type, + float x, + float y, + const XCEngine::UI::UIInputModifiers& modifiers, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + event.modifiers = modifiers; + return event; +} + +UIInputModifiers MakePointerModifiers(UIPointerButton button) { + UIInputModifiers modifiers = {}; + switch (button) { + case UIPointerButton::Left: + modifiers.leftMouse = true; + break; + case UIPointerButton::Right: + modifiers.rightMouse = true; + break; + case UIPointerButton::Middle: + modifiers.middleMouse = true; + break; + case UIPointerButton::X1: + modifiers.x1Mouse = true; + break; + case UIPointerButton::X2: + modifiers.x2Mouse = true; + break; + case UIPointerButton::None: + default: + break; + } + return modifiers; +} + UIInputEvent MakeKeyEvent( UIInputEventType type, std::int32_t keyCode) { @@ -106,6 +146,64 @@ TEST(UIEditorViewportInputBridgeTest, PointerUpEndsCaptureAndOutsidePointerDownC EXPECT_FALSE(frame.focused); } +TEST(UIEditorViewportInputBridgeTest, CoalescedPointerClickPreservesBothButtonTransitions) { + UIEditorViewportInputBridgeState state = {}; + + const auto frame = UpdateUIEditorViewportInputBridge( + state, + UIRect(100.0f, 200.0f, 640.0f, 360.0f), + { + MakePointerEvent( + UIInputEventType::PointerButtonDown, + 220.0f, + 280.0f, + UIPointerButton::Left), + MakePointerEvent( + UIInputEventType::PointerButtonUp, + 220.0f, + 280.0f, + UIPointerButton::Left) + }); + + ASSERT_EQ(frame.pointerButtonTransitions.size(), 2u); + EXPECT_TRUE(frame.pointerButtonTransitions[0].pressed); + EXPECT_EQ(frame.pointerButtonTransitions[0].button, UIPointerButton::Left); + EXPECT_TRUE(frame.pointerButtonTransitions[0].inside); + EXPECT_FALSE(frame.pointerButtonTransitions[1].pressed); + EXPECT_EQ(frame.pointerButtonTransitions[1].button, UIPointerButton::Left); + EXPECT_TRUE(frame.pointerButtonTransitions[1].inside); +} + +TEST(UIEditorViewportInputBridgeTest, InteractionRectCanIncludeTopBarWhileLocalCoordinatesStayBoundToSurface) { + UIEditorViewportInputBridgeState state = {}; + const UIRect interactionRect(100.0f, 80.0f, 640.0f, 406.0f); + const UIRect localRect(100.0f, 104.0f, 640.0f, 382.0f); + + const auto frame = UpdateUIEditorViewportInputBridge( + state, + interactionRect, + localRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 92.0f), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + 220.0f, + 92.0f, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.hasPointerPosition); + EXPECT_TRUE(frame.hovered); + EXPECT_TRUE(frame.focused); + EXPECT_TRUE(frame.captured); + EXPECT_TRUE(frame.pointerPressedInside); + EXPECT_FLOAT_EQ(frame.localPointerPosition.x, 120.0f); + EXPECT_FLOAT_EQ(frame.localPointerPosition.y, -12.0f); + ASSERT_EQ(frame.pointerButtonTransitions.size(), 1u); + EXPECT_TRUE(frame.pointerButtonTransitions.front().inside); + EXPECT_FLOAT_EQ(frame.pointerButtonTransitions.front().localPointerPosition.y, -12.0f); +} + TEST(UIEditorViewportInputBridgeTest, PointerMoveWhileCapturedKeepsDeltaEvenOutsideSurface) { UIEditorViewportInputBridgeState state = {}; UpdateUIEditorViewportInputBridge( @@ -119,7 +217,11 @@ TEST(UIEditorViewportInputBridgeTest, PointerMoveWhileCapturedKeepsDeltaEvenOuts state, UIRect(100.0f, 200.0f, 640.0f, 360.0f), { - MakePointerEvent(UIInputEventType::PointerMove, 60.0f, 120.0f) + MakePointerEventWithModifiers( + UIInputEventType::PointerMove, + 60.0f, + 120.0f, + MakePointerModifiers(UIPointerButton::Left)) }); EXPECT_TRUE(frame.pointerMoved); @@ -131,6 +233,96 @@ TEST(UIEditorViewportInputBridgeTest, PointerMoveWhileCapturedKeepsDeltaEvenOuts EXPECT_FLOAT_EQ(frame.localPointerPosition.y, -80.0f); } +TEST(UIEditorViewportInputBridgeTest, RightPointerCaptureTracksHeldButtonAcrossDragFrames) { + UIEditorViewportInputBridgeState state = {}; + const UIRect inputRect(100.0f, 200.0f, 640.0f, 360.0f); + + auto frame = UpdateUIEditorViewportInputBridge( + state, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 280.0f), + MakePointerEventWithModifiers( + UIInputEventType::PointerButtonDown, + 220.0f, + 280.0f, + MakePointerModifiers(UIPointerButton::Right), + UIPointerButton::Right) + }); + + EXPECT_TRUE(frame.focused); + EXPECT_TRUE(frame.captured); + EXPECT_TRUE(frame.captureStarted); + EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right)); + + frame = UpdateUIEditorViewportInputBridge( + state, + inputRect, + { + MakePointerEventWithModifiers( + UIInputEventType::PointerMove, + 260.0f, + 320.0f, + MakePointerModifiers(UIPointerButton::Right)) + }); + + EXPECT_TRUE(frame.captured); + EXPECT_TRUE(frame.pointerMoved); + EXPECT_FLOAT_EQ(frame.pointerDelta.x, 40.0f); + EXPECT_FLOAT_EQ(frame.pointerDelta.y, 40.0f); + EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right)); + + frame = UpdateUIEditorViewportInputBridge( + state, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerButtonUp, 260.0f, 320.0f, UIPointerButton::Right) + }); + + EXPECT_TRUE(frame.captureEnded); + EXPECT_FALSE(frame.captured); + EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Right)); +} + +TEST(UIEditorViewportInputBridgeTest, PointerMoveReconcilesReleasedMouseButtonWhenUpEventWasMissed) { + UIEditorViewportInputBridgeState state = {}; + const UIRect inputRect(100.0f, 200.0f, 640.0f, 360.0f); + + XCEngine::UI::UIInputModifiers leftDownModifiers = {}; + leftDownModifiers.leftMouse = true; + + auto frame = UpdateUIEditorViewportInputBridge( + state, + inputRect, + { + MakePointerEventWithModifiers( + UIInputEventType::PointerButtonDown, + 220.0f, + 280.0f, + leftDownModifiers, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.captured); + EXPECT_TRUE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left)); + + frame = UpdateUIEditorViewportInputBridge( + state, + inputRect, + { + MakePointerEventWithModifiers( + UIInputEventType::PointerMove, + 260.0f, + 320.0f, + {}) + }); + + EXPECT_TRUE(frame.pointerMoved); + EXPECT_TRUE(frame.captureEnded); + EXPECT_FALSE(frame.captured); + EXPECT_FALSE(IsUIEditorViewportInputBridgePointerButtonDown(state, UIPointerButton::Left)); +} + TEST(UIEditorViewportInputBridgeTest, WheelAndKeyboardAreAcceptedOnlyWhileFocused) { UIEditorViewportInputBridgeState state = {}; diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp index 84c41583..5751f3dc 100644 --- a/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_shell.cpp @@ -129,4 +129,48 @@ TEST(UIEditorViewportShellTest, UpdateShellDoesNotIntroduceInvalidIndicesByDefau EXPECT_FALSE(frame.slotState.statusBarState.focused); } +TEST(UIEditorViewportShellTest, TopBarPointerDownUsesShellBoundsButKeepsSurfaceHoverSeparate) { + UIEditorViewportShellModel model = {}; + model.spec.chrome.showTopBar = true; + model.spec.toolItems = { + XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem{ + "scene.pivot.toggle", + "Pivot", + XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot::Leading, + true, + false, + 68.0f + } + }; + UIEditorViewportShellState state = {}; + + const auto request = ResolveUIEditorViewportShellRequest( + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model.spec); + const UIRect topBarRect = request.slotLayout.topBarRect; + const UIPoint clickPoint( + topBarRect.x + 20.0f, + topBarRect.y + topBarRect.height * 0.5f); + + const auto frame = UpdateUIEditorViewportShell( + state, + UIRect(10.0f, 20.0f, 800.0f, 600.0f), + model, + { + MakePointerEvent(UIInputEventType::PointerMove, clickPoint.x, clickPoint.y), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + clickPoint.x, + clickPoint.y, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.inputFrame.pointerPressedInside); + EXPECT_TRUE(frame.inputFrame.captureStarted); + EXPECT_TRUE(frame.inputFrame.focused); + EXPECT_FALSE(frame.slotState.surfaceHovered); + EXPECT_TRUE(frame.slotState.inputCaptured); + EXPECT_LT(frame.inputFrame.localPointerPosition.y, 0.0f); +} + } // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp b/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp index b7ae53dd..43a17032 100644 --- a/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_viewport_slot.cpp @@ -143,7 +143,9 @@ TEST(UIEditorViewportSlotTest, ToolItemsAlignToEdgesAndTitleRectClampsBetweenToo {}); EXPECT_FLOAT_EQ(layout.toolItemRects[0].x, 8.0f); + EXPECT_FLOAT_EQ(layout.toolItemRects[0].y, 0.0f); EXPECT_FLOAT_EQ(layout.toolItemRects[0].width, 96.0f); + EXPECT_FLOAT_EQ(layout.toolItemRects[0].height, 40.0f); EXPECT_FLOAT_EQ(layout.toolItemRects[1].x, 768.0f); EXPECT_FLOAT_EQ(layout.toolItemRects[2].x, 820.0f); EXPECT_FLOAT_EQ(layout.titleRect.x, 110.0f); diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_interaction.cpp index eb020f0b..80cf3e3f 100644 --- a/tests/UI/Editor/unit/test_ui_editor_workspace_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_interaction.cpp @@ -186,6 +186,45 @@ TEST(UIEditorWorkspaceInteractionTest, PointerUpInsideViewportBubblesPointerRele EXPECT_TRUE(frame.result.viewportInputFrame.captureEnded); } +TEST(UIEditorWorkspaceInteractionTest, PointerDownOnViewportTopBarBubblesPointerCaptureRequest) { + auto controller = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + UIEditorWorkspaceInteractionState state = {}; + const UIEditorWorkspaceInteractionModel model = BuildInteractionModel(); + + auto frame = UpdateUIEditorWorkspaceInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + {}); + const auto* viewportFrame = + FindUIEditorWorkspaceViewportPresentationFrame(frame.composeFrame, "viewport"); + ASSERT_NE(viewportFrame, nullptr); + const UIPoint topBarCenter = RectCenter(viewportFrame->viewportShellFrame.slotLayout.topBarRect); + + frame = UpdateUIEditorWorkspaceInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model, + { + MakePointerEvent(UIInputEventType::PointerMove, topBarCenter.x, topBarCenter.y), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + topBarCenter.x, + topBarCenter.y, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.consumed); + EXPECT_TRUE(frame.result.requestPointerCapture); + EXPECT_EQ(frame.result.viewportPanelId, "viewport"); + EXPECT_TRUE(frame.result.viewportInputFrame.captureStarted); + EXPECT_TRUE(frame.result.viewportInputFrame.pointerPressedInside); + EXPECT_LT(frame.result.viewportInputFrame.localPointerPosition.y, 0.0f); +} + TEST(UIEditorWorkspaceInteractionTest, ActivatingDocumentTabRemovesViewportPresentationInSameFrame) { auto controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp index 37d8af84..191c9b65 100644 --- a/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp @@ -80,6 +80,29 @@ TEST(UIEditorWorkspaceModelTest, ValidationRejectsTabStackWithNestedSplitChild) EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::NonPanelTabChild); } +TEST(UIEditorWorkspaceModelTest, ValidationRejectsDuplicateNodeIds) { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root-split", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.5f, + BuildUIEditorWorkspaceTabStack( + "left-tabs", + { + BuildUIEditorWorkspacePanel("duplicate-node", "panel-a", "Panel A", true) + }, + 0u), + BuildUIEditorWorkspaceTabStack( + "right-tabs", + { + BuildUIEditorWorkspacePanel("duplicate-node", "panel-b", "Panel B", true) + }, + 0u)); + + const auto result = ValidateUIEditorWorkspace(workspace); + EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::DuplicateNodeId); +} + TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplitTree) { UIEditorWorkspaceModel workspace = {}; workspace.root = BuildUIEditorWorkspaceSplit( diff --git a/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp b/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp new file mode 100644 index 00000000..295a4dbc --- /dev/null +++ b/tests/UI/Editor/unit/test_viewport_object_id_picker.cpp @@ -0,0 +1,97 @@ +#include "app/Rendering/Viewport/ViewportObjectIdPicker.h" + +#include + +namespace { + +using XCEngine::RHI::ResourceStates; +using XCEngine::Rendering::RenderObjectId; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UISize; +using XCEngine::UI::Editor::App::BuildViewportObjectIdReadbackRequest; +using XCEngine::UI::Editor::App::PickViewportObjectIdEntity; +using XCEngine::UI::Editor::App::ResolveViewportObjectIdPixelCoordinate; +using XCEngine::UI::Editor::App::ViewportObjectIdPickContext; +using XCEngine::UI::Editor::App::ViewportObjectIdPickStatus; +using XCEngine::UI::Editor::App::ViewportObjectIdReadbackRequest; + +TEST(ViewportObjectIdPickerTest, ResolveViewportObjectIdPixelCoordinateScalesAndClampsToTextureExtent) { + EXPECT_EQ( + ResolveViewportObjectIdPixelCoordinate(100.0f, 200.0f, 800u), + 400u); + EXPECT_EQ( + ResolveViewportObjectIdPixelCoordinate(200.0f, 200.0f, 800u), + 799u); + EXPECT_EQ( + ResolveViewportObjectIdPixelCoordinate(-30.0f, 200.0f, 800u), + 0u); +} + +TEST(ViewportObjectIdPickerTest, BuildViewportObjectIdReadbackRequestRejectsUnavailableContext) { + ViewportObjectIdReadbackRequest request = {}; + EXPECT_FALSE(BuildViewportObjectIdReadbackRequest({}, request)); +} + +TEST(ViewportObjectIdPickerTest, BuildViewportObjectIdReadbackRequestMapsViewportCoordinatesIntoTextureSpace) { + ViewportObjectIdPickContext context = {}; + context.commandQueue = reinterpret_cast(static_cast(0x1)); + context.texture = reinterpret_cast(static_cast(0x2)); + context.textureState = ResourceStates::PixelShaderResource; + context.textureWidth = 1024u; + context.textureHeight = 512u; + context.hasValidFrame = true; + context.viewportSize = UISize(256.0f, 128.0f); + context.viewportMousePosition = UIPoint(128.0f, 64.0f); + + ViewportObjectIdReadbackRequest request = {}; + ASSERT_TRUE(BuildViewportObjectIdReadbackRequest(context, request)); + EXPECT_EQ(request.pixelX, 512u); + EXPECT_EQ(request.pixelY, 256u); +} + +TEST(ViewportObjectIdPickerTest, PickViewportObjectIdEntityDecodesRuntimeObjectIdFromReadbackColor) { + ViewportObjectIdPickContext context = {}; + context.commandQueue = reinterpret_cast(static_cast(0x1)); + context.texture = reinterpret_cast(static_cast(0x2)); + context.textureState = ResourceStates::PixelShaderResource; + context.textureWidth = 256u; + context.textureHeight = 256u; + context.hasValidFrame = true; + context.viewportSize = UISize(256.0f, 256.0f); + context.viewportMousePosition = UIPoint(32.0f, 48.0f); + + constexpr RenderObjectId kExpectedObjectId = 0x00030201u; + const auto result = PickViewportObjectIdEntity( + context, + [](const auto&, std::array& outRgba) { + outRgba = { 0x01u, 0x02u, 0x03u, 0x00u }; + return true; + }); + + EXPECT_EQ(result.status, ViewportObjectIdPickStatus::Success); + EXPECT_EQ(result.renderObjectId, kExpectedObjectId); + EXPECT_EQ(result.entityId, static_cast(kExpectedObjectId)); +} + +TEST(ViewportObjectIdPickerTest, PickViewportObjectIdEntityReportsReadbackFailure) { + ViewportObjectIdPickContext context = {}; + context.commandQueue = reinterpret_cast(static_cast(0x1)); + context.texture = reinterpret_cast(static_cast(0x2)); + context.textureState = ResourceStates::PixelShaderResource; + context.textureWidth = 64u; + context.textureHeight = 64u; + context.hasValidFrame = true; + context.viewportSize = UISize(64.0f, 64.0f); + context.viewportMousePosition = UIPoint(4.0f, 8.0f); + + const auto result = PickViewportObjectIdEntity( + context, + [](const auto&, std::array&) { + return false; + }); + + EXPECT_EQ(result.status, ViewportObjectIdPickStatus::ReadbackFailed); + EXPECT_EQ(result.entityId, 0u); +} + +} // namespace diff --git a/tests/editor/test_scene_viewport_overlay_renderer.cpp b/tests/editor/test_scene_viewport_overlay_renderer.cpp index ff5840c4..824582dc 100644 --- a/tests/editor/test_scene_viewport_overlay_renderer.cpp +++ b/tests/editor/test_scene_viewport_overlay_renderer.cpp @@ -3,10 +3,10 @@ #include "Viewport/SceneViewportCameraController.h" #include "Viewport/SceneViewportHudOverlay.h" #include "Viewport/SceneViewportMath.h" +#include "Viewport/SceneViewportPassSpecs.h" #include #include -#include #include namespace { @@ -48,9 +48,9 @@ bool IsPowerOfTenSpacing(float value) { } // namespace -XCEngine::Rendering::Passes::InfiniteGridPassData ToInfiniteGridPassData( +XCEngine::Editor::SceneViewportGridPassData ToInfiniteGridPassData( const XCEngine::Editor::SceneViewportOverlayData& overlay) { - XCEngine::Rendering::Passes::InfiniteGridPassData data = {}; + XCEngine::Editor::SceneViewportGridPassData data = {}; data.valid = overlay.valid; data.cameraPosition = overlay.cameraPosition; data.cameraForward = overlay.cameraForward; @@ -76,11 +76,11 @@ using XCEngine::Editor::SceneViewportOverlayFrameData; using XCEngine::Editor::SceneViewportOverlaySpritePrimitive; using XCEngine::Editor::SceneViewportOverlaySpriteTextureKind; using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Editor::BuildInfiniteGridParameters; +using XCEngine::Editor::InfiniteGridParameters; using XCEngine::Components::GameObject; using XCEngine::Math::Vector3; using XCEngine::Math::Vector4; -using XCEngine::Rendering::Passes::BuildInfiniteGridParameters; -using XCEngine::Rendering::Passes::InfiniteGridParameters; TEST(SceneViewportOverlayRenderer_Test, BuildInfiniteGridParametersUsesPowerOfTenSpacingSeries) { SceneViewportOverlayData overlay = {}; diff --git a/tests/editor/test_viewport_render_flow_utils.cpp b/tests/editor/test_viewport_render_flow_utils.cpp index 43c6f76e..9bc2c711 100644 --- a/tests/editor/test_viewport_render_flow_utils.cpp +++ b/tests/editor/test_viewport_render_flow_utils.cpp @@ -206,23 +206,23 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupAttachesOptionalPa RenderPassSequence postPasses; postPasses.AddPass(std::make_unique()); - XCEngine::Rendering::CameraRenderRequest request = {}; - request.surface = RenderSurface(800, 600); - request.surface.SetRenderArea(XCEngine::Math::RectInt(64, 32, 320, 240)); + XCEngine::Rendering::CameraFramePlan plan = {}; + plan.request.surface = RenderSurface(800, 600); + plan.request.surface.SetRenderArea(XCEngine::Math::RectInt(64, 32, 320, 240)); ApplySceneViewportRenderRequestSetup( targets, &postPasses, - request); + plan); - EXPECT_EQ(request.postScenePasses, &postPasses); - EXPECT_TRUE(request.objectId.IsRequested()); - ASSERT_EQ(request.objectId.surface.GetColorAttachments().size(), 1u); - EXPECT_EQ(request.objectId.surface.GetColorAttachments()[0], &objectIdView); - EXPECT_EQ(request.objectId.surface.GetDepthAttachment(), &objectIdDepthView); + EXPECT_EQ(plan.postScenePasses, &postPasses); + EXPECT_TRUE(plan.request.objectId.IsRequested()); + ASSERT_EQ(plan.request.objectId.surface.GetColorAttachments().size(), 1u); + EXPECT_EQ(plan.request.objectId.surface.GetColorAttachments()[0], &objectIdView); + EXPECT_EQ(plan.request.objectId.surface.GetDepthAttachment(), &objectIdDepthView); - const auto requestArea = request.surface.GetRenderArea(); - const auto objectIdArea = request.objectId.surface.GetRenderArea(); + const auto requestArea = plan.request.surface.GetRenderArea(); + const auto objectIdArea = plan.request.objectId.surface.GetRenderArea(); EXPECT_EQ(objectIdArea.x, requestArea.x); EXPECT_EQ(objectIdArea.y, requestArea.y); EXPECT_EQ(objectIdArea.width, requestArea.width); @@ -236,16 +236,16 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneRenderRequestSetupSkipsUnavailableOp RenderPassSequence postPasses; - XCEngine::Rendering::CameraRenderRequest request = {}; - request.postScenePasses = reinterpret_cast(static_cast(0x1)); - request.objectId.surface = RenderSurface(1, 1); - request.objectId.surface.SetColorAttachment( + XCEngine::Rendering::CameraFramePlan plan = {}; + plan.postScenePasses = reinterpret_cast(static_cast(0x1)); + plan.request.objectId.surface = RenderSurface(1, 1); + plan.request.objectId.surface.SetColorAttachment( reinterpret_cast(static_cast(0x2))); - ApplySceneViewportRenderRequestSetup(targets, &postPasses, request); + ApplySceneViewportRenderRequestSetup(targets, &postPasses, plan); - EXPECT_EQ(request.postScenePasses, nullptr); - EXPECT_FALSE(request.objectId.IsRequested()); + EXPECT_EQ(plan.postScenePasses, nullptr); + EXPECT_FALSE(plan.request.objectId.IsRequested()); } TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanCollectsPostSceneAndOverlayPasses) { @@ -397,19 +397,19 @@ TEST(ViewportRenderFlowUtilsTest, ApplySceneViewportRenderPlanAttachesPlannedPas plan.overlayPasses.AddPass(std::make_unique()); plan.clearColorOverride = XCEngine::Math::Color(0.1f, 0.2f, 0.3f, 1.0f); - XCEngine::Rendering::CameraRenderRequest request = {}; - request.surface = RenderSurface(800, 600); - request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200)); + XCEngine::Rendering::CameraFramePlan framePlan = {}; + framePlan.request.surface = RenderSurface(800, 600); + framePlan.request.surface.SetRenderArea(XCEngine::Math::RectInt(10, 20, 300, 200)); - ApplySceneViewportRenderPlan(targets, plan, request); + ApplySceneViewportRenderPlan(targets, plan, framePlan); - EXPECT_EQ(request.postScenePasses, &plan.postScenePasses); - EXPECT_EQ(request.overlayPasses, &plan.overlayPasses); - EXPECT_TRUE(request.objectId.IsRequested()); - EXPECT_TRUE(request.hasClearColorOverride); - EXPECT_FLOAT_EQ(request.clearColorOverride.r, 0.1f); - EXPECT_FLOAT_EQ(request.clearColorOverride.g, 0.2f); - EXPECT_FLOAT_EQ(request.clearColorOverride.b, 0.3f); + EXPECT_EQ(framePlan.postScenePasses, &plan.postScenePasses); + EXPECT_EQ(framePlan.overlayPasses, &plan.overlayPasses); + EXPECT_TRUE(framePlan.request.objectId.IsRequested()); + EXPECT_TRUE(framePlan.request.hasClearColorOverride); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.r, 0.1f); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.g, 0.2f); + EXPECT_FLOAT_EQ(framePlan.request.clearColorOverride.b, 0.3f); } TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderResourceState) { @@ -429,11 +429,11 @@ TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderReso targets.objectIdState = ResourceStates::Common; targets.selectionMaskState = ResourceStates::Common; - XCEngine::Rendering::CameraRenderRequest request = {}; - request.surface = RenderSurface(640, 360); - ApplySceneViewportRenderRequestSetup(targets, nullptr, request); + XCEngine::Rendering::CameraFramePlan framePlan = {}; + framePlan.request.surface = RenderSurface(640, 360); + ApplySceneViewportRenderRequestSetup(targets, nullptr, framePlan); - MarkSceneViewportRenderSuccess(targets, request); + MarkSceneViewportRenderSuccess(targets, framePlan); EXPECT_EQ(targets.colorState, ResourceStates::PixelShaderResource); EXPECT_EQ(targets.objectIdState, ResourceStates::PixelShaderResource); EXPECT_EQ(targets.selectionMaskState, ResourceStates::PixelShaderResource); @@ -443,9 +443,9 @@ TEST(ViewportRenderFlowUtilsTest, MarkSceneRenderSuccessMovesTargetsToShaderReso noObjectIdTargets.colorState = ResourceStates::Common; noObjectIdTargets.objectIdState = ResourceStates::Common; noObjectIdTargets.selectionMaskState = ResourceStates::Common; - XCEngine::Rendering::CameraRenderRequest noObjectIdRequest = {}; + XCEngine::Rendering::CameraFramePlan noObjectIdPlan = {}; - MarkSceneViewportRenderSuccess(noObjectIdTargets, noObjectIdRequest); + MarkSceneViewportRenderSuccess(noObjectIdTargets, noObjectIdPlan); EXPECT_EQ(noObjectIdTargets.colorState, ResourceStates::PixelShaderResource); EXPECT_EQ(noObjectIdTargets.objectIdState, ResourceStates::PixelShaderResource); EXPECT_EQ(noObjectIdTargets.selectionMaskState, ResourceStates::PixelShaderResource);