editor: centralize product panel manifest

This commit is contained in:
2026-04-28 04:33:44 +08:00
parent ea765b9481
commit 8c66cdac07
12 changed files with 574 additions and 136 deletions

View File

@@ -61,7 +61,7 @@
- `engine/` 构建静态库 `XCEngine``editor/` 构建 `XCUIEditor` 静态库和 `XCEditor` 可执行目标。
- `editor/` 目前继续保留为当前正式编辑器、行为对照和视觉基线来源。
- 启用 `XCENGINE_BUILD_XCUI_EDITOR_APP` 时,`XCEditor` 输出 `build/editor/<Config>/XCEngine.exe`
- editor 默认把仓库内的 `project/` 识别为工程根目录,也支持 `--project <path>` 覆盖
- 当前 XCUI editor 把仓库内的 `project/` 固定识别为工程根目录;不要再沿用旧的 `--project <path>` 说法
- 当前工程真实使用 `Assets/ + .meta + Library/` 的项目布局;`project/Library/` 是当前 workflow 的一部分,不是可随手忽略的垃圾目录。
- Mono 运行时与 editor 的脚本类发现都从 `<project>/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

123
editor/AGENTS.md Normal file
View File

@@ -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` 将项目根固定为 `<repoRoot>/project`,不要沿用旧文档里的 `--project <path>` 说法。
- 当前面板 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=<n>`smoke 模式帧数上限。
- `XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS=<n>`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`

View File

@@ -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
)

View File

@@ -5,6 +5,7 @@
#include <vector>
#include <utility>
#include "Panels/EditorPanelIds.h"
#include "Product/EditorProductManifest.h"
#include <XCEngine/Input/InputTypes.h>
#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<UIEditorMenuItemDescriptor> 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 = {};

View File

@@ -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

View File

@@ -0,0 +1,201 @@
#include "Product/EditorProductManifest.h"
#include "Panels/EditorPanelIds.h"
#include <array>
#include <sstream>
namespace XCEngine::UI::Editor::App {
namespace {
constexpr std::array<EditorProductPanelDescriptor, 6> 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<const EditorProductPanelDescriptor> 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<const EditorProductPanelDescriptor> 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

View File

@@ -0,0 +1,77 @@
#pragma once
#include "State/EditorSession.h"
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
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<const EditorProductPanelDescriptor> GetEditorProductPanels();
const EditorProductPanelDescriptor* FindEditorProductPanel(
std::string_view panelId);
bool IsEditorProductViewportPanel(std::string_view panelId);
EditorProductManifestValidationResult ValidateEditorProductManifest();
} // namespace XCEngine::UI::Editor::App

View File

@@ -1,6 +1,6 @@
#include "State/EditorSession.h"
#include "Panels/EditorPanelIds.h"
#include "Product/EditorProductManifest.h"
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
@@ -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;
}

View File

@@ -7,6 +7,7 @@
#include "Project/ProjectPanel.h"
#include "Scene/SceneEditCommandRoute.h"
#include "Scene/SceneViewportFeature.h"
#include "Product/EditorProductManifest.h"
#include <XCEditor/Panels/UIEditorHostedPanelDispatch.h>
@@ -449,15 +450,36 @@ private:
SceneEditCommandRoute m_commandRoute = {};
};
std::unique_ptr<EditorWorkspacePanel> CreateWorkspacePanelRuntime(
const EditorProductPanelDescriptor& panel) {
switch (panel.runtimeKind) {
case EditorProductPanelRuntimeKind::Console:
return std::make_unique<ConsoleWorkspacePanel>();
case EditorProductPanelRuntimeKind::Hierarchy:
return std::make_unique<HierarchyWorkspacePanel>();
case EditorProductPanelRuntimeKind::Inspector:
return std::make_unique<InspectorWorkspacePanel>();
case EditorProductPanelRuntimeKind::Project:
return std::make_unique<ProjectWorkspacePanel>();
case EditorProductPanelRuntimeKind::Scene:
return std::make_unique<SceneWorkspacePanel>();
case EditorProductPanelRuntimeKind::None:
default:
return nullptr;
}
}
} // namespace
EditorWorkspacePanelRuntimeSet CreateEditorWorkspacePanelRuntimeSet() {
EditorWorkspacePanelRuntimeSet panels = {};
panels.AddPanel(std::make_unique<ConsoleWorkspacePanel>());
panels.AddPanel(std::make_unique<HierarchyWorkspacePanel>());
panels.AddPanel(std::make_unique<InspectorWorkspacePanel>());
panels.AddPanel(std::make_unique<ProjectWorkspacePanel>());
panels.AddPanel(std::make_unique<SceneWorkspacePanel>());
for (const EditorProductPanelDescriptor& panel : GetEditorProductPanels()) {
if (std::unique_ptr<EditorWorkspacePanel> runtime =
CreateWorkspacePanelRuntime(panel);
runtime != nullptr) {
panels.AddPanel(std::move(runtime));
}
}
return panels;
}

View File

@@ -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<PlaceholderViewportContentRenderer>(
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);

View File

@@ -10,6 +10,7 @@
#include <XCEngine/UI/Types.h>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
@@ -82,6 +83,7 @@ private:
bool m_surfacePresentationEnabled = false;
std::unordered_map<std::string, ViewportEntry> m_entries = {};
std::vector<std::vector<ViewportRenderTargets>> m_retiredTargetsBySlot = {};
std::vector<std::unique_ptr<IViewportContentRenderer>> m_placeholderRenderers = {};
SceneViewportRenderService m_sceneViewportRuntime = {};
};

View File

@@ -1,6 +1,8 @@
#include <gtest/gtest.h>
#include "EditorShellAssetBuilder.h"
#include "Panels/EditorPanelIds.h"
#include "Product/EditorProductManifest.h"
#include <XCEditor/Shell/UIEditorShellAsset.h>
@@ -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(".");