diff --git a/AGENT.md b/AGENT.md index 47fe5875..c341687b 100644 --- a/AGENT.md +++ b/AGENT.md @@ -61,7 +61,7 @@ - `engine/` 构建静态库 `XCEngine`;`editor/` 构建 `XCUIEditor` 静态库和 `XCEditor` 可执行目标。 - `editor/` 目前继续保留为当前正式编辑器、行为对照和视觉基线来源。 - 启用 `XCENGINE_BUILD_XCUI_EDITOR_APP` 时,`XCEditor` 输出 `build/editor//XCEngine.exe`。 -- editor 默认把仓库内的 `project/` 识别为工程根目录,也支持 `--project ` 覆盖。 +- 当前 XCUI editor 把仓库内的 `project/` 固定识别为工程根目录;不要再沿用旧的 `--project ` 说法。 - 当前工程真实使用 `Assets/ + .meta + Library/` 的项目布局;`project/Library/` 是当前 workflow 的一部分,不是可随手忽略的垃圾目录。 - Mono 运行时与 editor 的脚本类发现都从 `/Library/ScriptAssemblies/` 加载程序集。 - 当前根目录里没有 `native` 占位项;更新目录树或迁移旧文档时,不要继续传播这条过期事实。 @@ -108,13 +108,14 @@ ### 3.3 Editor - editor 仍然是 `D3D12` 宿主应用。 -- Scene/Game viewport 已通过引擎 `Rendering + RHI` 输出离屏纹理,再由 editor 宿主接入 ImGui。 +- Scene/Game viewport 已通过引擎 `Rendering + RHI` 输出离屏纹理,再由 editor 宿主接入 XCUI shell。 - 当前 Scene View 主链已经显式拆成: - `SceneViewportChrome` - `SceneViewportInteractionFrame` - `SceneViewportNavigation` - `SceneViewportTransformGizmoCoordinator` - `ViewportHostService` +- 当前 editor 产品装配的单一事实源是 `editor/app/Core/Product/EditorProductManifest.*`。panel 声明、action route、runtime owner、viewport renderer owner 先看这里,再看派生出来的 shell / menu / runtime / viewport 注册。 - `editor/src/Viewport/` 当前稳定存在的关键入口包括: - `SceneViewportCameraController` - `SceneViewportChrome` @@ -149,9 +150,11 @@ - `editor/` 是当前 XCUI 编辑器主线;不要再把 `new_editor/` 当成当前 checkout 的构建入口。 - 当前宿主分层是: - `XCUIEditor` + - `XCEditorCore` - `XCEditor`(可选应用壳,输出 `XCEngine.exe`) - 共享 UI core、runtime screen host 与 widget 基础能力主要沉淀在 `engine/include/XCEngine/UI/` 与 `engine/src/UI/`;`editor/` 负责 XCUI editor 壳、宿主与产品装配。 - `tests/UI/` 是当前 XCUI `Core / Editor / Runtime` 三层的唯一正式基础层验证入口;`editor/` 不承担测试堆场职责。 +- 当前 `game` panel 已在产品 manifest 中显式声明为 placeholder viewport:它会显示未实现状态,但没有正式 Game runtime / renderer owner,不要把它当作已完成能力。 ### 3.5 Scripting diff --git a/editor/AGENTS.md b/editor/AGENTS.md new file mode 100644 index 00000000..a68cce0b --- /dev/null +++ b/editor/AGENTS.md @@ -0,0 +1,123 @@ +# XCEditor Agent Guide + +本文面向以后在 `editor/` 下工作的 coding agent / 开发者。若本文与代码、`CMakeLists.txt` 或测试目标冲突,以代码为准,并在同一次改动中更新本文。 + +## 长期目标 + +- `editor/` 是当前 XCUI 编辑器主线,不是旧 `mvs/editor` 或 `new_editor` 入口。长期方向是把可复用 UI shell 与产品编辑器装配继续分离清楚。 +- `XCUIEditor` 保持为平台无关、后端无关的静态库。公共头在 `include/XCEditor/**`,实现主要在 `src/**`;不要放入 Win32、D3D12、DXGI、产品面板状态或工程 runtime 事实。 +- `XCEditorCore` 承担产品编辑器核心。代码主要在 `app/Core`、`app/Composition`、`app/Features`、`app/Services`、`app/Windowing`,并通过 `XCEditorCoreRendering` 对象库接入 `app/Rendering`;它不直接拥有 Win32 消息循环。 +- `XCEditor` 是可选 Win32 + D3D12 应用壳。代码在 `app/Bootstrap`、`app/Host/Win32`、`app/Host/D3D12`,负责窗口、DPI、消息分发、swapchain、UI 纹理和截图。 +- 产品装配应以 `app/Core/Product/EditorProductManifest.*` 为单一事实源。正式 panel 集、action route、runtime owner、viewport renderer owner 先在 manifest 中声明,再派生 shell / menu / command / runtime / viewport 注册。 +- UI widget / shell / workspace 代码优先保持 model/state/request/frame/result 风格。新增行为要能被 `tests/UI/Editor/unit` 以纯状态方式测试。 +- scene/project 的用户操作应通过 runtime 或 command route 进入,不要在 draw/append 阶段直接改 scene 或文件系统。 +- scene 渲染私有逻辑不要塞进 panel 或 shell。新增渲染能力时先判断它属于 engine `Rendering/RHI`、editor viewport pass bundle,还是 UI overlay。 +- 资源路径、图标、shader、截图输出都应走明确服务或 host 接口。不要硬编码从当前工作目录猜路径;手动验证截图不得写回 source tree。 + +## 当前情况 + +- `editor/AGENTS.md` 本身被 `app/Bootstrap/Application.cpp` 的 `HasEditorRepoMarkers()` 用作仓库根定位标记之一。不要重命名、删除或移动它;如果根定位规则变了,同时更新本文。 +- 顶层 `CMakeLists.txt` 默认启用 `XCENGINE_BUILD_XCUI_EDITOR_CORE` 和 `XCENGINE_BUILD_XCUI_EDITOR_APP`。构建 core/app 时必须启用 `XCENGINE_ENABLE_RENDERING_EDITOR_SUPPORT`。 +- `XCEditor` 目标输出名是 `XCEngine.exe`,Debug 产物位于 `build/editor/Debug/XCEngine.exe`。 +- 当前应用没有命令行项目选择流程;`EditorContext` 将项目根固定为 `/project`,不要沿用旧文档里的 `--project ` 说法。 +- 当前面板 ID 为 `hierarchy`、`scene`、`game`、`inspector`、`console`、`project`,由 `EditorProductManifest.*` 声明并驱动注册。 +- 当前 `game` panel 是 viewport shell,但 renderer owner 是 placeholder,不是正式 Game runtime。它会显示 `Game view runtime is not implemented`,不要假设 Game view 已完整实现。 +- 当前 command 事实:`file.exit` 已绑定退出;`edit.*` 通过 active route 分发到 hierarchy/project/scene/inspector;`assets.*` 通过 project route 分发;多数 `file.*`、`run.*`、`scripts.*`、`help.about` 仍只是菜单/命令 surface,未拥有完整 host owner。 +- `EditorContext` 是产品级状态聚合点:`EditorSession`、project runtime、scene runtime、selection、command focus、utility window request、shortcut manager 和 host command bridge 都在这里串接。 +- `UIEditorWorkspaceController` 管单窗口 workspace model/session;`EditorWindowSystem` 管多窗口 workspace set。跨窗口 detach、close、update 必须经过 synchronization plan。 +- `EditorWorkspacePanelRuntimeSet` 托管产品面板生命周期。新增 panel 时先改 `EditorProductManifest.*`,再按需要调整 `BuildEditorWorkspaceModel()` 的默认布局和测试;不要再手工在 registry / menu / runtime set / viewport host 多处分别补定义。 +- `EditorSelectionService` 是 hierarchy/project/inspector/scene viewport 之间的选择同步核心。不要在单个 panel 内维护另一套长期选择真相。 +- `EditorProjectRuntime` 包装 `ProjectBrowserModel`,负责 project tree/grid、选择、文件操作、scene asset open request。文件系统改动后要刷新并 revalidate selection。 +- `EditorSceneRuntime` 负责 startup scene、editor scene camera、hierarchy selection、component list、transform edit history 和 scene tool state。 + +当前目录地图: + +- `include/XCEditor/Foundation`:命令注册/分发、快捷键、主题、文本测量接口、runtime trace。 +- `include/XCEditor/Fields`:bool、number、text、enum、asset、object、color、vector、property grid 等编辑字段的绘制模型与交互。 +- `include/XCEditor/Collections`:list/tree/tab/scroll/inline rename/drag-drop 等集合控件。 +- `include/XCEditor/Docking`、`include/XCEditor/Workspace`:dock host、workspace tree、layout persistence、splitter correction、panel detach/transfer。 +- `include/XCEditor/Shell`:menu/toolbar/status/workspace 的 shell 组合与交互入口。 +- `include/XCEditor/Viewport`:viewport slot/shell/input bridge。这里是通用 UI viewport 容器,不是 scene renderer。 +- `include/XCEditor/Windowing`:多窗口 workspace 状态、同步计划和 presentation policy。 +- `src/**`:对应公共头的实现。保持偏纯函数/状态机风格。 +- `app/Core`:产品级 contracts、session、command focus、selection、panel services、scene/project/viewport/windowing 接口。 +- `app/Core/Product`:产品 manifest。这里定义正式 panel 集、route 归属、runtime owner 和 viewport renderer owner。 +- `app/Composition`:装配编辑器 shell。`EditorContext` 拥有 session、project runtime、scene runtime、selection、command bridge;`EditorShellRuntime` 驱动 shell interaction、hosted panels 和 viewport runtime。 +- `app/Features`:产品面板与场景视图工具。 +- `app/Rendering`:编辑器 viewport、icon、object-id picking、grid/outline/helper pass 相关服务。渲染执行仍走 engine `Rendering + RHI`。 +- `app/Host`:宿主接口实现。Win32/D3D12 细节只能待在这里或 rendering host 实现里。 +- `app/Windowing`:窗口实例、内容控制器、生命周期协调器、workspace 多窗口同步、截图和 frame orchestration。 +- `resources/Icons`、`resources/shaders/scene-viewport`:内置图标和 scene viewport shader。资源路径由 app/rendering 层解析。 + +当前启动和帧流程: + +```text +wWinMain +-> RunXCEditor +-> Application::Initialize / Run +-> EditorContext::Initialize +-> BuildEditorApplicationShellAsset +-> EditorWindowSystem::BootstrapPrimaryWindow +-> EditorWindowManager::CreateWorkspaceWindow +-> EditorWindowRuntimeController +-> EditorWorkspaceWindowContentController +-> EditorShellRuntime::Update / Append / RenderRequestedViewports +``` + +- `Application::Run()` pump Win32 message,更新 `ResourceManager::UpdateAsyncLoads()`,然后让 `EditorWindowManager::RenderAllWindows()` 驱动所有窗口。 +- `EditorWindowRuntimeController::BeginFrame()` 从 D3D12 runtime 取 `RenderContext`,content controller 先更新 shell 与 hosted panels,再 present UI draw data。 +- `EditorShellInteractionEngine::Update()` 先 `BeginFrame()` 清空 viewport 请求,再运行 `UpdateUIEditorShellInteraction()`,最后把每个 viewport shell 的尺寸提交给 `EditorViewportRuntimeServices::RequestViewport()`。 +- `EditorShellRuntime::RenderRequestedViewports()` 在 UI shell 更新后调用 viewport runtime,把 scene viewport 渲染进离屏纹理,再由 shell frame 展示。 + +当前 Scene Viewport 分层: + +- `src/Viewport` 和 `include/XCEditor/Viewport`:viewport slot/shell/input bridge,只处理 UI 容器和输入桥。 +- `app/Features/Scene/SceneViewportController.*`:场景视图产品交互,处理 Q/W/E/R 工具切换、F 聚焦、鼠标导航、scene icon picking、transform gizmo。 +- `app/Core/Scene/EditorSceneRuntime.*`:scene selection、editor camera、transform undo/redo、component mutation 和 scene render request 的事实来源。 +- `app/Rendering/Viewport/ViewportHostService.*`:离屏 viewport 资源管理和 renderer 注册。 +- `app/Rendering/Viewport/SceneViewportRenderService.*`:调用 engine `SceneRenderer`,插入 grid、selection outline、selected helpers、object-id 等 editor pass。 +- object-id picking 依赖有效 object-id surface 和 frame serial,修改时要覆盖 `test_viewport_object_id_picker.cpp` 及相关 viewport render plan 测试。 + +当前构建和验证入口: + +```powershell +cmake --build build --config Debug --target XCUIEditor +cmake --build build --config Debug --target XCEditorCore +cmake --build build --config Debug --target XCEditor +cmake --build build --config Debug --target editor_ui_tests +cmake --build build --config Debug --target editor_app_core_tests +cmake --build build --config Debug --target editor_app_feature_tests +cmake --build build --config Debug --target editor_windowing_phase1_tests +cmake --build build --config Debug --target editor_ui_smoke_targets +cmake --build build --config Debug --target editor_ui_manual_validation_scenarios +``` + +```powershell +ctest --test-dir build -C Debug -R "editor|xceditor" --output-on-failure +``` + +- `XCUIEditor` widget/shell/workspace 改动:跑 `editor_ui_tests`、`editor_windowing_phase1_tests`。 +- `app/Core`、project/scene/session/command 改动:跑 `editor_app_core_tests`。 +- scene viewport、project panel、window input routing 改动:跑 `editor_app_feature_tests`。 +- Win32/D3D12 host 或启动流程改动:跑 `editor_ui_smoke_targets`,必要时跑 `xceditor_smoke`。 +- 手动 UI 场景:跑 `editor_ui_manual_validation_scenarios`。 + +有用环境变量: + +- `XCUIEDITOR_SMOKE_TEST=1`:启用应用自退出 smoke 模式。 +- `XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT=`:smoke 模式帧数上限。 +- `XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS=`:smoke 模式持续时间。 +- `XCUI_AUTO_CAPTURE_ON_STARTUP=1`:启动后自动截图。 + +## 过去执行 + +- 已把 editor 产品装配的 panel 声明收敛到 `app/Core/Product/EditorProductManifest.*`,并让 panel registry、View > Panels 菜单、panel 激活命令、action route、workspace runtime set 和 viewport renderer 注册从 manifest 派生。 +- 已把 `game` panel 明确标成 placeholder viewport,而不是隐式共享 scene renderer 或假装 Game runtime 已完成。 +- 已新增 manifest validation 测试,确保产品 manifest 能声明 panel runtime owner 和 viewport renderer owner,并覆盖 `game` placeholder 的预期状态。 +- 已更新根 `AGENT.md` 和本文件,去掉旧 `--project` 说法,记录当前 XCUI editor、`XCEditorCore` 分层和 product manifest 规则。 +- 本次改动验证过: + - `cmake --build build --config Debug --target XCEditor` + - `cmake --build build --config Debug --target editor_app_core_tests` + - `.\build\tests\UI\Editor\unit\Debug\editor_app_core_tests.exe --gtest_filter=EditorShellAssetValidationTest.*` + - `cmake --build build --config Debug --target editor_app_feature_tests` + - `.\build\tests\UI\Editor\unit\Debug\editor_app_feature_tests.exe` diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index eb3c4cd1..79865dc6 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -215,6 +215,7 @@ set(XCUI_EDITOR_HOST_RENDERING_SOURCES if(XCENGINE_BUILD_XCUI_EDITOR_CORE) set(XCUI_EDITOR_APP_CORE_CONTRACT_SOURCES + app/Core/Product/EditorProductManifest.cpp app/Core/UtilityWindows/EditorUtilityWindowRegistry.cpp app/Core/WorkspacePanels/EditorWorkspacePanelRuntime.cpp ) diff --git a/editor/app/Composition/EditorShellAssetBuilder.cpp b/editor/app/Composition/EditorShellAssetBuilder.cpp index 93e4ddab..d017a796 100644 --- a/editor/app/Composition/EditorShellAssetBuilder.cpp +++ b/editor/app/Composition/EditorShellAssetBuilder.cpp @@ -5,6 +5,7 @@ #include #include #include "Panels/EditorPanelIds.h" +#include "Product/EditorProductManifest.h" #include #include "Assets/EditorIconService.h" @@ -134,6 +135,10 @@ UIEditorCommandDescriptor BuildWorkspaceCommand( return command; } +std::string BuildPanelActivationCommandId(std::string_view panelId) { + return std::string("view.activate_") + std::string(panelId); +} + UIShortcutBinding BuildBinding( std::string commandId, std::int32_t keyCode, @@ -187,38 +192,16 @@ UIEditorCommandRegistry BuildEditorCommandRegistry() { BuildWorkspaceCommand( "view.reset_layout", "Reset Layout", - UIEditorWorkspaceCommandKind::ResetWorkspace), - BuildWorkspaceCommand( - "view.activate_hierarchy", - "Hierarchy", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kHierarchyPanelId)), - BuildWorkspaceCommand( - "view.activate_scene", - "Scene", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kScenePanelId)), - BuildWorkspaceCommand( - "view.activate_game", - "Game", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kGamePanelId)), - BuildWorkspaceCommand( - "view.activate_inspector", - "Inspector", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kInspectorPanelId)), - BuildWorkspaceCommand( - "view.activate_console", - "Console", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kConsolePanelId)), - BuildWorkspaceCommand( - "view.activate_project", - "Project", - UIEditorWorkspaceCommandKind::ActivatePanel, - std::string(kProjectPanelId)) + UIEditorWorkspaceCommandKind::ResetWorkspace) }; + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + registry.commands.push_back( + BuildWorkspaceCommand( + BuildPanelActivationCommandId(panel.panelId), + std::string(panel.defaultTitle), + UIEditorWorkspaceCommandKind::ActivatePanel, + std::string(panel.panelId))); + } return registry; } @@ -250,39 +233,22 @@ namespace XCEngine::UI::Editor::App { namespace { -UIEditorPanelDescriptor BuildHostedContentPanelDescriptor( - std::string_view panelId, - std::string_view title, - bool placeholder, - bool canHide, - bool canClose) { +UIEditorPanelDescriptor BuildProductPanelDescriptor( + const EditorProductPanelDescriptor& productPanel) { 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; + descriptor.panelId = std::string(productPanel.panelId); + descriptor.defaultTitle = std::string(productPanel.defaultTitle); + descriptor.presentationKind = productPanel.presentationKind; + descriptor.placeholder = productPanel.placeholder; + descriptor.canHide = productPanel.canHide; + descriptor.canClose = productPanel.canClose; + if (descriptor.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + descriptor.viewportShellSpec.chrome.title = descriptor.defaultTitle; + descriptor.viewportShellSpec.chrome.showTopBar = + productPanel.showViewportTopBar; + descriptor.viewportShellSpec.chrome.showBottomBar = + productPanel.showViewportBottomBar; + } return descriptor; } @@ -303,14 +269,9 @@ const UIEditorPanelDescriptor& RequirePanelDescriptor( UIEditorPanelRegistry BuildEditorPanelRegistry() { UIEditorPanelRegistry registry = {}; - registry.panels = { - 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) - }; + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + registry.panels.push_back(BuildProductPanelDescriptor(panel)); + } return registry; } @@ -498,30 +459,20 @@ UIEditorMenuModel BuildEditorMenuModel() { BuildCommandItem("scripts-rebuild", "Rebuild Script Assemblies", "scripts.rebuild") }; - UIEditorMenuCheckedStateBinding hierarchyActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kHierarchyPanelId) - }; - UIEditorMenuCheckedStateBinding sceneActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kScenePanelId) - }; - UIEditorMenuCheckedStateBinding gameActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kGamePanelId) - }; - UIEditorMenuCheckedStateBinding inspectorActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kInspectorPanelId) - }; - UIEditorMenuCheckedStateBinding consoleActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kConsolePanelId) - }; - UIEditorMenuCheckedStateBinding projectActive = { - UIEditorMenuCheckedStateSource::PanelActive, - std::string(kProjectPanelId) - }; + std::vector panelMenuItems = {}; + panelMenuItems.reserve(GetEditorProductPanels().size()); + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + UIEditorMenuCheckedStateBinding activeBinding = { + UIEditorMenuCheckedStateSource::PanelActive, + std::string(panel.panelId) + }; + panelMenuItems.push_back( + BuildCommandItem( + std::string("view-panel-") + std::string(panel.panelId), + std::string(panel.defaultTitle), + BuildPanelActivationCommandId(panel.panelId), + std::move(activeBinding))); + } UIEditorMenuDescriptor viewMenu = {}; viewMenu.menuId = "view"; @@ -532,14 +483,7 @@ UIEditorMenuModel BuildEditorMenuModel() { BuildSubmenuItem( "view-panels", "Panels", - { - BuildCommandItem("view-panel-hierarchy", "Hierarchy", "view.activate_hierarchy", hierarchyActive), - BuildCommandItem("view-panel-scene", "Scene", "view.activate_scene", sceneActive), - BuildCommandItem("view-panel-game", "Game", "view.activate_game", gameActive), - BuildCommandItem("view-panel-inspector", "Inspector", "view.activate_inspector", inspectorActive), - BuildCommandItem("view-panel-console", "Console", "view.activate_console", consoleActive), - BuildCommandItem("view-panel-project", "Project", "view.activate_project", projectActive) - }) + std::move(panelMenuItems)) }; UIEditorMenuDescriptor helpMenu = {}; diff --git a/editor/app/Core/Panels/EditorPanelIds.h b/editor/app/Core/Panels/EditorPanelIds.h index ca608769..a10f4bb6 100644 --- a/editor/app/Core/Panels/EditorPanelIds.h +++ b/editor/app/Core/Panels/EditorPanelIds.h @@ -18,8 +18,6 @@ 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; -} +[[nodiscard]] bool IsEditorViewportPanelId(std::string_view panelId); } // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/Product/EditorProductManifest.cpp b/editor/app/Core/Product/EditorProductManifest.cpp new file mode 100644 index 00000000..d5f8179e --- /dev/null +++ b/editor/app/Core/Product/EditorProductManifest.cpp @@ -0,0 +1,201 @@ +#include "Product/EditorProductManifest.h" + +#include "Panels/EditorPanelIds.h" + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +constexpr std::array kEditorProductPanels = { + EditorProductPanelDescriptor{ + kHierarchyPanelId, + kHierarchyPanelTitle, + UIEditorPanelPresentationKind::HostedContent, + true, + false, + false, + false, + false, + EditorActionRoute::Hierarchy, + EditorProductPanelRuntimeKind::Hierarchy, + EditorProductViewportRendererKind::None, + {} }, + EditorProductPanelDescriptor{ + kScenePanelId, + kScenePanelTitle, + UIEditorPanelPresentationKind::ViewportShell, + false, + false, + false, + false, + false, + EditorActionRoute::Scene, + EditorProductPanelRuntimeKind::Scene, + EditorProductViewportRendererKind::Scene, + {} }, + EditorProductPanelDescriptor{ + kGamePanelId, + kGamePanelTitle, + UIEditorPanelPresentationKind::ViewportShell, + false, + false, + false, + false, + false, + EditorActionRoute::Game, + EditorProductPanelRuntimeKind::None, + EditorProductViewportRendererKind::Placeholder, + "Game view runtime is not implemented" }, + EditorProductPanelDescriptor{ + kInspectorPanelId, + kInspectorPanelTitle, + UIEditorPanelPresentationKind::HostedContent, + true, + false, + false, + false, + false, + EditorActionRoute::Inspector, + EditorProductPanelRuntimeKind::Inspector, + EditorProductViewportRendererKind::None, + {} }, + EditorProductPanelDescriptor{ + kConsolePanelId, + kConsolePanelTitle, + UIEditorPanelPresentationKind::HostedContent, + true, + false, + false, + false, + false, + EditorActionRoute::Console, + EditorProductPanelRuntimeKind::Console, + EditorProductViewportRendererKind::None, + {} }, + EditorProductPanelDescriptor{ + kProjectPanelId, + kProjectPanelTitle, + UIEditorPanelPresentationKind::HostedContent, + false, + false, + false, + false, + false, + EditorActionRoute::Project, + EditorProductPanelRuntimeKind::Project, + EditorProductViewportRendererKind::None, + {} }, +}; + +EditorProductManifestValidationResult BuildManifestError( + EditorProductManifestValidationCode code, + std::string_view message) { + EditorProductManifestValidationResult result = {}; + result.code = code; + result.message = std::string(message); + return result; +} + +} // namespace + +std::span GetEditorProductPanels() { + return kEditorProductPanels; +} + +const EditorProductPanelDescriptor* FindEditorProductPanel( + std::string_view panelId) { + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + if (panel.panelId == panelId) { + return &panel; + } + } + + return nullptr; +} + +bool IsEditorProductViewportPanel(std::string_view panelId) { + const EditorProductPanelDescriptor* panel = FindEditorProductPanel(panelId); + return panel != nullptr && + panel->presentationKind == UIEditorPanelPresentationKind::ViewportShell; +} + +EditorProductManifestValidationResult ValidateEditorProductManifest() { + const std::span panels = + GetEditorProductPanels(); + for (std::size_t index = 0u; index < panels.size(); ++index) { + const EditorProductPanelDescriptor& panel = panels[index]; + if (panel.panelId.empty()) { + return BuildManifestError( + EditorProductManifestValidationCode::EmptyPanelId, + "Editor product manifest contains a panel with an empty id."); + } + if (panel.defaultTitle.empty()) { + return BuildManifestError( + EditorProductManifestValidationCode::EmptyPanelTitle, + "Editor product manifest contains a panel with an empty title."); + } + + for (std::size_t other = index + 1u; other < panels.size(); ++other) { + if (panels[other].panelId == panel.panelId) { + std::ostringstream message = {}; + message << "Editor product manifest duplicates panel id '" + << panel.panelId << "'."; + return BuildManifestError( + EditorProductManifestValidationCode::DuplicatePanelId, + message.str()); + } + } + + if (panel.presentationKind == UIEditorPanelPresentationKind::HostedContent && + panel.runtimeKind == EditorProductPanelRuntimeKind::None) { + std::ostringstream message = {}; + message << "Hosted panel '" << panel.panelId + << "' has no runtime owner."; + return BuildManifestError( + EditorProductManifestValidationCode::HostedPanelMissingRuntime, + message.str()); + } + + if (panel.presentationKind == UIEditorPanelPresentationKind::ViewportShell && + panel.viewportRendererKind == EditorProductViewportRendererKind::None) { + std::ostringstream message = {}; + message << "Viewport panel '" << panel.panelId + << "' has no viewport renderer owner."; + return BuildManifestError( + EditorProductManifestValidationCode::ViewportPanelMissingRenderer, + message.str()); + } + + if (panel.presentationKind != UIEditorPanelPresentationKind::ViewportShell && + panel.viewportRendererKind != EditorProductViewportRendererKind::None) { + std::ostringstream message = {}; + message << "Non-viewport panel '" << panel.panelId + << "' declares a viewport renderer."; + return BuildManifestError( + EditorProductManifestValidationCode::NonViewportPanelHasRenderer, + message.str()); + } + + if (panel.viewportRendererKind == + EditorProductViewportRendererKind::Placeholder && + panel.viewportPlaceholderStatus.empty()) { + std::ostringstream message = {}; + message << "Placeholder viewport panel '" << panel.panelId + << "' has no status text."; + return BuildManifestError( + EditorProductManifestValidationCode::PlaceholderViewportMissingStatus, + message.str()); + } + } + + return {}; +} + +bool IsEditorViewportPanelId(std::string_view panelId) { + return IsEditorProductViewportPanel(panelId); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/Product/EditorProductManifest.h b/editor/app/Core/Product/EditorProductManifest.h new file mode 100644 index 00000000..8964318c --- /dev/null +++ b/editor/app/Core/Product/EditorProductManifest.h @@ -0,0 +1,77 @@ +#pragma once + +#include "State/EditorSession.h" + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +enum class EditorProductPanelRuntimeKind : std::uint8_t { + None = 0, + Console, + Hierarchy, + Inspector, + Project, + Scene +}; + +enum class EditorProductViewportRendererKind : std::uint8_t { + None = 0, + Scene, + Placeholder +}; + +struct EditorProductPanelDescriptor { + std::string_view panelId = {}; + std::string_view defaultTitle = {}; + UIEditorPanelPresentationKind presentationKind = + UIEditorPanelPresentationKind::Placeholder; + bool placeholder = true; + bool canHide = true; + bool canClose = true; + bool showViewportTopBar = false; + bool showViewportBottomBar = false; + EditorActionRoute actionRoute = EditorActionRoute::None; + EditorProductPanelRuntimeKind runtimeKind = + EditorProductPanelRuntimeKind::None; + EditorProductViewportRendererKind viewportRendererKind = + EditorProductViewportRendererKind::None; + std::string_view viewportPlaceholderStatus = {}; +}; + +enum class EditorProductManifestValidationCode : std::uint8_t { + None = 0, + EmptyPanelId, + EmptyPanelTitle, + DuplicatePanelId, + HostedPanelMissingRuntime, + ViewportPanelMissingRenderer, + NonViewportPanelHasRenderer, + PlaceholderViewportMissingStatus +}; + +struct EditorProductManifestValidationResult { + EditorProductManifestValidationCode code = + EditorProductManifestValidationCode::None; + std::string message = {}; + + [[nodiscard]] bool IsValid() const { + return code == EditorProductManifestValidationCode::None; + } +}; + +std::span GetEditorProductPanels(); + +const EditorProductPanelDescriptor* FindEditorProductPanel( + std::string_view panelId); + +bool IsEditorProductViewportPanel(std::string_view panelId); + +EditorProductManifestValidationResult ValidateEditorProductManifest(); + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Core/State/EditorSession.cpp b/editor/app/Core/State/EditorSession.cpp index ac2f80a1..83c4ea9f 100644 --- a/editor/app/Core/State/EditorSession.cpp +++ b/editor/app/Core/State/EditorSession.cpp @@ -1,6 +1,6 @@ #include "State/EditorSession.h" -#include "Panels/EditorPanelIds.h" +#include "Product/EditorProductManifest.h" #include @@ -52,23 +52,10 @@ std::string_view GetEditorSelectionKindName(EditorSelectionKind kind) { } EditorActionRoute ResolveEditorActionRoute(std::string_view panelId) { - if (panelId == kHierarchyPanelId) { - return EditorActionRoute::Hierarchy; - } - if (panelId == kProjectPanelId) { - return EditorActionRoute::Project; - } - if (panelId == kInspectorPanelId) { - return EditorActionRoute::Inspector; - } - if (panelId == kConsolePanelId) { - return EditorActionRoute::Console; - } - if (panelId == kScenePanelId) { - return EditorActionRoute::Scene; - } - if (panelId == kGamePanelId) { - return EditorActionRoute::Game; + if (const EditorProductPanelDescriptor* panel = + FindEditorProductPanel(panelId); + panel != nullptr) { + return panel->actionRoute; } return EditorActionRoute::None; } diff --git a/editor/app/Features/EditorWorkspacePanelRegistry.cpp b/editor/app/Features/EditorWorkspacePanelRegistry.cpp index 3137cdcb..1960929d 100644 --- a/editor/app/Features/EditorWorkspacePanelRegistry.cpp +++ b/editor/app/Features/EditorWorkspacePanelRegistry.cpp @@ -7,6 +7,7 @@ #include "Project/ProjectPanel.h" #include "Scene/SceneEditCommandRoute.h" #include "Scene/SceneViewportFeature.h" +#include "Product/EditorProductManifest.h" #include @@ -449,15 +450,36 @@ private: SceneEditCommandRoute m_commandRoute = {}; }; +std::unique_ptr CreateWorkspacePanelRuntime( + const EditorProductPanelDescriptor& panel) { + switch (panel.runtimeKind) { + case EditorProductPanelRuntimeKind::Console: + return std::make_unique(); + case EditorProductPanelRuntimeKind::Hierarchy: + return std::make_unique(); + case EditorProductPanelRuntimeKind::Inspector: + return std::make_unique(); + case EditorProductPanelRuntimeKind::Project: + return std::make_unique(); + case EditorProductPanelRuntimeKind::Scene: + return std::make_unique(); + case EditorProductPanelRuntimeKind::None: + default: + return nullptr; + } +} + } // namespace EditorWorkspacePanelRuntimeSet CreateEditorWorkspacePanelRuntimeSet() { EditorWorkspacePanelRuntimeSet panels = {}; - panels.AddPanel(std::make_unique()); - panels.AddPanel(std::make_unique()); - panels.AddPanel(std::make_unique()); - panels.AddPanel(std::make_unique()); - panels.AddPanel(std::make_unique()); + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + if (std::unique_ptr runtime = + CreateWorkspacePanelRuntime(panel); + runtime != nullptr) { + panels.AddPanel(std::move(runtime)); + } + } return panels; } diff --git a/editor/app/Rendering/Viewport/ViewportHostService.cpp b/editor/app/Rendering/Viewport/ViewportHostService.cpp index 3d9fcfbf..07ecbd4b 100644 --- a/editor/app/Rendering/Viewport/ViewportHostService.cpp +++ b/editor/app/Rendering/Viewport/ViewportHostService.cpp @@ -1,6 +1,7 @@ #include "ViewportHostService.h" #include "Panels/EditorPanelIds.h" +#include "Product/EditorProductManifest.h" #include "Viewport/SceneViewportResourcePaths.h" #include "ViewportRenderHost.h" @@ -14,6 +15,30 @@ namespace { using ::XCEngine::RHI::ResourceStates; +class PlaceholderViewportContentRenderer final : public IViewportContentRenderer { +public: + explicit PlaceholderViewportContentRenderer(std::string_view statusText) + : m_statusText(statusText) {} + + ViewportRenderResult Render( + ViewportRenderTargets&, + ::XCEngine::RHI::RHIDevice&, + const ::XCEngine::Rendering::RenderContext&) override { + ViewportRenderResult result = {}; + result.rendered = false; + result.requiresFallbackClear = true; + result.statusText = m_statusText; + result.fallbackClearR = 0.07f; + result.fallbackClearG = 0.08f; + result.fallbackClearB = 0.10f; + result.fallbackClearA = 1.0f; + return result; + } + +private: + std::string m_statusText = {}; +}; + } // namespace ViewportHostService::ViewportHostService() = default; @@ -22,10 +47,33 @@ ViewportHostService::~ViewportHostService() = default; void ViewportHostService::Initialize(const std::filesystem::path& repoRoot) { m_sceneViewportRuntime.Initialize(BuildSceneViewportShaderPaths(repoRoot)); - SetContentRenderer( - kScenePanelId, - &m_sceneViewportRuntime, - SceneViewportRenderService::GetViewportResourceRequirements()); + m_placeholderRenderers.clear(); + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + if (panel.presentationKind != UIEditorPanelPresentationKind::ViewportShell) { + continue; + } + + switch (panel.viewportRendererKind) { + case EditorProductViewportRendererKind::Scene: + SetContentRenderer( + panel.panelId, + &m_sceneViewportRuntime, + SceneViewportRenderService::GetViewportResourceRequirements()); + break; + case EditorProductViewportRendererKind::Placeholder: { + auto placeholder = + std::make_unique( + panel.viewportPlaceholderStatus); + SetContentRenderer(panel.panelId, placeholder.get(), {}); + m_placeholderRenderers.push_back(std::move(placeholder)); + break; + } + case EditorProductViewportRendererKind::None: + default: + SetContentRenderer(panel.panelId, nullptr, {}); + break; + } + } } void ViewportHostService::AttachWindowRenderer( @@ -60,7 +108,12 @@ void ViewportHostService::SetContentRenderer( } void ViewportHostService::Shutdown() { - SetContentRenderer(kScenePanelId, nullptr, {}); + for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) { + if (panel.presentationKind == UIEditorPanelPresentationKind::ViewportShell) { + SetContentRenderer(panel.panelId, nullptr, {}); + } + } + m_placeholderRenderers.clear(); m_sceneViewportRuntime.Shutdown(); for (auto& [viewportId, entry] : m_entries) { DestroyViewportEntry(entry); diff --git a/editor/app/Rendering/Viewport/ViewportHostService.h b/editor/app/Rendering/Viewport/ViewportHostService.h index c4f976fe..6197dd72 100644 --- a/editor/app/Rendering/Viewport/ViewportHostService.h +++ b/editor/app/Rendering/Viewport/ViewportHostService.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -82,6 +83,7 @@ private: bool m_surfacePresentationEnabled = false; std::unordered_map m_entries = {}; std::vector> m_retiredTargetsBySlot = {}; + std::vector> m_placeholderRenderers = {}; SceneViewportRenderService m_sceneViewportRuntime = {}; }; 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 d88d4416..9852f9eb 100644 --- a/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp +++ b/tests/UI/Editor/unit/test_editor_shell_asset_validation.cpp @@ -1,6 +1,8 @@ #include #include "EditorShellAssetBuilder.h" +#include "Panels/EditorPanelIds.h" +#include "Product/EditorProductManifest.h" #include @@ -10,6 +12,13 @@ namespace { using XCEngine::Input::KeyCode; using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset; +using XCEngine::UI::Editor::App::EditorProductManifestValidationCode; +using XCEngine::UI::Editor::App::EditorProductPanelRuntimeKind; +using XCEngine::UI::Editor::App::EditorProductViewportRendererKind; +using XCEngine::UI::Editor::App::FindEditorProductPanel; +using XCEngine::UI::Editor::App::GetEditorProductPanels; +using XCEngine::UI::Editor::App::kGamePanelId; +using XCEngine::UI::Editor::App::ValidateEditorProductManifest; using XCEngine::UI::Editor::EditorShellAssetValidationCode; using XCEngine::UI::Editor::FindUIEditorPanelDescriptor; using XCEngine::UI::Editor::UIEditorCommandPanelSource; @@ -64,6 +73,24 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) { shellAsset.panelRegistry.panels.front().presentationKind); } +TEST(EditorShellAssetValidationTest, ProductManifestDeclaresPanelRuntimeAndViewportOwners) { + const auto validation = ValidateEditorProductManifest(); + EXPECT_EQ(validation.code, EditorProductManifestValidationCode::None) + << validation.message; + ASSERT_TRUE(validation.IsValid()); + + const auto shellAsset = BuildEditorApplicationShellAsset("."); + ASSERT_EQ(shellAsset.panelRegistry.panels.size(), GetEditorProductPanels().size()); + + const auto* gamePanel = FindEditorProductPanel(kGamePanelId); + ASSERT_NE(gamePanel, nullptr); + EXPECT_EQ(gamePanel->runtimeKind, EditorProductPanelRuntimeKind::None); + EXPECT_EQ( + gamePanel->viewportRendererKind, + EditorProductViewportRendererKind::Placeholder); + EXPECT_FALSE(gamePanel->viewportPlaceholderStatus.empty()); +} + TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) { auto shellAsset = BuildEditorApplicationShellAsset(".");