From f45b34a03a7c1dce55dd75ad09b06d7290685b0d Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 19 Apr 2026 04:36:52 +0800 Subject: [PATCH] Refactor new editor state ownership model --- ...tor_UI第二阶段依赖方向子计划_2026-04-19.md | 227 ++++++++++ ...核心层与首方渲染管线拆层计划_2026-04-19.md | 420 ++++++++++++++++++ ...itor_UI第一阶段边界收口子计划_阶段归档_2026-04-19.md} | 0 ...四方向状态模型子计划_阶段归档_2026-04-19.md | 132 ++++++ .../SRP_Mainline_过期归档_2026-04-19.md} | 0 .../SRP_Runtime_v2_过期归档_2026-04-19.md} | 0 new_editor/CMakeLists.txt | 1 + .../app/Commands/EditorHostCommandBridge.h | 4 + .../EditorShellPointerInteraction.h | 27 ++ .../app/Composition/EditorShellRuntime.cpp | 62 ++- .../app/Composition/EditorShellRuntime.h | 2 + .../Composition/EditorShellRuntimeUpdate.cpp | 5 + .../app/Composition/WorkspaceEventSync.cpp | 3 +- .../app/Features/Hierarchy/HierarchyPanel.cpp | 39 ++ .../app/Features/Hierarchy/HierarchyPanel.h | 7 + .../app/Features/Inspector/InspectorPanel.cpp | 38 ++ .../app/Features/Inspector/InspectorPanel.h | 6 + .../Features/Inspector/InspectorSubject.cpp | 36 +- .../app/Features/Project/ProjectPanel.cpp | 32 ++ .../app/Features/Project/ProjectPanel.h | 7 + .../Scene/SceneViewportController.cpp | 18 + .../Features/Scene/SceneViewportController.h | 3 + new_editor/app/Platform/Win32/EditorWindow.h | 5 +- .../app/Platform/Win32/EditorWindowFrame.cpp | 35 +- .../Platform/Win32/EditorWindowLifecycle.cpp | 2 +- .../EditorWindowWorkspaceStore.cpp | 169 +++++++ .../EditorWindowWorkspaceStore.h | 53 +++ .../Platform/Win32/WindowManager/Internal.h | 14 +- .../Win32/WindowManager/Lifecycle.cpp | 105 +++-- .../Platform/Win32/WindowManager/TabDrag.cpp | 12 +- .../Win32/WindowManager/WindowSync.cpp | 22 +- .../app/Project/EditorProjectRuntime.cpp | 74 +-- new_editor/app/Project/EditorProjectRuntime.h | 11 +- new_editor/app/Scene/EditorSceneRuntime.cpp | 84 +++- new_editor/app/Scene/EditorSceneRuntime.h | 10 +- .../app/State/EditorCommandFocusService.h | 42 ++ new_editor/app/State/EditorContext.cpp | 34 +- new_editor/app/State/EditorContext.h | 9 +- new_editor/app/State/EditorSelectionService.h | 111 +++++ .../src/App/EditorHostCommandBridge.cpp | 35 +- new_editor/src/App/EditorSession.cpp | 1 - tests/UI/Editor/unit/CMakeLists.txt | 1 + .../unit/test_editor_host_command_bridge.cpp | 63 ++- .../unit/test_editor_project_runtime.cpp | 22 + .../test_editor_window_workspace_store.cpp | 160 +++++++ .../unit/test_scene_viewport_runtime.cpp | 53 ++- 46 files changed, 1979 insertions(+), 217 deletions(-) create mode 100644 docs/plan/NewEditor_UI第二阶段依赖方向子计划_2026-04-19.md create mode 100644 docs/plan/SRP_核心层与首方渲染管线拆层计划_2026-04-19.md rename docs/{plan/NewEditor_UI第一阶段边界收口子计划_2026-04-19.md => used/NewEditor_UI第一阶段边界收口子计划_阶段归档_2026-04-19.md} (100%) create mode 100644 docs/used/NewEditor_UI第四方向状态模型子计划_阶段归档_2026-04-19.md rename docs/{plan/SRP_Mainline_2026-04-16.md => used/SRP_Mainline_过期归档_2026-04-19.md} (100%) rename docs/{plan/SRP_Runtime_v2_2026-04-18.md => used/SRP_Runtime_v2_过期归档_2026-04-19.md} (100%) create mode 100644 new_editor/app/Composition/EditorShellPointerInteraction.h create mode 100644 new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.cpp create mode 100644 new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h create mode 100644 new_editor/app/State/EditorCommandFocusService.h create mode 100644 new_editor/app/State/EditorSelectionService.h create mode 100644 tests/UI/Editor/unit/test_editor_window_workspace_store.cpp diff --git a/docs/plan/NewEditor_UI第二阶段依赖方向子计划_2026-04-19.md b/docs/plan/NewEditor_UI第二阶段依赖方向子计划_2026-04-19.md new file mode 100644 index 00000000..e9eaec84 --- /dev/null +++ b/docs/plan/NewEditor_UI第二阶段依赖方向子计划_2026-04-19.md @@ -0,0 +1,227 @@ +# NewEditor UI第二阶段依赖方向子计划 +日期:2026-04-19 + +## 目标 + +本阶段只覆盖审核方向中的“依赖方向审核”,从软件工程最佳实践出发,严格审查 `UI基础层 / UI runtime层 / UI editor层 / new_editor` 当前的真实依赖关系,确认依赖是否单向、公开面是否稳定、跨层访问是否被明确约束。 + +本阶段完成后,应得到两类结果: + +- 一份基于真实代码与构建图的依赖问题清单 +- 一套从根源收口依赖方向的整改方案,而不是继续靠局部兼容和路径补丁维持 + +## 范围 + +本阶段聚焦以下对象: + +- `engine/include`、`engine/src` 中被 UI 模块与 `new_editor` 使用到的接口 +- `new_editor/include`、`new_editor/src`、`new_editor/app` +- UI 三层相关的 CMake target 与 link 关系 +- 公开头文件、include path、命名空间、桥接层、适配层 +- `new_editor` 与旧 `editor` 之间是否仍存在隐式耦合 + +本阶段不覆盖: + +- 新功能开发 +- 纯视觉重做 +- 与依赖方向无关的局部交互 bug +- feature 级表现优化 + +## 审核重点 + +### 一、Target 依赖图是否真实表达层次 + +- `XCUIEditorLib`、`XCUIEditorHost`、`XCUIEditorAppLib`、`XCUIEditorApp` 的依赖方向是否符合分层 +- target 的 `PUBLIC / PRIVATE / INTERFACE` 暴露是否正确 +- 是否存在“编译能过,但架构已经反向引用”的目标关系 + +### 二、公开头文件暴露面是否过宽 + +- `include` 下是否仍暴露实现细节、产品语义或临时桥接 +- 公共头是否继续向上游泄漏具体 feature、平台、宿主、旧实现细节 +- 是否存在头文件看似是公共 API,实际只能在当前产品里使用 + +### 三、include path 是否允许跨层直连 + +- 是否仍通过过宽的 include path 允许访问不该访问的内部目录 +- 是否存在“目录分层了,但 include 仍然可以随意穿透”的情况 +- 是否存在 `src` 内部实现被上层直接 include 的情况 + +### 四、命名空间与目录是否表达同一层次 + +- 命名空间是否真实表达所属层,而不是只改了目录没改责任 +- 是否存在挂在通用命名空间下、实则依赖 app/product 语义的代码 +- 是否存在桥接层、compat 层、legacy 层长期停留在错误边界 + +### 五、new_editor 是否仍对旧 editor 或其他内部实现形成隐式耦合 + +- 是否仍有直接源码复用、头文件直连、实现复制后未完成本地化的情况 +- 是否仍依赖旧 editor 的内部约定、类型命名、生命周期或行为假设 +- 若存在旧实现复用,是否已经被真正下沉为正式能力,而不是继续临时借用 + +## 根因导向的检查方法 + +本阶段不接受“看到一个 include 就删一个 include”的补丁式处理,优先从根因定位: + +1. 先画真实 target 依赖图,再看代码依赖是否与其一致 +2. 再看公开 API 面是否逼着上层跨层访问 +3. 再看 include path 与命名空间是否放大了错误依赖 +4. 最后才决定是: + - 缩小公开面 + - 下沉正式接口 + - 上提通用能力 + - 本地化 legacy 逻辑 + - 删除错误桥接 + +## 执行步骤 + +1. 盘点 `new_editor` 与 UI 三层相关 target 的 link 关系、include path、公开头暴露面 +2. 基于代码搜索列出所有跨层访问、旧 editor 耦合、`engine/src` 直连、产品语义泄漏点 +3. 把问题按根因分组,而不是按文件零散罗列 +4. 为每类根因给出正式整改策略: + - 调整 target 依赖 + - 收缩公共头 + - 重放目录与命名空间边界 + - 建立明确桥接契约 + - 将临时 legacy 依赖正式化或彻底移除 +5. 对确定可以立即收口的问题,给出下一轮可执行子任务 + +## 预期产出 + +- 第二方向依赖审核报告 +- 依赖问题的根因分类 +- 下一轮可执行的整改子计划 + +## 验收标准 + +- 能明确说明当前依赖图中哪些部分是健康的,哪些部分是错误的 +- 每个问题都能落到“根因 + 正式解法”,而不是临时规避 +- 能明确回答 `new_editor` 是否还在通过任何形式依赖旧 `editor` 内部实现 +- 能明确回答 UI 三层与 `new_editor` 的公共暴露面是否符合长期演进要求 + +## 严格审查结论 + +### 当前健康项 + +- 当前 `new_editor` 产品代码中,未发现新的 `new_editor -> editor/src` 直接源码依赖 +- 当前 `new_editor` 产品代码中,未发现新的 `new_editor -> engine/src` 直接 include 依赖 +- `new_editor/include/XCEditor` 当前未再暴露 `XCEditor/App/*` 这一类产品层公开头 +- `app/Commands`、`app/State`、`app/Composition` 等产品层头文件当前都留在 `new_editor/app`,没有重新回流到公共 `include` + +### 根因一:`XCUIEditorHost` 与 `XCUIEditorAppLib` 的 target 边界仍然是假的 + +问题现状: + +- `XCUIEditorHost` 与 `XCUIEditorAppLib` 都通过 `${CMAKE_CURRENT_SOURCE_DIR}/app` 作为私有 include 根目录 +- 这导致 `XCUIEditorAppLib` 虽然在 target 依赖上“依赖 Host”,但代码层仍可直接 include Host 实现头,边界没有被真实约束 +- 典型表现: + - `app/Composition/EditorShellRuntime.cpp` 直接 include `Rendering/D3D12/D3D12WindowRenderer.h` 与 `Rendering/Native/NativeRenderer.h` + - `app/Rendering/Assets/BuiltInIcons.h` + - `app/Internal/EmbeddedPngLoader.h` + - `app/Rendering/Viewport/ViewportRenderTargets.h` + - `app/Rendering/Viewport/ViewportHostService.h` + 都直接暴露了 Host/D3D12 实现头或具体实现类型 + +根因判断: + +- 不是“某几个 include 写错了”,而是 Host 与 App 的内部契约没有被单独建模,导致产品层只能直接抓宿主实现头 +- target 分层已经存在,但头文件分层与 include 根没有跟上,所以依赖方向在代码层是松的 + +正式解决方案: + +1. 先建立 App 内部使用的窄 Host 契约前向声明层,禁止 App-facing 头文件继续直接暴露 Host 实现头 +2. 把 App-facing 头中的 Host 具体成员改为前向声明 + 指针/句柄/PIMPL,避免具体实现类型出现在上层可见头中 +3. 下一批继续收口 `app` 目录内部的 include 边界,让 App 不能再默认直连 Host 实现目录 + +执行状态: + +- 已开始执行并完成第一批收口: + - 新增 `new_editor/app/Host/HostFwd.h` + - `BuiltInIcons.h`、`EmbeddedPngLoader.h`、`ViewportRenderTargets.h`、`ViewportHostService.h` 已改为不直接暴露 Host 实现头 + - `ViewportHostService` 已改为通过前向声明与延迟持有方式使用 Host 资源分配器 + +### 根因二:`XCUIEditorLib` 的公共 include path 仍然过宽 + +问题现状: + +- `XCUIEditorLib` 当前把 `${CMAKE_SOURCE_DIR}/engine/include/XCEngine` 作为 `PUBLIC` include path 暴露 +- 这会放大跨层访问面,让依赖方更容易绕开正式头路径,弱化公共 API 的边界 + +根因判断: + +- 这是构建层面对公共暴露面的放宽,不是单个头文件问题 +- 正式公共能力应只通过稳定的 `engine/include` 根暴露,而不是把更深一层目录继续公开 + +正式解决方案: + +1. 删除冗余的 `${CMAKE_SOURCE_DIR}/engine/include/XCEngine` 公共暴露 +2. 要求依赖方统一经由正式 `XCEngine/...` 路径访问 Engine 公共头 + +执行状态: + +- 已执行:`new_editor/CMakeLists.txt` 中已移除该冗余 `PUBLIC` include path + +### 根因三:测试 target 没有保护分层,反而重新放开了错误依赖 + +问题现状: + +- `editor_ui_tests` 当前原本只应验证 `XCUIEditorLib` 的公共层,但其 include path 仍加入了整个 `new_editor` 根目录 +- 这使测试可以直接 include `app/*` +- 实际已出现越层测试: + - `test_editor_window_input_routing.cpp` 直接 include `app/Platform/Win32/EditorWindowPointerCapture.h` + - `test_viewport_object_id_picker.cpp` 直接 include `app/Rendering/Viewport/ViewportObjectIdPicker.h` + +根因判断: + +- 不是“测试写错了两处”这么简单,而是 `editor_ui_tests` 的 target 边界定义本身就错了 +- 当测试 target 可以随便看到 `app`,它就不再能作为 `XCUIEditorLib` 公共边界的保护网 + +正式解决方案: + +1. 从 `editor_ui_tests` 中移除整个 `new_editor` 根目录 include path +2. 把依赖 `app/*` 的测试迁移到 `editor_app_feature_tests` +3. 后续继续以 target 为单位区分“公共层测试”和“产品层测试” + +执行状态: + +- 已执行: + - `editor_ui_tests` 已移除 `${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}` + - `test_editor_window_input_routing.cpp` + - `test_viewport_object_id_picker.cpp` + 已迁移到 `editor_app_feature_tests` + +### 根因四:公共 API 中仍有少量“默认装配策略”与“产品默认值”残留 + +问题现状: + +- `XCEditor/Workspace/*` 里仍保留 `BuildDefaultUIEditorWorkspaceController`、`BuildDefaultUIEditorWindowWorkspaceController`、`BuildDefaultUIEditorWindowWorkspaceSet`、`BuildDefaultUIEditorWorkspaceSession` +- `XCEditor/Shell/UIEditorShellAsset.h` 中仍内嵌 `screenId = "editor.shell"` 这样的默认产品值 + +根因判断: + +- 这部分不是立即破坏依赖方向的最高风险项,但它说明公共层里仍残留默认装配策略与产品默认语义 +- 是否保留这些 API,需要明确回答:它们究竟是稳定的 editor 通用装配能力,还是当前产品的默认策略 + +正式解决方案: + +1. 下一批逐个判定这些 API 的真实归属 +2. 真正通用的保留在公共层,但去掉产品默认字面量 +3. 仅服务当前产品装配的,迁回 `new_editor/app/Composition` + +执行状态: + +- 已纳入下一批整改范围,本轮先不做补丁式挪动 + +## 当前阶段执行项 + +### 已完成 + +1. 收紧 `XCUIEditorLib` 的 Engine 公共 include 暴露面 +2. 收紧 `editor_ui_tests` 的 include 边界,并把 app 级测试迁移到 `editor_app_feature_tests` +3. 为 App-facing 头文件建立 Host 前向声明层,减少 Host 实现头外溢 + +### 下一批继续执行 + +1. 继续收紧 `XCUIEditorHost` 与 `XCUIEditorAppLib` 的内部 include 边界,让 target 边界在代码层也成立 +2. 逐步把 App-facing 头里残留的 Host/D3D12 具体实现类型继续下沉到 `.cpp` 或私有实现 +3. 决定 `BuildDefault*` 与 `EditorShellAsset` 默认值的真实归属,并做正式迁移 diff --git a/docs/plan/SRP_核心层与首方渲染管线拆层计划_2026-04-19.md b/docs/plan/SRP_核心层与首方渲染管线拆层计划_2026-04-19.md new file mode 100644 index 00000000..3d9f51f2 --- /dev/null +++ b/docs/plan/SRP_核心层与首方渲染管线拆层计划_2026-04-19.md @@ -0,0 +1,420 @@ +# SRP 核心层与首方渲染管线拆层计划 2026-04-19 + +## 1. 结论 + +`docs/plan/SRP_Mainline_2026-04-16.md` 和 +`docs/plan/SRP_Runtime_v2_2026-04-18.md` +都应该归档。 + +原因不是“SRP 已经全部做完”,而是这两份计划对应的目标阶段, +已经被当前代码状态实质性越过了。 + +当前代码已经具备: + +1. 可运行的 native RenderGraph / planning / execution 主链 +2. 可运行的 managed SRP runtime +3. `Renderer / Feature / Pass` 这一层的基础模型 +4. 一条能跑起来的 first-party forward renderer 骨架 + +所以下一阶段的真正主线,不再是“继续搭 SRP runtime”,而是: + +**把当前混在 `managed/XCEngine.ScriptCore` 里的 SRP Core 与首方渲染管线层拆开, +把边界做干净,为后续真正的 URP-like 包层和用户自定义渲染管线做准备。** + +另外,`RenderGraph` 仍然应该留在 C++ 层。 +它属于渲染内核,不属于未来的 URP 包层。 + +--- + +## 2. 当前状态判断 + +### 2.1 已经成立的部分 + +从当前代码看,下面这些能力已经不是计划,而是已经存在: + +#### Native 侧 + +1. `engine/src/Rendering/Graph/RenderGraph.cpp` +2. `engine/src/Rendering/Graph/RenderGraphCompiler.cpp` +3. `engine/src/Rendering/Graph/RenderGraphExecutor.cpp` +4. `engine/src/Rendering/Pipelines/ScriptableRenderPipelineHost.cpp` +5. `engine/src/Rendering/Pipelines/ManagedScriptableRenderPipelineAsset.cpp` +6. `engine/src/Rendering/GraphicsSettingsState.cpp` + +这说明 native 侧已经具备: + +1. RenderGraph 录制、编译、执行 +2. pipeline host +3. managed pipeline bridge +4. graphics settings 状态承接 + +#### Managed 侧 SRP Core / Runtime + +1. `managed/XCEngine.ScriptCore/RenderPipelineAsset.cs` +2. `managed/XCEngine.ScriptCore/ScriptableRenderPipelineAsset.cs` +3. `managed/XCEngine.ScriptCore/ScriptableRenderPipeline.cs` +4. `managed/XCEngine.ScriptCore/GraphicsSettings.cs` +5. `managed/XCEngine.ScriptCore/ScriptableRenderContext.cs` +6. `managed/XCEngine.ScriptCore/ScriptableRenderPipelineCameraRequestContext.cs` +7. `managed/XCEngine.ScriptCore/ScriptableRenderPipelinePlanningContext.cs` + +这说明 managed 侧已经不是只有一个“空壳入口”,而是已经有真正的 +pipeline asset、pipeline、context、graphics settings 运行时接口。 + +#### Managed 侧 Renderer / Feature / Pass 层 + +1. `managed/XCEngine.ScriptCore/ScriptableRendererData.cs` +2. `managed/XCEngine.ScriptCore/ScriptableRenderer.cs` +3. `managed/XCEngine.ScriptCore/ScriptableRendererFeature.cs` +4. `managed/XCEngine.ScriptCore/ScriptableRenderPass.cs` +5. `managed/XCEngine.ScriptCore/RenderPassEvent.cs` +6. `managed/XCEngine.ScriptCore/RenderingData.cs` +7. `managed/XCEngine.ScriptCore/CameraData.cs` + +这意味着旧计划里“下一步先把 Renderer / Feature / Pass 搭出来” +这件事,现在也已经不是待做事项了。 + +#### 当前首方 forward 骨架 + +1. `managed/XCEngine.ScriptCore/ForwardRenderPipelineAsset.cs` +2. `managed/XCEngine.ScriptCore/ForwardRendererData.cs` +3. `managed/XCEngine.ScriptCore/ForwardRenderer.cs` +4. `managed/XCEngine.ScriptCore/ColorScalePostProcessRendererFeature.cs` +5. `managed/XCEngine.ScriptCore/DisableDirectionalShadowRendererFeature.cs` +6. `managed/GameScripts/RenderPipelineApiProbe.cs` + +这说明当前已经不仅有 SRP runtime, +而且已经有一条“首方 renderer + feature + pass”的工作链路。 + +### 2.2 当前真正的问题 + +现在的主矛盾已经不是“SRP 能不能跑起来”,而是下面这些结构问题: + +1. `managed/XCEngine.ScriptCore` 同时塞了基础脚本 API、SRP Core、首方 forward renderer、示例 feature +2. `ScriptableRenderer / ScriptableRenderPass / RenderPassEvent / RenderingData` + 这些更像 URP-like 层的东西,仍然和 SRP Core 混在一起 +3. `ForwardRenderer*`、`ColorScalePostProcessRendererFeature`、 + `DisableDirectionalShadowRendererFeature` 这种明显属于首方管线层的实现, + 还在“ScriptCore”里直接暴露 +4. `ScriptableRenderContext` 目前仍暴露了 + `ScenePhase`、`SceneRenderInjectionPoint`、`FullscreenPassDescriptor` + 这类偏 prototype / native contract 的接口 +5. `GraphicsSettingsState` 虽然已经比以前正规很多,但项目级配置语义仍然偏 + “descriptor/bootstrap”,还不是最终的资源化资产语义 +6. `managed/CMakeLists.txt` 目前只假设有 + `XCEngine.ScriptCore.dll + GameScripts.dll` + 两层 managed 程序集,说明还不适合直接粗暴切成多个 engine 程序集 + +--- + +## 3. 根因分析 + +### 3.1 旧计划解决的是“能不能跑” + +前两轮 SRP 计划的核心任务是: + +1. 让 managed pipeline 真正存在 +2. 让 native 能持有并回调 managed pipeline +3. 让 C# 能参与 RenderGraph / stage 录制 +4. 让 `Renderer / Feature / Pass` 这一层先长出来 + +这些事情现在已经基本成立。 + +### 3.2 当前阶段要解决的是“边界是否稳定” + +如果现在不先做拆层,后面继续往上堆功能,就会出现三个后果: + +1. `XCEngine.ScriptCore` 会继续膨胀成一个“什么都往里塞”的程序集 +2. 未来 URP-like 包层会直接继承今天这些 prototype API, + 把临时接口永久化 +3. 用户以后做自定义渲染管线时,拿到的不是干净的 SRP Core, + 而是一坨混合了引擎内核、首方 renderer 和试验性 feature 的 API + +所以当前最值钱的一刀不是继续加 pass, +而是先把层级边界做对。 + +--- + +## 4. 下一阶段的正确目标 + +下一阶段的目标不是“立刻做完整 URP”,而是先把下面这个结构稳定下来: + +`Native RHI / Native RenderGraph / Native Renderer Contract` +`-> Managed Base Script API` +`-> Managed SRP Core` +`-> Managed First-Party Renderer Layer` +`-> GameScripts / 用户自定义渲染管线与 Feature` + +这里最关键的是: + +1. C++ 继续负责渲染执行内核 +2. SRP Core 只保留真正稳定、长期保留的管线抽象 +3. `Renderer / Feature / Pass` 和当前 `ForwardRenderer*` + 视为首方渲染层,不再和 SRP Core 混放 +4. 真正的 URP-like 包层,要建立在这条边界已经清晰之后 + +--- + +## 5. 目标边界 + +### 5.1 应该留在 SRP Core 的内容 + +当前阶段建议保留在 SRP Core 的主要内容: + +1. `RenderPipelineAsset` +2. `ScriptableRenderPipelineAsset` +3. `ScriptableRenderPipeline` +4. `ScriptableRenderContext` +5. `GraphicsSettings` +6. `CameraFrameStage` +7. `CameraFrameColorSource` +8. `ScriptableRenderPipelineCameraRequestContext` +9. `ScriptableRenderPipelinePlanningContext` +10. 和 pipeline asset / frame planning 直接相关的最小数据契约 + +### 5.2 应该归入首方渲染层的内容 + +当前阶段建议划到首方渲染层的内容: + +1. `ScriptableRendererData` +2. `ScriptableRenderer` +3. `ScriptableRendererFeature` +4. `ScriptableRenderPass` +5. `RenderPassEvent` +6. `RenderingData` +7. `CameraData` +8. `LightingData` +9. `ShadowData` +10. `EnvironmentData` +11. `FinalColorData` +12. `StageColorData` +13. `ForwardRenderPipelineAsset` +14. `ForwardRendererData` +15. `ForwardRenderer` +16. `ColorScalePostProcessRendererFeature` +17. `DisableDirectionalShadowRendererFeature` + +理由很简单: + +这些类型描述的不是“最底层 SRP 抽象”, +而是“一个首方 renderer 如何组织 pass 和 feature”。 + +这在 Unity 语义里更接近 URP 包层,而不是 SRP Core。 + +### 5.3 应该收紧或下沉的 prototype 接口 + +下面这些东西不适合长期作为 public SRP Core API 暴露: + +1. `ScenePhase` +2. `SceneRenderInjectionPoint` +3. `FullscreenPassDescriptor` + +它们当前可以存在, +但后续应该逐步: + +1. 移入首方渲染层 +2. 或改成更中性的 context helper / renderer backend contract +3. 而不是长期挂在用户直接面对的 Core 表面 + +--- + +## 6. 执行策略 + +### 原则 1:先拆层,再拆程序集 + +虽然长期目标是把 SRP Core 和首方渲染层真正拆到不同程序集, +但当前 `managed/CMakeLists.txt` 还只支持: + +1. `XCEngine.ScriptCore.dll` +2. `GameScripts.dll` + +所以不能一上来同时做: + +1. 大量 API 重命名 +2. 多程序集构建 +3. 运行时装载链改造 +4. probe / tests 全量迁移 + +这样风险太高。 + +更正确的顺序是: + +1. 先在现有单程序集内把文件结构、命名空间、责任边界拆清楚 +2. 再补多程序集构建与装载 +3. 最后再把首方渲染层独立成真正的 URP-like 包层 + +### 原则 2:不长期保留双轨兼容 + +这一阶段不建议为了“兼容旧 API”长期保留两套命名或两套入口。 + +可以有极短暂的迁移提交, +但目标必须是: + +1. core 是 core +2. first-party renderer 是 first-party renderer +3. 旧的混放路径尽快清掉 + +### 原则 3:先整理 ownership,再继续加功能 + +在拆层完成之前,不应该继续往现有 `ScriptCore` 里堆: + +1. 更多 post-process feature +2. 更多 shadow 策略脚本封装 +3. deferred +4. lightmap +5. 大量 renderer asset 编辑器工作流 + +否则只会把错的边界继续固化。 + +--- + +## 7. 分阶段实施 + +### Phase A: 单程序集内先完成目录与命名空间拆层 + +目标: + +1. 在 `managed/XCEngine.ScriptCore` 内先把 + `Base / SRP Core / FirstParty Rendering` + 至少分目录 +2. 能拆命名空间的地方先拆命名空间 +3. 更新 `managed/CMakeLists.txt`,不再把 SRP 与首方 forward 实现混成一坨平铺文件 +4. 不改变 native 行为,只做结构重组与边界收口 + +建议的第一版目录方向: + +1. `managed/XCEngine.ScriptCore/Rendering/Core/*` +2. `managed/XCEngine.ScriptCore/Rendering/FirstParty/*` +3. `managed/XCEngine.ScriptCore/Rendering/Internal/*` + +验收: + +1. `XCEngine.ScriptCore.dll` 继续可编译 +2. 旧 editor 能继续加载脚本程序集 +3. SRP 现有 probe 不回退 + +### Phase B: 把首方 renderer 契约从 Core 里拔出去 + +目标: + +1. 让 `ScriptableRenderer* / ScriptableRenderPass / RenderPassEvent` + 从“像 Core”变成“明确属于首方渲染层” +2. `ForwardRenderer*` 不再伪装成“底层核心的一部分” +3. 现有示例 feature 一并迁入首方层 + +这一阶段完成后,`ScriptableRenderPipeline` 本身只负责: + +1. pipeline 生命周期 +2. 选择和驱动 renderer +3. 不再自己承担首方 renderer 的组织细节 + +### Phase C: 收紧 `ScriptableRenderContext` 的原型泄漏 + +目标: + +1. 盘点 `ScenePhase / SceneRenderInjectionPoint / FullscreenPassDescriptor` + 这些接口是否应该长期 public +2. 把明显属于首方 renderer backend 的能力收口到更合适的位置 +3. 如果需要新增中性 helper,则以最小包装形式补上 + +这一阶段的原则是: + +**不是马上做成 Unity 全量 `ScriptableRenderContext`,** +**而是先把今天暴露错层的接口收回正确层。** + +### Phase D: GraphicsSettings 和 pipeline asset 语义正式化 + +目标: + +1. 保留当前 `GraphicsSettings.renderPipelineAsset` + 作为 managed 入口 +2. 继续弱化 native 侧“descriptor-only”的语义 +3. 为未来项目级资源引用、资产持久化和默认管线恢复打地基 + +注意: + +这一步不是优先去做 editor UI, +而是先把 runtime 语义收干净。 + +### Phase E: 多程序集 / 包层化前置改造 + +当 Phase A-D 收口后,再进入真正的多程序集前置: + +1. 扩展 `managed/CMakeLists.txt` +2. 让 engine managed assemblies 不再只有 `XCEngine.ScriptCore.dll` +3. 让 `GameScripts.dll` 能引用: + - base script core + - SRP core + - first-party pipeline assembly +4. 让 runtime 装载链支持多个 engine managed assemblies + +只有到这里,后面再开: + +1. `XCEngine.RenderPipelines.Core.dll` +2. `XCEngine.RenderPipelines.Universal.dll` + +才是顺的。 + +--- + +## 8. 第一刀应该落在哪里 + +下一步最正确的第一刀是: + +**先做 Phase A,先把 `managed/XCEngine.ScriptCore` 内部的 SRP Core 与首方渲染层拆目录、拆责任、拆命名空间。** + +原因: + +1. 这是当前最根上的结构问题 +2. 这一步不需要先动 editor +3. 这一步不需要先动 deferred / lightmap / shadow 迁移 +4. 这一步做对了,后面的 URP-like 包层才不会建立在混乱边界上 + +--- + +## 9. 本阶段明确不做的内容 + +这一阶段明确不做: + +1. deferred pipeline +2. lightmap / baking +3. 大规模 editor 面板工作流 +4. 把阴影、体积、高斯全部立刻搬到 C# +5. 直接把 RenderGraph 暴露给用户脚本 +6. 直接把所有 managed 渲染代码拆成多个程序集并同时改运行时装载链 + +这些都不是现在最值钱的一刀。 + +--- + +## 10. 阶段验收标准 + +这一阶段收口时,至少要满足: + +1. `docs/plan` 和代码主线一致,不再把已经完成的 SRP runtime 阶段继续挂在当前计划里 +2. `managed/XCEngine.ScriptCore` 内部已经能明确区分: + - Base Script API + - SRP Core + - First-Party Rendering +3. 首方 forward renderer 与示例 feature 不再伪装成“core” +4. `ScriptableRenderContext` 上明显错层的 public surface 已被盘点并开始收口 +5. `GraphicsSettings` 与 pipeline asset 的后续正式化方向已经明确 +6. editor 与现有 SRP probe 不回退 + +只要上面这些条件不成立,这一阶段就不能算收口。 + +--- + +## 11. 收口后的下一阶段 + +等这一阶段收口后,下一阶段才适合正式切到: + +`SRP Core 正式独立` +`-> First-Party Universal/Forward Pipeline 包层` +`-> 用户自定义 RendererFeature / Pass` +`-> 再往后才是 deferred / lightmap / 更完整的 renderer asset 工作流` + +也就是说: + +**现在不是“继续补 SRP runtime”,** +**而是“把已经长出来的 SRP runtime 和首方渲染层做一次真正的结构定型”。** diff --git a/docs/plan/NewEditor_UI第一阶段边界收口子计划_2026-04-19.md b/docs/used/NewEditor_UI第一阶段边界收口子计划_阶段归档_2026-04-19.md similarity index 100% rename from docs/plan/NewEditor_UI第一阶段边界收口子计划_2026-04-19.md rename to docs/used/NewEditor_UI第一阶段边界收口子计划_阶段归档_2026-04-19.md diff --git a/docs/used/NewEditor_UI第四方向状态模型子计划_阶段归档_2026-04-19.md b/docs/used/NewEditor_UI第四方向状态模型子计划_阶段归档_2026-04-19.md new file mode 100644 index 00000000..3bbcb6ce --- /dev/null +++ b/docs/used/NewEditor_UI第四方向状态模型子计划_阶段归档_2026-04-19.md @@ -0,0 +1,132 @@ +# NewEditor UI 第四方向状态模型子计划 + +日期:2026-04-19 + +状态:已完成 + +## 目标 + +本子计划只覆盖 `NewEditor UI 模块三层与 new_editor 实现审核` 中的第四个审核方向:状态模型。 + +目标不是继续靠局部补丁修交互,而是从根源上收口 `new_editor` 的状态组织方式,解决以下四类问题: + +1. 全局选择状态存在多真源。 +2. 命令焦点被错误地建模为 `workspace.activePanelId` 的派生物。 +3. 指针交互所有权分散在 host、shell、viewport、feature 多层。 +4. 多窗口 workspace 以每个窗口的 controller 副本为基础,缺少中心真源。 + +## 根因结论 + +### 1. 全局选择没有 editor 级唯一真源 + +`EditorSession`、`EditorProjectRuntime`、`EditorSceneRuntime` 都在维护“当前选择”,`Inspector` 再通过来源竞争去决定显示谁。这不是同步时机问题,而是状态主模型缺失问题。 + +### 2. 命令焦点被错误绑定到布局激活状态 + +`workspace.activePanelId` 本质是布局状态,不是真实命令焦点。继续用它驱动 `edit.*` 路由,会让 viewport、property grid、rename、context menu 等真实交互所有者无法成为正式命令焦点源。 + +### 3. 指针 owner 没有统一模型 + +host capture、shell capture、hosted content capture、viewport capture、scene tool capture 各自维护状态,最后再靠布尔值拼接。结果就是 capture 生命周期分裂,冲突只能靠事件过滤硬挡。 + +### 4. 多窗口 workspace 没有中心 store + +原实现把每个 `EditorWindow` 里的 `UIEditorWorkspaceController` 当作活的权威副本,再由窗口协调层从这些窗口副本反向拼出 `UIEditorWindowWorkspaceSet`。这会让“窗口投影”和“全局真源”混成一层,分离、合并、关闭窗口都只能通过重建和回灌状态维持正确性。 + +## 重构原则 + +1. 先建立 editor 级真源,再把窗口、runtime、UI 降级成投影或领域解释层。 +2. 不再接受双向同步、stamp 竞争、布尔拼接这类维持性方案。 +3. 任何“当前状态”都必须能明确回答: + - 谁是唯一写入者? + - 谁只是投影或缓存? +4. 每次收口都要让最终结构更清晰,而不是再引入新的兼容层长期滞留。 + +## 执行结果 + +### 阶段 1:统一全局选择真源 + +已完成。 + +落地结果: + +- 新增 `EditorSelectionService`,作为 editor 级唯一选择真源。 +- `EditorProjectRuntime` 与 `EditorSceneRuntime` 在 app 模式下绑定共享选择服务;测试或独立场景下才使用各自回退状态。 +- `EditorSession.selection` 降级为投影快照,不再作为权威来源。 +- `InspectorSubject` 不再在 project/scene 之间做 stamp 竞争,而是直接消费统一选择快照。 +- `WorkspaceEventSync` 改为从选择服务同步会话投影。 + +### 阶段 2:建立独立命令焦点服务 + +已完成。 + +落地结果: + +- 新增 `EditorCommandFocusService`。 +- `EditorHostCommandBridge` 的 `edit.*` 路由先读取命令焦点服务,`activePanelId` 只保留为兜底投影。 +- `HierarchyPanel`、`ProjectPanel`、`InspectorPanel`、`SceneViewportController` 在真实交互发生时显式声明命令焦点。 +- `EditorSession.activeRoute` 改为命令焦点投影,不再由 workspace 同步逻辑直接写入。 + +### 阶段 3:统一指针交互 owner + +已完成。 + +落地结果: + +- 新增 `EditorShellPointerInteraction`,把 shell 层输出统一成单一 owner 语义。 +- `EditorShellRuntime` 统一对外暴露当前指针 owner。 +- `EditorWindow` 的 Win32 capture 投影改为只根据 shell runtime 的当前 owner 应用,不再同时拼接 shell 结果和 hosted-content 请求。 + +### 阶段 4:收口多窗口 workspace 真源 + +已完成。 + +落地结果: + +- 新增 `EditorWindowWorkspaceStore`,作为多窗口 workspace 的中心状态源。 +- `EditorWindowManager::CreateEditorWindow()` 创建窗口后立即把窗口投影注册进中心 store。 +- `EditorWindowHostRuntime::RenderAllWindows()` 在每帧渲染后把窗口内产生的 workspace/session 变更回写到中心 store。 +- `EditorWindowWorkspaceCoordinator` 不再从 open windows 反向重建 `UIEditorWindowWorkspaceSet`。 +- detach / drag-merge / dock 这类跨窗口操作改为: + 1. 从中心 store 构造 mutation controller。 + 2. 直接在中心 window set 上执行操作。 + 3. 把结果同步回各窗口投影。 + 4. 成功后再提交回中心 store。 +- 窗口销毁时,中心 store 会移除对应窗口状态;主窗口销毁则清空整套多窗口状态。 +- `EditorWindow` 对外不再暴露可变 `workspace controller`,窗口内 controller 进一步明确为内部可变投影,而不是外部可写权威对象。 + +## 验收结论 + +当前验收结论: + +1. 多窗口状态不再通过“从窗口副本反向拼全局状态”的方式工作。 +2. 跨窗口操作已经直接作用于中心 store,再把结果投影回窗口。 +3. 窗口内局部布局变更会在渲染后回写中心 store,窗口 controller 不再承担长期权威职责。 +4. 状态模型四个阶段已经全部落地。 + +## 测试结果 + +已验证: + +- `cmake --build build_codex_verify --config Debug --target editor_app_feature_tests` +- `build_codex_verify/tests/UI/Editor/unit/Debug/editor_app_feature_tests.exe` + +结果: + +- 89 tests from 14 test suites +- all passed + +本轮新增覆盖: + +- `test_editor_window_workspace_store.cpp` + +覆盖点包括: + +- 主窗口投影注册进入中心 store +- 窗口内局部变更回写中心 store +- 跨窗口 mutation 基于中心 store 执行 +- 移除 detached window 后自动修复 active window 引用 + +## 归档建议 + +该子计划已经完成,可以归档到 `docs/used`。 diff --git a/docs/plan/SRP_Mainline_2026-04-16.md b/docs/used/SRP_Mainline_过期归档_2026-04-19.md similarity index 100% rename from docs/plan/SRP_Mainline_2026-04-16.md rename to docs/used/SRP_Mainline_过期归档_2026-04-19.md diff --git a/docs/plan/SRP_Runtime_v2_2026-04-18.md b/docs/used/SRP_Runtime_v2_过期归档_2026-04-19.md similarity index 100% rename from docs/plan/SRP_Runtime_v2_2026-04-18.md rename to docs/used/SRP_Runtime_v2_过期归档_2026-04-19.md diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 5679257d..d42a6c49 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -303,6 +303,7 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) ) set(XCUI_EDITOR_APP_PLATFORM_SOURCES + app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.cpp app/Platform/Win32/EditorWindowBorderlessPlacement.cpp app/Platform/Win32/EditorWindowBorderlessResize.cpp app/Platform/Win32/EditorWindowFrame.cpp diff --git a/new_editor/app/Commands/EditorHostCommandBridge.h b/new_editor/app/Commands/EditorHostCommandBridge.h index 0f217cda..6c515b15 100644 --- a/new_editor/app/Commands/EditorHostCommandBridge.h +++ b/new_editor/app/Commands/EditorHostCommandBridge.h @@ -1,6 +1,7 @@ #pragma once #include "Commands/EditorEditCommandRoute.h" +#include "State/EditorCommandFocusService.h" #include "State/EditorSession.h" #include @@ -13,6 +14,7 @@ namespace XCEngine::UI::Editor::App { class EditorHostCommandBridge : public UIEditorHostCommandHandler { public: void BindSession(EditorSession& session); + void BindCommandFocusService(const EditorCommandFocusService& commandFocusService); void BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, EditorEditCommandRoute* projectRoute, @@ -46,9 +48,11 @@ private: std::string_view commandId); UIEditorHostCommandEvaluationResult EvaluateUnsupportedHostCommand( std::string_view commandId) const; + EditorActionRoute ResolveActiveEditRoute() const; EditorEditCommandRoute* ResolveEditCommandRoute(EditorActionRoute route) const; EditorSession* m_session = nullptr; + const EditorCommandFocusService* m_commandFocusService = nullptr; EditorEditCommandRoute* m_hierarchyRoute = nullptr; EditorEditCommandRoute* m_projectRoute = nullptr; EditorEditCommandRoute* m_sceneRoute = nullptr; diff --git a/new_editor/app/Composition/EditorShellPointerInteraction.h b/new_editor/app/Composition/EditorShellPointerInteraction.h new file mode 100644 index 00000000..f802f049 --- /dev/null +++ b/new_editor/app/Composition/EditorShellPointerInteraction.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace XCEngine::UI::Editor::App { + +enum class EditorShellPointerOwner : std::uint8_t { + None = 0, + DockHost, + SceneViewport, + GameViewport, + HierarchyPanel, + ProjectPanel, +}; + +constexpr bool IsShellPointerOwner(EditorShellPointerOwner owner) { + return owner == EditorShellPointerOwner::DockHost || + owner == EditorShellPointerOwner::SceneViewport || + owner == EditorShellPointerOwner::GameViewport; +} + +constexpr bool IsHostedContentPointerOwner(EditorShellPointerOwner owner) { + return owner == EditorShellPointerOwner::HierarchyPanel || + owner == EditorShellPointerOwner::ProjectPanel; +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellRuntime.cpp b/new_editor/app/Composition/EditorShellRuntime.cpp index 70a4b398..81d21e53 100644 --- a/new_editor/app/Composition/EditorShellRuntime.cpp +++ b/new_editor/app/Composition/EditorShellRuntime.cpp @@ -4,6 +4,8 @@ #include "Host/ViewportRenderHost.h" #include "State/EditorContext.h" +#include "Composition/EditorPanelIds.h" + #include namespace XCEngine::UI::Editor::App { @@ -104,42 +106,56 @@ Widgets::UIEditorDockHostCursorKind EditorShellRuntime::GetDockCursorKind() cons m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout); } +EditorShellPointerOwner EditorShellRuntime::GetPointerOwner() const { + const auto& dockHostInteractionState = + m_shellInteractionState.workspaceInteractionState.dockHostInteractionState; + if (dockHostInteractionState.splitterDragState.active || + !dockHostInteractionState.activeTabDragNodeId.empty()) { + return EditorShellPointerOwner::DockHost; + } + + for (const auto& panelState : + m_shellInteractionState.workspaceInteractionState.composeState.panelStates) { + if (!panelState.viewportShellState.inputBridgeState.captured) { + continue; + } + + if (panelState.panelId == kGamePanelId) { + return EditorShellPointerOwner::GameViewport; + } + + return EditorShellPointerOwner::SceneViewport; + } + + if (m_hierarchyPanel.HasActivePointerCapture()) { + return EditorShellPointerOwner::HierarchyPanel; + } + + if (m_projectPanel.HasActivePointerCapture()) { + return EditorShellPointerOwner::ProjectPanel; + } + + return EditorShellPointerOwner::None; +} + bool EditorShellRuntime::WantsHostPointerCapture() const { - return m_hierarchyPanel.WantsHostPointerCapture() || - m_projectPanel.WantsHostPointerCapture(); + return IsHostedContentPointerOwner(GetPointerOwner()); } bool EditorShellRuntime::WantsHostPointerRelease() const { - return (m_hierarchyPanel.WantsHostPointerRelease() || - m_projectPanel.WantsHostPointerRelease()) && - !HasHostedContentCapture(); + return !IsHostedContentPointerOwner(GetPointerOwner()); } bool EditorShellRuntime::HasHostedContentCapture() const { - return m_hierarchyPanel.HasActivePointerCapture() || - m_projectPanel.HasActivePointerCapture(); + return IsHostedContentPointerOwner(GetPointerOwner()); } bool EditorShellRuntime::HasShellInteractiveCapture() const { - if (m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.splitterDragState.active) { - return true; - } - - if (!m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.activeTabDragNodeId.empty()) { - return true; - } - - for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) { - if (panelState.viewportShellState.inputBridgeState.captured) { - return true; - } - } - - return false; + return IsShellPointerOwner(GetPointerOwner()); } bool EditorShellRuntime::HasInteractiveCapture() const { - return HasShellInteractiveCapture() || HasHostedContentCapture(); + return GetPointerOwner() != EditorShellPointerOwner::None; } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Composition/EditorShellRuntime.h b/new_editor/app/Composition/EditorShellRuntime.h index cec14a13..ca432ce8 100644 --- a/new_editor/app/Composition/EditorShellRuntime.h +++ b/new_editor/app/Composition/EditorShellRuntime.h @@ -1,5 +1,6 @@ #pragma once +#include "Composition/EditorShellPointerInteraction.h" #include "Composition/EditorShellVariant.h" #include "Features/Console/ConsolePanel.h" #include "Features/Hierarchy/HierarchyPanel.h" @@ -80,6 +81,7 @@ public: ProjectPanel::CursorKind GetHostedContentCursorKind() const; Widgets::UIEditorDockHostCursorKind GetDockCursorKind() const; + EditorShellPointerOwner GetPointerOwner() const; bool WantsHostPointerCapture() const; bool WantsHostPointerRelease() const; bool HasHostedContentCapture() const; diff --git a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp b/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp index da14daa0..67e8a9c8 100644 --- a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp +++ b/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp @@ -123,7 +123,9 @@ void EditorShellRuntime::Update( m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout; m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime()); + m_hierarchyPanel.SetCommandFocusService(&context.GetCommandFocusService()); m_sceneEditCommandRoute.BindSceneRuntime(&context.GetSceneRuntime()); + m_sceneViewportController.SetCommandFocusService(&context.GetCommandFocusService()); // Keep the previous render request available for readback-based picking during // this update, then refresh it again after camera/navigation state changes. m_viewportHostService.SetSceneViewportRenderRequest( @@ -190,6 +192,8 @@ void EditorShellRuntime::Update( const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; m_projectPanel.SetProjectRuntime(&context.GetProjectRuntime()); + m_projectPanel.SetCommandFocusService(&context.GetCommandFocusService()); + m_inspectorPanel.SetCommandFocusService(&context.GetCommandFocusService()); m_hierarchyPanel.Update( m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, hostedContentEvents, @@ -208,6 +212,7 @@ void EditorShellRuntime::Update( hostedContentEvents, !m_shellFrame.result.workspaceInputSuppressed, activePanelId == kInspectorPanelId); + context.SyncSessionFromCommandFocusService(); m_consolePanel.Update( context.GetSession(), m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); diff --git a/new_editor/app/Composition/WorkspaceEventSync.cpp b/new_editor/app/Composition/WorkspaceEventSync.cpp index b3fc5f1b..6f38bb94 100644 --- a/new_editor/app/Composition/WorkspaceEventSync.cpp +++ b/new_editor/app/Composition/WorkspaceEventSync.cpp @@ -144,11 +144,12 @@ std::vector SyncWorkspaceEvents( EditorContext& context, const EditorShellRuntime& runtime) { std::vector entries = {}; - context.SyncSessionFromProjectRuntime(); + context.SyncSessionFromSelectionService(); if (const std::optional scenePath = context.GetProjectRuntime().ConsumePendingSceneOpenPath(); scenePath.has_value()) { context.GetSceneRuntime().OpenSceneAsset(scenePath.value()); + context.SyncSessionFromSelectionService(); } for (const HierarchyPanel::Event& event : runtime.GetHierarchyPanelEvents()) { diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp b/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp index 18cc8407..7a9353b4 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp +++ b/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp @@ -1,6 +1,7 @@ #include "HierarchyPanelInternal.h" #include "Scene/EditorSceneRuntime.h" +#include "State/EditorCommandFocusService.h" #include #include @@ -37,6 +38,13 @@ bool HasValidBounds(const UIRect& bounds) { return bounds.width > 0.0f && bounds.height > 0.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; +} + } // namespace void HierarchyPanel::Initialize() { @@ -52,6 +60,11 @@ void HierarchyPanel::SetSceneRuntime(EditorSceneRuntime* sceneRuntime) { SyncModelFromScene(); } +void HierarchyPanel::SetCommandFocusService( + EditorCommandFocusService* commandFocusService) { + m_commandFocusService = commandFocusService; +} + void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) { m_icons = icons; RebuildItems(); @@ -319,6 +332,31 @@ void HierarchyPanel::SyncTreeFocusState( } } +void HierarchyPanel::ClaimCommandFocus( + const std::vector& inputEvents, + const UIRect& bounds, + bool allowInteraction) { + if (m_commandFocusService == nullptr) { + return; + } + + for (const UIInputEvent& event : inputEvents) { + if (event.type == ::XCEngine::UI::UIInputEventType::FocusGained) { + m_commandFocusService->ClaimFocus(EditorActionRoute::Hierarchy); + return; + } + + if (!allowInteraction || + event.type != ::XCEngine::UI::UIInputEventType::PointerButtonDown || + !ContainsPoint(bounds, event.position)) { + continue; + } + + m_commandFocusService->ClaimFocus(EditorActionRoute::Hierarchy); + return; + } +} + UIRect HierarchyPanel::BuildRenameBounds( std::string_view itemId, const Widgets::UIEditorTreeViewLayout& layout) const { @@ -564,6 +602,7 @@ void HierarchyPanel::Update( .captureActive = HasActivePointerCapture() }); SyncTreeFocusState(filteredEvents); + ClaimCommandFocus(filteredEvents, panelState->bounds, allowInteraction); const Widgets::UIEditorTreeViewMetrics treeMetrics = ResolveUIEditorTreeViewMetrics(); diff --git a/new_editor/app/Features/Hierarchy/HierarchyPanel.h b/new_editor/app/Features/Hierarchy/HierarchyPanel.h index 61c60f95..1e125ffe 100644 --- a/new_editor/app/Features/Hierarchy/HierarchyPanel.h +++ b/new_editor/app/Features/Hierarchy/HierarchyPanel.h @@ -19,6 +19,7 @@ namespace XCEngine::UI::Editor::App { class BuiltInIcons; +class EditorCommandFocusService; class EditorSceneRuntime; class HierarchyPanel final : public EditorEditCommandRoute { @@ -40,6 +41,7 @@ public: void Initialize(); void SetSceneRuntime(EditorSceneRuntime* sceneRuntime); + void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void SetBuiltInIcons(const BuiltInIcons* icons); void ResetInteractionState(); void Update( @@ -86,11 +88,16 @@ private: const Widgets::UIEditorTreeViewLayout& layout); void SyncTreeFocusState( const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents); + void ClaimCommandFocus( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const ::XCEngine::UI::UIRect& bounds, + bool allowInteraction); ::XCEngine::UI::UIRect BuildRenameBounds( std::string_view itemId, const Widgets::UIEditorTreeViewLayout& layout) const; const BuiltInIcons* m_icons = nullptr; + EditorCommandFocusService* m_commandFocusService = nullptr; EditorSceneRuntime* m_sceneRuntime = nullptr; HierarchyModel m_model = {}; std::vector m_treeItems = {}; diff --git a/new_editor/app/Features/Inspector/InspectorPanel.cpp b/new_editor/app/Features/Inspector/InspectorPanel.cpp index 0ad32788..cc7f98a1 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.cpp +++ b/new_editor/app/Features/Inspector/InspectorPanel.cpp @@ -8,6 +8,7 @@ #include "Features/Inspector/Components/IInspectorComponentEditor.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Scene/EditorSceneRuntime.h" +#include "State/EditorCommandFocusService.h" #include #include @@ -32,6 +33,13 @@ constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f); constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 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); @@ -68,6 +76,11 @@ const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPa return nullptr; } +void InspectorPanel::SetCommandFocusService( + EditorCommandFocusService* commandFocusService) { + m_commandFocusService = commandFocusService; +} + void InspectorPanel::ResetPanelState() { m_visible = false; m_bounds = {}; @@ -154,6 +167,30 @@ UIRect InspectorPanel::BuildGridBounds() const { return UIRect(x, y, width, height); } +void InspectorPanel::ClaimCommandFocus( + const std::vector& inputEvents, + bool allowInteraction) { + if (m_commandFocusService == nullptr) { + return; + } + + for (const UIInputEvent& event : inputEvents) { + if (event.type == UIInputEventType::FocusGained) { + m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector); + return; + } + + if (!allowInteraction || + event.type != UIInputEventType::PointerButtonDown || + !ContainsPoint(m_bounds, event.position)) { + continue; + } + + m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector); + return; + } +} + const InspectorPresentationComponentBinding* InspectorPanel::FindSelectedComponentBinding() const { if (!m_fieldSelection.HasSelection()) { return nullptr; @@ -275,6 +312,7 @@ void InspectorPanel::Update( .allowFocusEvents = panelActive, .includePointerLeave = allowInteraction || panelActive }); + ClaimCommandFocus(filteredEvents, allowInteraction); m_gridFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, m_fieldSelection, diff --git a/new_editor/app/Features/Inspector/InspectorPanel.h b/new_editor/app/Features/Inspector/InspectorPanel.h index 1add1924..7f055d9c 100644 --- a/new_editor/app/Features/Inspector/InspectorPanel.h +++ b/new_editor/app/Features/Inspector/InspectorPanel.h @@ -18,10 +18,12 @@ namespace XCEngine::UI::Editor::App { +class EditorCommandFocusService; class EditorSceneRuntime; class InspectorPanel final : public EditorEditCommandRoute { public: + void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void Update( const EditorSession& session, EditorSceneRuntime& sceneRuntime, @@ -45,9 +47,13 @@ private: void SyncSelectionState(); std::string BuildSubjectKey() const; ::XCEngine::UI::UIRect BuildGridBounds() const; + void ClaimCommandFocus( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + bool allowInteraction); const InspectorPresentationComponentBinding* FindSelectedComponentBinding() const; bool ApplyChangedField(std::string_view fieldId); + EditorCommandFocusService* m_commandFocusService = nullptr; EditorSceneRuntime* m_sceneRuntime = nullptr; bool m_visible = false; ::XCEngine::UI::UIRect m_bounds = {}; diff --git a/new_editor/app/Features/Inspector/InspectorSubject.cpp b/new_editor/app/Features/Inspector/InspectorSubject.cpp index 03933c84..1e3fba42 100644 --- a/new_editor/app/Features/Inspector/InspectorSubject.cpp +++ b/new_editor/app/Features/Inspector/InspectorSubject.cpp @@ -7,32 +7,28 @@ 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(); + switch (session.selection.kind) { + case EditorSelectionKind::HierarchyNode: + if (sceneRuntime.HasSceneSelection()) { + return InspectorSelectionSource::Scene; + } + return InspectorSelectionSource::None; - if (projectStamp > sceneStamp) { - return hasProjectSelection - ? InspectorSelectionSource::Project - : InspectorSelectionSource::None; + case EditorSelectionKind::ProjectItem: + if (!session.selection.itemId.empty()) { + return InspectorSelectionSource::Project; + } + return InspectorSelectionSource::None; + + case EditorSelectionKind::None: + default: + break; } - if (sceneStamp > projectStamp) { - return hasSceneSelection - ? InspectorSelectionSource::Scene - : InspectorSelectionSource::None; - } - - if (hasSceneSelection) { + if (sceneRuntime.HasSceneSelection()) { return InspectorSelectionSource::Scene; } - if (hasProjectSelection) { - return InspectorSelectionSource::Project; - } - return InspectorSelectionSource::None; } diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index c9b914a6..1a3d9714 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -1,6 +1,7 @@ #include "ProjectPanelInternal.h" #include "Project/EditorProjectRuntime.h" +#include "State/EditorCommandFocusService.h" #include #include @@ -212,6 +213,11 @@ void ProjectPanel::SetProjectRuntime(EditorProjectRuntime* projectRuntime) { SyncAssetSelectionFromRuntime(); } +void ProjectPanel::SetCommandFocusService( + EditorCommandFocusService* commandFocusService) { + m_commandFocusService = commandFocusService; +} + void ProjectPanel::SetBuiltInIcons(const BuiltInIcons* icons) { m_icons = icons; if (EditorProjectRuntime* runtime = ResolveProjectRuntime(); @@ -1308,6 +1314,31 @@ void ProjectPanel::ResetTransientFrames() { m_splitterDragging = false; } +void ProjectPanel::ClaimCommandFocus( + const std::vector& inputEvents, + const UIRect& bounds, + bool allowInteraction) { + if (m_commandFocusService == nullptr) { + return; + } + + for (const UIInputEvent& event : inputEvents) { + if (event.type == UIInputEventType::FocusGained) { + m_commandFocusService->ClaimFocus(EditorActionRoute::Project); + return; + } + + if (!allowInteraction || + event.type != UIInputEventType::PointerButtonDown || + !ContainsPoint(bounds, event.position)) { + continue; + } + + m_commandFocusService->ClaimFocus(EditorActionRoute::Project); + return; + } +} + void ProjectPanel::Update( const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, @@ -1364,6 +1395,7 @@ void ProjectPanel::Update( .allowFocusEvents = panelActive || HasActivePointerCapture(), .includePointerLeave = allowInteraction || HasActivePointerCapture() }); + ClaimCommandFocus(filteredEvents, panelState->bounds, allowInteraction); m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width); m_layout = BuildLayout(panelState->bounds); diff --git a/new_editor/app/Features/Project/ProjectPanel.h b/new_editor/app/Features/Project/ProjectPanel.h index 0545c438..1fd7c376 100644 --- a/new_editor/app/Features/Project/ProjectPanel.h +++ b/new_editor/app/Features/Project/ProjectPanel.h @@ -27,6 +27,7 @@ namespace XCEngine::UI::Editor::App { class BuiltInIcons; +class EditorCommandFocusService; class ProjectPanel final : public EditorEditCommandRoute { public: enum class CursorKind : std::uint8_t { @@ -69,6 +70,7 @@ public: void Initialize(const std::filesystem::path& repoRoot); void SetProjectRuntime(EditorProjectRuntime* projectRuntime); + void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void SetBuiltInIcons(const BuiltInIcons* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); void ResetInteractionState(); @@ -205,6 +207,10 @@ private: const ::XCEngine::UI::UIRect& bounds, bool allowInteraction, bool panelActive) const; + void ClaimCommandFocus( + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const ::XCEngine::UI::UIRect& bounds, + bool allowInteraction); UIEditorHostCommandEvaluationResult EvaluateAssetCommand( std::string_view commandId, std::string_view explicitItemId, @@ -224,6 +230,7 @@ private: std::unique_ptr m_ownedProjectRuntime = {}; EditorProjectRuntime* m_projectRuntime = nullptr; + EditorCommandFocusService* m_commandFocusService = nullptr; const BuiltInIcons* m_icons = nullptr; const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr; ::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {}; diff --git a/new_editor/app/Features/Scene/SceneViewportController.cpp b/new_editor/app/Features/Scene/SceneViewportController.cpp index 1046c1f3..dd557edb 100644 --- a/new_editor/app/Features/Scene/SceneViewportController.cpp +++ b/new_editor/app/Features/Scene/SceneViewportController.cpp @@ -2,6 +2,7 @@ #include "Rendering/Viewport/ViewportHostService.h" #include "Scene/EditorSceneRuntime.h" +#include "State/EditorCommandFocusService.h" #include "Composition/EditorPanelIds.h" #include @@ -134,6 +135,11 @@ void SceneViewportController::ResetInteractionState() { m_legacyGizmo.ResetVisualState(); } +void SceneViewportController::SetCommandFocusService( + EditorCommandFocusService* commandFocusService) { + m_commandFocusService = commandFocusService; +} + void SceneViewportController::Update( EditorSceneRuntime& sceneRuntime, ViewportHostService& viewportHostService, @@ -168,6 +174,18 @@ void SceneViewportController::Update( ? inputState.lastScreenPointerPosition : inputFrame.screenPointerPosition; + if (m_commandFocusService != nullptr && + (inputFrame.focused || + std::any_of( + inputFrame.pointerButtonTransitions.begin(), + inputFrame.pointerButtonTransitions.end(), + [&](const auto& transition) { + return transition.pressed && + ContainsPoint(slotLayout.inputRect, transition.screenPosition); + }))) { + m_commandFocusService->ClaimFocus(EditorActionRoute::Scene); + } + if (inputFrame.focusLost) { m_navigationState = {}; m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; diff --git a/new_editor/app/Features/Scene/SceneViewportController.h b/new_editor/app/Features/Scene/SceneViewportController.h index 8ab02cae..4bc0cb52 100644 --- a/new_editor/app/Features/Scene/SceneViewportController.h +++ b/new_editor/app/Features/Scene/SceneViewportController.h @@ -13,6 +13,7 @@ namespace XCEngine::UI::Editor::App { +class EditorCommandFocusService; class EditorSceneRuntime; class ViewportHostService; @@ -33,6 +34,7 @@ public: Host::TextureHost& renderer); void Shutdown(Host::TextureHost& renderer); void ResetInteractionState(); + void SetCommandFocusService(EditorCommandFocusService* commandFocusService); void Update( EditorSceneRuntime& sceneRuntime, @@ -53,6 +55,7 @@ private: float ConsumeDeltaTimeSeconds(); NavigationState m_navigationState = {}; + EditorCommandFocusService* m_commandFocusService = nullptr; LegacySceneViewportGizmo m_legacyGizmo = {}; SceneViewportToolOverlay m_toolOverlay = {}; std::size_t m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index a46ddac1..032e1e1f 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -94,7 +94,6 @@ public: bool IsClosing() const; const std::wstring& GetTitle() const; const UIEditorWorkspaceController& GetWorkspaceController() const; - UIEditorWorkspaceController& GetWorkspaceController(); const UIEditorShellInteractionFrame& GetShellFrame() const; ::XCEngine::UI::UIPoint ConvertScreenPixelsToClientDips(const POINT& screenPoint) const; @@ -121,6 +120,7 @@ private: void SetTrackingMouseLeave(bool trackingMouseLeave); void SetTitle(std::wstring title); void ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController); + UIEditorWorkspaceController& GetMutableWorkspaceController(); bool Initialize( const std::filesystem::path& repoRoot, @@ -228,8 +228,7 @@ private: float PixelsToDips(float pixels) const; ::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const; std::string BuildCaptureStatusText() const; - void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); - void ApplyHostedContentCaptureRequests(); + void ApplyShellRuntimePointerCapture(); std::string DescribeInputEvents( const std::vector<::XCEngine::UI::UIInputEvent>& events) const; Host::BorderlessWindowResizeEdge HitTestBorderlessWindowResizeEdge(LPARAM lParam) const; diff --git a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp b/new_editor/app/Platform/Win32/EditorWindowFrame.cpp index 49fe1a1a..6fa393c4 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrame.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrame.cpp @@ -3,6 +3,7 @@ #include "Platform/Win32/EditorWindowInternalState.h" #include "Platform/Win32/EditorWindowRuntimeInternal.h" #include "Platform/Win32/EditorWindowStyle.h" +#include "Composition/EditorShellPointerInteraction.h" #include "State/EditorContext.h" #include @@ -218,7 +219,7 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( "input", DescribeInputEvents(frameEvents) + " | " + editorContext.DescribeWorkspaceState( - m_state->composition.workspaceController, + GetWorkspaceController(), m_state->composition.shellRuntime.GetShellInteractionState())); } @@ -230,9 +231,10 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( editorContext.AttachTextMeasurer(m_state->render.renderer); const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(); + UIEditorWorkspaceController& workspaceController = GetMutableWorkspaceController(); m_state->composition.shellRuntime.Update( editorContext, - m_state->composition.workspaceController, + workspaceController, workspaceBounds, frameEvents, BuildCaptureStatusText(), @@ -253,12 +255,11 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( const EditorWindowFrameTransferRequests transferRequests = BuildShellTransferRequests(globalTabDragActive, dockHostInteractionState, shellFrame); - ApplyHostCaptureRequests(shellFrame.result); + ApplyShellRuntimePointerCapture(); for (const WorkspaceTraceEntry& entry : m_state->composition.shellRuntime.GetTraceEntries()) { LogRuntimeTrace(entry.channel, entry.message); } - ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); m_state->composition.shellRuntime.Append(drawList); if (frameContext.canRenderViewports) { @@ -303,7 +304,7 @@ void EditorWindow::LogFrameInteractionTrace( << " commandExecuted=" << (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") << " active=" - << m_state->composition.workspaceController.GetWorkspace().activePanelId + << GetWorkspaceController().GetWorkspace().activePanelId << " message=" << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; LogRuntimeTrace("frame", frameTrace.str()); @@ -375,22 +376,24 @@ void EditorWindow::SyncShellCapturedPointerButtonsFromSystemState() { QueueSyntheticPointerStateSyncEvent(modifiers); } -void EditorWindow::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { - if (result.requestPointerCapture) { +void EditorWindow::ApplyShellRuntimePointerCapture() { + const EditorShellPointerOwner owner = + m_state->composition.shellRuntime.GetPointerOwner(); + if (IsShellPointerOwner(owner)) { AcquirePointerCapture(EditorWindowPointerCaptureOwner::Shell); + return; } - if (result.releasePointerCapture) { + + if (IsHostedContentPointerOwner(owner)) { + AcquirePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); + return; + } + + if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::Shell)) { ReleasePointerCapture(EditorWindowPointerCaptureOwner::Shell); } -} -void EditorWindow::ApplyHostedContentCaptureRequests() { - if (m_state->composition.shellRuntime.WantsHostPointerCapture()) { - AcquirePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); - } - - if (m_state->composition.shellRuntime.WantsHostPointerRelease() && - !m_state->composition.shellRuntime.HasShellInteractiveCapture()) { + if (OwnsPointerCapture(EditorWindowPointerCaptureOwner::HostedContent)) { ReleasePointerCapture(EditorWindowPointerCaptureOwner::HostedContent); } } diff --git a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp index b7ef3e64..a43da9d4 100644 --- a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp @@ -125,7 +125,7 @@ const UIEditorWorkspaceController& EditorWindow::GetWorkspaceController() const return m_state->composition.workspaceController; } -UIEditorWorkspaceController& EditorWindow::GetWorkspaceController() { +UIEditorWorkspaceController& EditorWindow::GetMutableWorkspaceController() { return m_state->composition.workspaceController; } diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.cpp new file mode 100644 index 00000000..26af2f79 --- /dev/null +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.cpp @@ -0,0 +1,169 @@ +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h" + +#include +#include + +namespace XCEngine::UI::Editor::App::Internal { + +namespace { + +UIEditorWindowWorkspaceState BuildWindowState( + std::string_view windowId, + const UIEditorWorkspaceController& workspaceController) { + UIEditorWindowWorkspaceState state = {}; + state.windowId = std::string(windowId); + state.workspace = workspaceController.GetWorkspace(); + state.session = workspaceController.GetSession(); + return state; +} + +std::string ResolveFallbackWindowId(const UIEditorWindowWorkspaceSet& windowSet) { + if (!windowSet.primaryWindowId.empty() && + FindUIEditorWindowWorkspaceState(windowSet, windowSet.primaryWindowId) != nullptr) { + return windowSet.primaryWindowId; + } + + return windowSet.windows.empty() ? std::string() : windowSet.windows.front().windowId; +} + +void UpsertWindowState( + UIEditorWindowWorkspaceSet& windowSet, + UIEditorWindowWorkspaceState windowState) { + if (UIEditorWindowWorkspaceState* existing = + FindMutableUIEditorWindowWorkspaceState(windowSet, windowState.windowId); + existing != nullptr) { + *existing = std::move(windowState); + return; + } + + windowSet.windows.push_back(std::move(windowState)); +} + +} // namespace + +EditorWindowWorkspaceStore::EditorWindowWorkspaceStore(UIEditorPanelRegistry panelRegistry) + : m_panelRegistry(std::move(panelRegistry)) {} + +UIEditorWindowWorkspaceController EditorWindowWorkspaceStore::BuildMutationController() const { + return UIEditorWindowWorkspaceController(m_panelRegistry, m_windowSet); +} + +bool EditorWindowWorkspaceStore::RegisterWindowProjection( + std::string_view windowId, + bool primary, + const UIEditorWorkspaceController& workspaceController, + std::string& outError) { + UIEditorWindowWorkspaceSet nextWindowSet = m_windowSet; + UpsertWindowState(nextWindowSet, BuildWindowState(windowId, workspaceController)); + + if (primary) { + if (!nextWindowSet.primaryWindowId.empty() && nextWindowSet.primaryWindowId != windowId) { + outError = + "Cannot register a second primary window '" + std::string(windowId) + + "' while '" + nextWindowSet.primaryWindowId + "' is still primary."; + return false; + } + nextWindowSet.primaryWindowId = std::string(windowId); + } else if (nextWindowSet.primaryWindowId.empty()) { + outError = + "Cannot register detached window '" + std::string(windowId) + + "' before the primary window exists."; + return false; + } + + if (nextWindowSet.activeWindowId.empty() || + FindUIEditorWindowWorkspaceState(nextWindowSet, nextWindowSet.activeWindowId) == nullptr) { + nextWindowSet.activeWindowId = ResolveFallbackWindowId(nextWindowSet); + } + + if (!ValidateWindowSet(nextWindowSet, outError)) { + return false; + } + + ReplaceWindowSet(std::move(nextWindowSet)); + outError.clear(); + return true; +} + +bool EditorWindowWorkspaceStore::CommitWindowProjection( + std::string_view windowId, + const UIEditorWorkspaceController& workspaceController, + std::string& outError) { + UIEditorWindowWorkspaceSet nextWindowSet = m_windowSet; + UIEditorWindowWorkspaceState* existingWindow = + FindMutableUIEditorWindowWorkspaceState(nextWindowSet, windowId); + if (existingWindow == nullptr) { + outError = "Window '" + std::string(windowId) + "' is not registered in the workspace store."; + return false; + } + + existingWindow->workspace = workspaceController.GetWorkspace(); + existingWindow->session = workspaceController.GetSession(); + + if (nextWindowSet.activeWindowId.empty() || + FindUIEditorWindowWorkspaceState(nextWindowSet, nextWindowSet.activeWindowId) == nullptr) { + nextWindowSet.activeWindowId = ResolveFallbackWindowId(nextWindowSet); + } + + if (!ValidateWindowSet(nextWindowSet, outError)) { + return false; + } + + ReplaceWindowSet(std::move(nextWindowSet)); + outError.clear(); + return true; +} + +bool EditorWindowWorkspaceStore::ValidateWindowSet( + const UIEditorWindowWorkspaceSet& windowSet, + std::string& outError) const { + const UIEditorWindowWorkspaceValidationResult validation = + ValidateUIEditorWindowWorkspaceSet(m_panelRegistry, windowSet); + if (!validation.IsValid()) { + outError = validation.message; + return false; + } + + outError.clear(); + return true; +} + +void EditorWindowWorkspaceStore::ReplaceWindowSet(UIEditorWindowWorkspaceSet windowSet) { + m_windowSet = std::move(windowSet); +} + +void EditorWindowWorkspaceStore::RemoveWindow(std::string_view windowId, bool primary) { + if (m_windowSet.windows.empty()) { + return; + } + + if (primary || m_windowSet.primaryWindowId == windowId) { + m_windowSet = {}; + return; + } + + m_windowSet.windows.erase( + std::remove_if( + m_windowSet.windows.begin(), + m_windowSet.windows.end(), + [windowId](const UIEditorWindowWorkspaceState& state) { + return state.windowId == windowId; + }), + m_windowSet.windows.end()); + + if (m_windowSet.windows.empty()) { + m_windowSet = {}; + return; + } + + if (FindUIEditorWindowWorkspaceState(m_windowSet, m_windowSet.primaryWindowId) == nullptr) { + m_windowSet.primaryWindowId = ResolveFallbackWindowId(m_windowSet); + } + + if (m_windowSet.activeWindowId == windowId || + FindUIEditorWindowWorkspaceState(m_windowSet, m_windowSet.activeWindowId) == nullptr) { + m_windowSet.activeWindowId = ResolveFallbackWindowId(m_windowSet); + } +} + +} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h new file mode 100644 index 00000000..32120a87 --- /dev/null +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App::Internal { + +class EditorWindowWorkspaceStore final { +public: + explicit EditorWindowWorkspaceStore(UIEditorPanelRegistry panelRegistry); + + const UIEditorPanelRegistry& GetPanelRegistry() const { + return m_panelRegistry; + } + + bool HasState() const { + return !m_windowSet.windows.empty(); + } + + const UIEditorWindowWorkspaceSet& GetWindowSet() const { + return m_windowSet; + } + + UIEditorWindowWorkspaceController BuildMutationController() const; + + bool RegisterWindowProjection( + std::string_view windowId, + bool primary, + const UIEditorWorkspaceController& workspaceController, + std::string& outError); + + bool CommitWindowProjection( + std::string_view windowId, + const UIEditorWorkspaceController& workspaceController, + std::string& outError); + + bool ValidateWindowSet( + const UIEditorWindowWorkspaceSet& windowSet, + std::string& outError) const; + + void ReplaceWindowSet(UIEditorWindowWorkspaceSet windowSet); + + void RemoveWindow(std::string_view windowId, bool primary); + +private: + UIEditorPanelRegistry m_panelRegistry = {}; + UIEditorWindowWorkspaceSet m_windowSet = {}; +}; + +} // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Platform/Win32/WindowManager/Internal.h b/new_editor/app/Platform/Win32/WindowManager/Internal.h index e0bc1445..db595d6c 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Internal.h +++ b/new_editor/app/Platform/Win32/WindowManager/Internal.h @@ -1,6 +1,7 @@ #pragma once #include "Platform/Win32/EditorWindowManager.h" +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h" namespace XCEngine::UI::Editor::App::Internal { @@ -75,6 +76,10 @@ public: explicit EditorWindowWorkspaceCoordinator(EditorWindowHostRuntime& hostRuntime); ~EditorWindowWorkspaceCoordinator(); + void RegisterExistingWindow(EditorWindow& window); + void CommitWindowProjection(EditorWindow& window); + void HandleWindowDestroyed(const EditorWindow& window); + bool IsGlobalTabDragActive() const; bool OwnsActiveGlobalTabDrag(std::string_view windowId) const; void EndGlobalTabDragSession(); @@ -95,15 +100,12 @@ private: POINT dragHotspot = {}; }; - UIEditorWindowWorkspaceSet BuildWindowWorkspaceSet( - std::string_view activeWindowId = {}) const; - UIEditorWindowWorkspaceController BuildLiveWindowWorkspaceController( - std::string_view activeWindowId) const; + UIEditorWindowWorkspaceController BuildWorkspaceMutationController() const; bool SynchronizeWindowsFromWindowSet( const UIEditorWindowWorkspaceSet& windowSet, std::string_view preferredNewWindowId, const POINT& preferredScreenPoint); - bool SynchronizeWindowsFromController( + bool CommitWindowWorkspaceMutation( const UIEditorWindowWorkspaceController& windowWorkspaceController, std::string_view preferredNewWindowId, const POINT& preferredScreenPoint); @@ -111,6 +113,7 @@ private: const UIEditorWindowWorkspaceState& windowState) const; std::wstring BuildWindowTitle( const UIEditorWorkspaceController& workspaceController) const; + void RefreshWindowTitle(EditorWindow& window) const; RECT BuildDetachedWindowRect(const POINT& screenPoint) const; void BeginGlobalTabDragSession( std::string_view panelWindowId, @@ -142,6 +145,7 @@ private: void LogRuntimeTrace(std::string_view channel, std::string_view message) const; EditorWindowHostRuntime& m_hostRuntime; + EditorWindowWorkspaceStore m_workspaceStore; GlobalTabDragSession m_globalTabDragSession = {}; }; diff --git a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp index 11315a02..420a58b3 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp @@ -30,7 +30,12 @@ EditorWindowManager::~EditorWindowManager() = default; EditorWindow* EditorWindowManager::CreateEditorWindow( UIEditorWorkspaceController workspaceController, const CreateParams& params) { - return m_hostRuntime->CreateEditorWindow(std::move(workspaceController), params); + EditorWindow* const window = + m_hostRuntime->CreateEditorWindow(std::move(workspaceController), params); + if (window != nullptr) { + m_workspaceCoordinator->RegisterExistingWindow(*window); + } + return window; } void EditorWindowManager::HandlePendingNativeWindowCreated(HWND hwnd) { @@ -93,6 +98,9 @@ void EditorWindowManager::EndGlobalTabDragSession() { } void EditorWindowManager::HandleDestroyedWindow(HWND hwnd) { + if (EditorWindow* window = m_hostRuntime->FindWindow(hwnd); window != nullptr) { + m_workspaceCoordinator->HandleWindowDestroyed(*window); + } m_hostRuntime->HandleDestroyedWindow(hwnd); } @@ -284,6 +292,7 @@ void EditorWindowHostRuntime::RenderAllWindows( EditorWindowFrameTransferRequests transferRequests = window->RenderFrame(m_editorContext, globalTabDragActive); + workspaceCoordinator.CommitWindowProjection(*window); if (!transferRequests.HasPendingRequests()) { continue; } @@ -382,64 +391,86 @@ void EditorWindowHostRuntime::LogRuntimeTrace( EditorWindowWorkspaceCoordinator::EditorWindowWorkspaceCoordinator( EditorWindowHostRuntime& hostRuntime) - : m_hostRuntime(hostRuntime) {} + : m_hostRuntime(hostRuntime), + m_workspaceStore(hostRuntime.GetEditorContext().GetShellAsset().panelRegistry) {} EditorWindowWorkspaceCoordinator::~EditorWindowWorkspaceCoordinator() = default; -UIEditorWindowWorkspaceSet EditorWindowWorkspaceCoordinator::BuildWindowWorkspaceSet( - std::string_view activeWindowId) const { - UIEditorWindowWorkspaceSet windowSet = {}; - if (const EditorWindow* primaryWindow = m_hostRuntime.FindPrimaryWindow(); - primaryWindow != nullptr && - primaryWindow->GetHwnd() != nullptr && - !primaryWindow->IsClosing()) { - windowSet.primaryWindowId = std::string(primaryWindow->GetWindowId()); +void EditorWindowWorkspaceCoordinator::RegisterExistingWindow(EditorWindow& window) { + std::string error = {}; + if (!m_workspaceStore.RegisterWindowProjection( + window.GetWindowId(), + window.IsPrimary(), + window.GetWorkspaceController(), + error)) { + LogRuntimeTrace( + "window", + "failed to register window '" + std::string(window.GetWindowId()) + + "' in workspace store: " + error); + return; } - for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { - if (window == nullptr || - window->GetHwnd() == nullptr || - window->IsClosing()) { - continue; + RefreshWindowTitle(window); +} + +void EditorWindowWorkspaceCoordinator::CommitWindowProjection(EditorWindow& window) { + std::string error = {}; + if (!m_workspaceStore.CommitWindowProjection( + window.GetWindowId(), + window.GetWorkspaceController(), + error)) { + if (m_workspaceStore.RegisterWindowProjection( + window.GetWindowId(), + window.IsPrimary(), + window.GetWorkspaceController(), + error)) { + RefreshWindowTitle(window); + return; } - 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)); + LogRuntimeTrace( + "window", + "failed to commit window projection for '" + std::string(window.GetWindowId()) + + "': " + error); + return; } - windowSet.activeWindowId = - !activeWindowId.empty() && - ([this, activeWindowId]() { - const EditorWindow* activeWindow = m_hostRuntime.FindWindow(activeWindowId); - return activeWindow != nullptr && - activeWindow->GetHwnd() != nullptr && - !activeWindow->IsClosing(); - })() - ? std::string(activeWindowId) - : windowSet.primaryWindowId; + RefreshWindowTitle(window); +} - return windowSet; +void EditorWindowWorkspaceCoordinator::HandleWindowDestroyed(const EditorWindow& window) { + m_workspaceStore.RemoveWindow(window.GetWindowId(), window.IsPrimary()); } UIEditorWindowWorkspaceController -EditorWindowWorkspaceCoordinator::BuildLiveWindowWorkspaceController( - std::string_view activeWindowId) const { - return UIEditorWindowWorkspaceController( - m_hostRuntime.GetEditorContext().GetShellAsset().panelRegistry, - BuildWindowWorkspaceSet(activeWindowId)); +EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController() const { + return m_workspaceStore.BuildMutationController(); } UIEditorWorkspaceController EditorWindowWorkspaceCoordinator::BuildWorkspaceControllerForWindow( const UIEditorWindowWorkspaceState& windowState) const { return UIEditorWorkspaceController( - m_hostRuntime.GetEditorContext().GetShellAsset().panelRegistry, + m_workspaceStore.GetPanelRegistry(), windowState.workspace, windowState.session); } +void EditorWindowWorkspaceCoordinator::RefreshWindowTitle(EditorWindow& window) const { + if (window.IsPrimary()) { + return; + } + + const std::wstring title = BuildWindowTitle(window.GetWorkspaceController()); + if (title == window.GetTitle()) { + return; + } + + window.SetTitle(title); + if (window.GetHwnd() != nullptr) { + SetWindowTextW(window.GetHwnd(), window.GetTitle().c_str()); + } +} + void EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests( EditorWindow& sourceWindow, EditorWindowFrameTransferRequests&& transferRequests) { diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp index 7a4864ca..91798bef 100644 --- a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp @@ -405,7 +405,7 @@ bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND h } UIEditorWindowWorkspaceController windowWorkspaceController = - BuildLiveWindowWorkspaceController(targetWindow->GetWindowId()); + BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = dropTarget.placement == UIEditorWorkspaceDockPlacement::Center ? windowWorkspaceController.MovePanelToStack( @@ -427,7 +427,7 @@ bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND h return true; } - if (!SynchronizeWindowsFromController( + if (!CommitWindowWorkspaceMutation( windowWorkspaceController, {}, screenPoint)) { @@ -468,7 +468,7 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( [this, &request, &dragHotspot]( UIEditorWindowWorkspaceController& windowWorkspaceController, const UIEditorWindowWorkspaceOperationResult& result) { - if (!SynchronizeWindowsFromController( + if (!CommitWindowWorkspaceMutation( windowWorkspaceController, result.targetWindowId, request.screenPoint)) { @@ -500,7 +500,7 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( }; UIEditorWindowWorkspaceController windowWorkspaceController = - BuildLiveWindowWorkspaceController(sourceWindow.GetWindowId()); + BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindow.GetWindowId(), @@ -557,7 +557,7 @@ bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( const std::string sourceWindowId(sourceWindow.GetWindowId()); UIEditorWindowWorkspaceController windowWorkspaceController = - BuildLiveWindowWorkspaceController(sourceWindowId); + BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindowId, @@ -568,7 +568,7 @@ bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( return false; } - if (!SynchronizeWindowsFromController( + if (!CommitWindowWorkspaceMutation( windowWorkspaceController, result.targetWindowId, request.screenPoint)) { diff --git a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp index 7944ed6b..fdfec0da 100644 --- a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp @@ -182,14 +182,26 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( return true; } -bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromController( +bool EditorWindowWorkspaceCoordinator::CommitWindowWorkspaceMutation( const UIEditorWindowWorkspaceController& windowWorkspaceController, std::string_view preferredNewWindowId, const POINT& preferredScreenPoint) { - return SynchronizeWindowsFromWindowSet( - windowWorkspaceController.GetWindowSet(), - preferredNewWindowId, - preferredScreenPoint); + const UIEditorWindowWorkspaceSet nextWindowSet = windowWorkspaceController.GetWindowSet(); + std::string error = {}; + if (!m_workspaceStore.ValidateWindowSet(nextWindowSet, error)) { + LogRuntimeTrace("window", "workspace mutation validation failed: " + error); + return false; + } + + if (!SynchronizeWindowsFromWindowSet( + nextWindowSet, + preferredNewWindowId, + preferredScreenPoint)) { + return false; + } + + m_workspaceStore.ReplaceWindowSet(nextWindowSet); + return true; } } // namespace XCEngine::UI::Editor::App::Internal diff --git a/new_editor/app/Project/EditorProjectRuntime.cpp b/new_editor/app/Project/EditorProjectRuntime.cpp index 27a25e01..4d1a038b 100644 --- a/new_editor/app/Project/EditorProjectRuntime.cpp +++ b/new_editor/app/Project/EditorProjectRuntime.cpp @@ -1,7 +1,5 @@ #include "Project/EditorProjectRuntime.h" -#include "State/EditorSelectionStamp.h" - namespace XCEngine::UI::Editor::App { namespace { @@ -81,11 +79,18 @@ EditCommandTarget BuildEditCommandTarget( bool EditorProjectRuntime::Initialize(const std::filesystem::path& repoRoot) { m_browserModel.Initialize(repoRoot); - m_selection = {}; + m_ownedSelectionService = {}; + m_selectionService = &m_ownedSelectionService; m_pendingSceneOpenPath.reset(); return true; } +void EditorProjectRuntime::BindSelectionService( + EditorSelectionService* selectionService) { + m_selectionService = + selectionService != nullptr ? selectionService : &m_ownedSelectionService; +} + void EditorProjectRuntime::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) { m_browserModel.SetFolderIcon(icon); } @@ -104,16 +109,15 @@ ProjectBrowserModel& EditorProjectRuntime::GetBrowserModel() { } bool EditorProjectRuntime::HasSelection() const { - return m_selection.kind == EditorSelectionKind::ProjectItem && - !m_selection.itemId.empty(); + return SelectionService().HasSelectionKind(EditorSelectionKind::ProjectItem); } const EditorSelectionState& EditorProjectRuntime::GetSelection() const { - return m_selection; + return SelectionService().GetSelection(); } std::uint64_t EditorProjectRuntime::GetSelectionStamp() const { - return m_selection.stamp; + return SelectionService().GetStamp(); } bool EditorProjectRuntime::SetSelection(std::string_view itemId) { @@ -122,18 +126,18 @@ bool EditorProjectRuntime::SetSelection(std::string_view itemId) { return false; } + const EditorSelectionState& selection = SelectionService().GetSelection(); 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); + selection.kind != EditorSelectionKind::ProjectItem || + selection.itemId != asset->itemId || + selection.absolutePath != asset->absolutePath || + selection.directory != asset->directory; + ApplySelection(*asset); return changed; } void EditorProjectRuntime::ClearSelection() { - m_selection = {}; - m_selection.stamp = GenerateEditorSelectionStamp(); + SelectionService().ClearSelection(); } bool EditorProjectRuntime::NavigateToFolder(std::string_view itemId) { @@ -230,8 +234,9 @@ EditorProjectRuntime::ResolveAssetCommandTarget( } if (HasSelection()) { + const EditorSelectionState& selection = GetSelection(); if (const ProjectBrowserModel::AssetEntry* selectedAsset = - FindAssetEntry(m_selection.itemId); + FindAssetEntry(selection.itemId); selectedAsset != nullptr) { PopulateAssetCommandTargetFromAsset( target, @@ -278,8 +283,9 @@ EditorProjectRuntime::ResolveEditCommandTarget( } if (HasSelection()) { + const EditorSelectionState& selection = GetSelection(); if (const ProjectBrowserModel::AssetEntry* selectedAsset = - FindAssetEntry(m_selection.itemId); + FindAssetEntry(selection.itemId); selectedAsset != nullptr) { return BuildEditCommandTarget(*selectedAsset); } @@ -416,21 +422,21 @@ bool EditorProjectRuntime::MoveFolderToRoot( return true; } +EditorSelectionService& EditorProjectRuntime::SelectionService() { + return *m_selectionService; +} + +const EditorSelectionService& EditorProjectRuntime::SelectionService() const { + return *m_selectionService; +} + 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(); + const ProjectBrowserModel::AssetEntry& asset) { + SelectionService().SetProjectSelection( + asset.itemId, + asset.displayName.empty() ? asset.nameWithExtension : asset.displayName, + asset.absolutePath, + asset.directory); } void EditorProjectRuntime::RevalidateSelection() { @@ -438,17 +444,19 @@ void EditorProjectRuntime::RevalidateSelection() { return; } - const ProjectBrowserModel::AssetEntry* asset = FindAssetEntry(m_selection.itemId); + const ProjectBrowserModel::AssetEntry* asset = + FindAssetEntry(GetSelection().itemId); if (asset == nullptr) { ClearSelection(); return; } - ApplySelection(*asset, true); + ApplySelection(*asset); } bool EditorProjectRuntime::SelectionTargetsItem(std::string_view itemId) const { - return HasSelection() && IsSameOrDescendantItemId(m_selection.itemId, itemId); + return HasSelection() && + IsSameOrDescendantItemId(GetSelection().itemId, itemId); } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Project/EditorProjectRuntime.h b/new_editor/app/Project/EditorProjectRuntime.h index 801f3e8f..76b8035f 100644 --- a/new_editor/app/Project/EditorProjectRuntime.h +++ b/new_editor/app/Project/EditorProjectRuntime.h @@ -2,6 +2,7 @@ #include "Features/Project/ProjectBrowserModel.h" +#include "State/EditorSelectionService.h" #include "State/EditorSession.h" #include @@ -38,6 +39,7 @@ public: }; bool Initialize(const std::filesystem::path& repoRoot); + void BindSelectionService(EditorSelectionService* selectionService); void SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon); void Refresh(); @@ -98,14 +100,15 @@ public: std::string* movedFolderId = nullptr); private: - void ApplySelection( - const ProjectBrowserModel::AssetEntry& asset, - bool preserveStamp); + EditorSelectionService& SelectionService(); + const EditorSelectionService& SelectionService() const; + void ApplySelection(const ProjectBrowserModel::AssetEntry& asset); void RevalidateSelection(); bool SelectionTargetsItem(std::string_view itemId) const; ProjectBrowserModel m_browserModel = {}; - EditorSelectionState m_selection = {}; + EditorSelectionService m_ownedSelectionService = {}; + EditorSelectionService* m_selectionService = &m_ownedSelectionService; std::optional m_pendingSceneOpenPath = std::nullopt; }; diff --git a/new_editor/app/Scene/EditorSceneRuntime.cpp b/new_editor/app/Scene/EditorSceneRuntime.cpp index aa49e11b..c6fd777f 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.cpp +++ b/new_editor/app/Scene/EditorSceneRuntime.cpp @@ -1,7 +1,5 @@ #include "Scene/EditorSceneRuntime.h" -#include "State/EditorSelectionStamp.h" - #include #include #include @@ -174,6 +172,8 @@ std::string_view GetSceneToolInteractionLockName(SceneToolInteractionLock lock) bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { m_projectRoot = projectRoot; + m_ownedSelectionService = {}; + m_selectionService = &m_ownedSelectionService; m_startupSceneResult = EnsureEditorStartupScene(projectRoot); EnsureSceneViewCamera(); ResetTransformEditHistory(); @@ -182,17 +182,29 @@ bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { return m_startupSceneResult.ready; } +void EditorSceneRuntime::BindSelectionService( + EditorSelectionService* selectionService) { + m_selectionService = + selectionService != nullptr ? selectionService : &m_ownedSelectionService; +} + void EditorSceneRuntime::RefreshScene() { - if (m_selectedGameObjectId.has_value() && !HasValidSelection()) { - m_selectedGameObjectId.reset(); - m_selectionStamp = GenerateEditorSelectionStamp(); + if (HasHierarchySelection()) { + if (!HasValidSelection()) { + ClearSelection(); + } else { + RevalidateSelection(); + } + } else if (m_toolState.dragState.active) { + ResetToolInteractionState(); } ClearInvalidToolInteractionState(); } void EditorSceneRuntime::EnsureSceneSelection() { - if (HasValidSelection()) { + if (HasValidSelection() || + SelectionService().HasSelectionKind(EditorSelectionKind::ProjectItem)) { return; } @@ -262,7 +274,11 @@ bool EditorSceneRuntime::HasSceneSelection() const { } std::optional EditorSceneRuntime::GetSelectedGameObjectId() const { - return HasValidSelection() ? m_selectedGameObjectId : std::nullopt; + if (!HasHierarchySelection()) { + return std::nullopt; + } + + return ParseEditorGameObjectItemId(SelectionService().GetSelection().itemId); } std::string EditorSceneRuntime::GetSelectedItemId() const { @@ -280,14 +296,13 @@ std::string EditorSceneRuntime::GetSelectedDisplayName() const { } const GameObject* EditorSceneRuntime::GetSelectedGameObject() const { - if (!m_selectedGameObjectId.has_value()) { + const std::optional selectedId = GetSelectedGameObjectId(); + if (!selectedId.has_value()) { return nullptr; } Scene* scene = GetActiveScene(); - return scene != nullptr - ? scene->FindByID(m_selectedGameObjectId.value()) - : nullptr; + return scene != nullptr ? scene->FindByID(selectedId.value()) : nullptr; } std::vector EditorSceneRuntime::GetSelectedComponents() const { @@ -316,7 +331,7 @@ std::vector EditorSceneRuntime::GetSelectedCompo } std::uint64_t EditorSceneRuntime::GetSelectionStamp() const { - return m_selectionStamp; + return SelectionService().GetStamp(); } bool EditorSceneRuntime::SetSelection(std::string_view itemId) { @@ -335,26 +350,29 @@ bool EditorSceneRuntime::SetSelection(GameObject::ID id) { } Scene* scene = GetActiveScene(); - if (scene == nullptr || scene->FindByID(id) == nullptr) { + GameObject* gameObject = scene != nullptr ? scene->FindByID(id) : nullptr; + if (gameObject == nullptr) { return false; } + const std::optional previousId = GetSelectedGameObjectId(); const bool changed = - !m_selectedGameObjectId.has_value() || - m_selectedGameObjectId.value() != id; + !previousId.has_value() || + previousId.value() != id || + !HasHierarchySelection(); if (changed) { ResetToolInteractionState(); - m_selectionStamp = GenerateEditorSelectionStamp(); } - m_selectedGameObjectId = id; + SelectionService().SetHierarchySelection( + MakeEditorGameObjectItemId(id), + ResolveGameObjectDisplayName(*gameObject)); ClearInvalidToolInteractionState(); return changed; } void EditorSceneRuntime::ClearSelection() { ResetToolInteractionState(); - m_selectedGameObjectId.reset(); - m_selectionStamp = GenerateEditorSelectionStamp(); + SelectionService().ClearSelection(); } bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { @@ -374,6 +392,7 @@ bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) ResetTransformEditHistory(); ResetToolInteractionState(); + SelectionService().ClearSelection(); RefreshScene(); EnsureSceneSelection(); return true; @@ -836,6 +855,29 @@ void EditorSceneRuntime::ApplySceneViewCameraController() { } } +EditorSelectionService& EditorSceneRuntime::SelectionService() { + return *m_selectionService; +} + +const EditorSelectionService& EditorSceneRuntime::SelectionService() const { + return *m_selectionService; +} + +bool EditorSceneRuntime::HasHierarchySelection() const { + return SelectionService().HasSelectionKind(EditorSelectionKind::HierarchyNode); +} + +void EditorSceneRuntime::RevalidateSelection() { + const GameObject* gameObject = GetSelectedGameObject(); + if (gameObject == nullptr) { + return; + } + + SelectionService().SetHierarchySelection( + MakeEditorGameObjectItemId(gameObject->GetID()), + ResolveGameObjectDisplayName(*gameObject)); +} + bool EditorSceneRuntime::HasValidSelection() const { return GetSelectedGameObject() != nullptr; } @@ -872,7 +914,7 @@ EditorSceneComponentDescriptor EditorSceneRuntime::ResolveSelectedComponentDescr bool EditorSceneRuntime::SelectFirstAvailableGameObject() { Scene* scene = GetActiveScene(); if (scene == nullptr) { - if (m_selectedGameObjectId.has_value()) { + if (HasHierarchySelection()) { ClearSelection(); } return false; @@ -886,7 +928,7 @@ bool EditorSceneRuntime::SelectFirstAvailableGameObject() { return SetSelection(root->GetID()); } - if (m_selectedGameObjectId.has_value()) { + if (HasHierarchySelection()) { ClearSelection(); } return false; diff --git a/new_editor/app/Scene/EditorSceneRuntime.h b/new_editor/app/Scene/EditorSceneRuntime.h index c80b54cd..34ea1cf1 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.h +++ b/new_editor/app/Scene/EditorSceneRuntime.h @@ -3,6 +3,7 @@ #include "Scene/EditorSceneBridge.h" #include "Scene/SceneViewportCameraController.h" #include "Scene/SceneToolState.h" +#include "State/EditorSelectionService.h" #include @@ -59,6 +60,7 @@ struct EditorSceneComponentDescriptor { class EditorSceneRuntime { public: bool Initialize(const std::filesystem::path& projectRoot); + void BindSelectionService(EditorSelectionService* selectionService); void RefreshScene(); void EnsureSceneSelection(); @@ -155,6 +157,10 @@ private: bool EnsureSceneViewCamera(); void ApplySceneViewCameraController(); + EditorSelectionService& SelectionService(); + const EditorSelectionService& SelectionService() const; + bool HasHierarchySelection() const; + void RevalidateSelection(); bool HasValidSelection() const; EditorSceneComponentDescriptor ResolveSelectedComponentDescriptor( std::string_view componentId) const; @@ -165,8 +171,8 @@ private: std::filesystem::path m_projectRoot = {}; EditorStartupSceneResult m_startupSceneResult = {}; - std::optional<::XCEngine::Components::GameObject::ID> m_selectedGameObjectId = std::nullopt; - std::uint64_t m_selectionStamp = 0u; + EditorSelectionService m_ownedSelectionService = {}; + EditorSelectionService* m_selectionService = &m_ownedSelectionService; SceneViewCameraState m_sceneViewCamera = {}; SceneToolState m_toolState = {}; std::vector m_transformUndoStack = {}; diff --git a/new_editor/app/State/EditorCommandFocusService.h b/new_editor/app/State/EditorCommandFocusService.h new file mode 100644 index 00000000..5c0300b9 --- /dev/null +++ b/new_editor/app/State/EditorCommandFocusService.h @@ -0,0 +1,42 @@ +#pragma once + +#include "State/EditorSession.h" + +namespace XCEngine::UI::Editor::App { + +class EditorCommandFocusService { +public: + EditorActionRoute GetExplicitRoute() const { + return m_explicitRoute; + } + + bool HasExplicitRoute() const { + return m_explicitRoute != EditorActionRoute::None; + } + + EditorActionRoute ResolveRoute( + EditorActionRoute fallbackRoute = EditorActionRoute::None) const { + return HasExplicitRoute() ? m_explicitRoute : fallbackRoute; + } + + bool ClaimFocus(EditorActionRoute route) { + if (route == EditorActionRoute::None) { + return false; + } + + const bool changed = m_explicitRoute != route; + m_explicitRoute = route; + return changed; + } + + void ClearFocus(EditorActionRoute route = EditorActionRoute::None) { + if (route == EditorActionRoute::None || m_explicitRoute == route) { + m_explicitRoute = EditorActionRoute::None; + } + } + +private: + EditorActionRoute m_explicitRoute = EditorActionRoute::None; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/State/EditorContext.cpp b/new_editor/app/State/EditorContext.cpp index 0e54e9f8..593c43ea 100644 --- a/new_editor/app/State/EditorContext.cpp +++ b/new_editor/app/State/EditorContext.cpp @@ -2,7 +2,6 @@ #include "Composition/EditorShellAssetBuilder.h" #include "Scene/EditorSceneRuntime.h" -#include "State/EditorSelectionStamp.h" #include "Composition/EditorPanelIds.h" @@ -53,11 +52,18 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { m_session = {}; m_session.repoRoot = repoRoot; m_session.projectRoot = (repoRoot / "project").lexically_normal(); + m_commandFocusService = {}; + m_selectionService = {}; m_projectRuntime = {}; m_projectRuntime.Initialize(repoRoot); + m_projectRuntime.BindSelectionService(&m_selectionService); m_sceneRuntime = {}; m_sceneRuntime.Initialize(m_session.projectRoot); + m_sceneRuntime.BindSelectionService(&m_selectionService); + SyncSessionFromSelectionService(); m_hostCommandBridge.BindSession(m_session); + m_hostCommandBridge.BindCommandFocusService(m_commandFocusService); + SyncSessionFromCommandFocusService(); m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge); m_shellServices = {}; @@ -91,6 +97,7 @@ void EditorContext::SetExitRequestHandler(std::function handler) { void EditorContext::SyncSessionFromWorkspace( const UIEditorWorkspaceController& workspaceController) { SyncEditorSessionFromWorkspace(m_session, workspaceController); + SyncSessionFromCommandFocusService(); } bool EditorContext::IsValid() const { @@ -109,6 +116,14 @@ const EditorSession& EditorContext::GetSession() const { return m_session; } +EditorCommandFocusService& EditorContext::GetCommandFocusService() { + return m_commandFocusService; +} + +const EditorCommandFocusService& EditorContext::GetCommandFocusService() const { + return m_commandFocusService; +} + EditorProjectRuntime& EditorContext::GetProjectRuntime() { return m_projectRuntime; } @@ -126,17 +141,22 @@ const EditorSceneRuntime& EditorContext::GetSceneRuntime() const { } void EditorContext::SetSelection(EditorSelectionState selection) { - selection.stamp = GenerateEditorSelectionStamp(); - m_session.selection = std::move(selection); + m_selectionService.SetSelection(std::move(selection)); + SyncSessionFromSelectionService(); } void EditorContext::ClearSelection() { - m_session.selection = {}; - m_session.selection.stamp = GenerateEditorSelectionStamp(); + m_selectionService.ClearSelection(); + SyncSessionFromSelectionService(); } -void EditorContext::SyncSessionFromProjectRuntime() { - m_session.selection = m_projectRuntime.GetSelection(); +void EditorContext::SyncSessionFromSelectionService() { + m_session.selection = m_selectionService.GetSelection(); +} + +void EditorContext::SyncSessionFromCommandFocusService() { + m_session.activeRoute = m_commandFocusService.ResolveRoute( + ResolveEditorActionRoute(m_session.activePanelId)); } UIEditorWorkspaceController EditorContext::BuildWorkspaceController() const { diff --git a/new_editor/app/State/EditorContext.h b/new_editor/app/State/EditorContext.h index ff35605f..d6aa9b1c 100644 --- a/new_editor/app/State/EditorContext.h +++ b/new_editor/app/State/EditorContext.h @@ -5,6 +5,8 @@ #include "Scene/EditorSceneRuntime.h" #include "Commands/EditorHostCommandBridge.h" +#include "State/EditorCommandFocusService.h" +#include "State/EditorSelectionService.h" #include "State/EditorSession.h" #include #include @@ -36,13 +38,16 @@ public: const std::string& GetValidationMessage() const; const EditorShellAsset& GetShellAsset() const; const EditorSession& GetSession() const; + EditorCommandFocusService& GetCommandFocusService(); + const EditorCommandFocusService& GetCommandFocusService() const; EditorProjectRuntime& GetProjectRuntime(); const EditorProjectRuntime& GetProjectRuntime() const; EditorSceneRuntime& GetSceneRuntime(); const EditorSceneRuntime& GetSceneRuntime() const; void SetSelection(EditorSelectionState selection); void ClearSelection(); - void SyncSessionFromProjectRuntime(); + void SyncSessionFromSelectionService(); + void SyncSessionFromCommandFocusService(); UIEditorWorkspaceController BuildWorkspaceController() const; const UIEditorShellInteractionServices& GetShellServices() const; @@ -69,6 +74,8 @@ private: UIEditorShortcutManager m_shortcutManager = {}; UIEditorShellInteractionServices m_shellServices = {}; EditorSession m_session = {}; + EditorCommandFocusService m_commandFocusService = {}; + EditorSelectionService m_selectionService = {}; EditorProjectRuntime m_projectRuntime = {}; EditorSceneRuntime m_sceneRuntime = {}; EditorHostCommandBridge m_hostCommandBridge = {}; diff --git a/new_editor/app/State/EditorSelectionService.h b/new_editor/app/State/EditorSelectionService.h new file mode 100644 index 00000000..e7ddd0ae --- /dev/null +++ b/new_editor/app/State/EditorSelectionService.h @@ -0,0 +1,111 @@ +#pragma once + +#include "State/EditorSelectionStamp.h" +#include "State/EditorSession.h" + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorSelectionService { +public: + const EditorSelectionState& GetSelection() const { + return m_selection; + } + + bool HasSelection() const { + return m_selection.kind != EditorSelectionKind::None && + !m_selection.itemId.empty(); + } + + bool HasSelectionKind(EditorSelectionKind kind) const { + return m_selection.kind == kind && HasSelection(); + } + + std::uint64_t GetStamp() const { + return m_selection.stamp; + } + + bool SetProjectSelection( + std::string_view itemId, + std::string_view displayName, + const std::filesystem::path& absolutePath, + bool directory) { + EditorSelectionState selection = {}; + selection.kind = EditorSelectionKind::ProjectItem; + selection.itemId = std::string(itemId); + selection.displayName = std::string(displayName); + selection.absolutePath = absolutePath; + selection.directory = directory; + const bool changed = + m_selection.kind != selection.kind || + m_selection.itemId != selection.itemId || + m_selection.absolutePath != selection.absolutePath || + m_selection.directory != selection.directory; + ApplySelection(std::move(selection), changed); + return changed; + } + + bool SetHierarchySelection( + std::string_view itemId, + std::string_view displayName = {}) { + EditorSelectionState selection = {}; + selection.kind = EditorSelectionKind::HierarchyNode; + selection.itemId = std::string(itemId); + selection.displayName = std::string(displayName); + const bool changed = + m_selection.kind != selection.kind || + m_selection.itemId != selection.itemId; + ApplySelection(std::move(selection), changed); + return changed; + } + + void SetSelection(EditorSelectionState selection) { + bool changed = true; + switch (selection.kind) { + case EditorSelectionKind::ProjectItem: + changed = + m_selection.kind != selection.kind || + m_selection.itemId != selection.itemId || + m_selection.absolutePath != selection.absolutePath || + m_selection.directory != selection.directory; + break; + + case EditorSelectionKind::HierarchyNode: + changed = + m_selection.kind != selection.kind || + m_selection.itemId != selection.itemId; + break; + + case EditorSelectionKind::None: + default: + ClearSelection(); + return; + } + + ApplySelection(std::move(selection), changed); + } + + void ClearSelection() { + m_selection = {}; + m_selection.stamp = GenerateEditorSelectionStamp(); + } + +private: + void ApplySelection( + EditorSelectionState selection, + bool changed) { + const std::uint64_t previousStamp = m_selection.stamp; + selection.stamp = changed + ? GenerateEditorSelectionStamp() + : (previousStamp != 0u ? previousStamp : GenerateEditorSelectionStamp()); + m_selection = std::move(selection); + } + + EditorSelectionState m_selection = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/src/App/EditorHostCommandBridge.cpp b/new_editor/src/App/EditorHostCommandBridge.cpp index 4c95b802..30c07a1a 100644 --- a/new_editor/src/App/EditorHostCommandBridge.cpp +++ b/new_editor/src/App/EditorHostCommandBridge.cpp @@ -9,6 +9,11 @@ void EditorHostCommandBridge::BindSession(EditorSession& session) { m_session = &session; } +void EditorHostCommandBridge::BindCommandFocusService( + const EditorCommandFocusService& commandFocusService) { + m_commandFocusService = &commandFocusService; +} + void EditorHostCommandBridge::BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, EditorEditCommandRoute* projectRoute, @@ -130,13 +135,14 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateEditCommand return BuildDisabledResult("Editor session is unavailable."); } - if (m_session->activeRoute == EditorActionRoute::None) { + const EditorActionRoute route = ResolveActiveEditRoute(); + if (route == EditorActionRoute::None) { return BuildDisabledResult("No active edit route."); } - EditorEditCommandRoute* route = ResolveEditCommandRoute(m_session->activeRoute); - if (route == nullptr) { - switch (m_session->activeRoute) { + EditorEditCommandRoute* editRoute = ResolveEditCommandRoute(route); + if (editRoute == nullptr) { + switch (route) { case EditorActionRoute::Hierarchy: return BuildDisabledResult("Hierarchy command route is unavailable in the current window."); case EditorActionRoute::Project: @@ -155,7 +161,7 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateEditCommand } } - return route->EvaluateEditCommand(commandId); + return editRoute->EvaluateEditCommand(commandId); } UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchEditCommand( @@ -167,8 +173,7 @@ UIEditorHostCommandDispatchResult EditorHostCommandBridge::DispatchEditCommand( return result; } - EditorEditCommandRoute* route = - ResolveEditCommandRoute(m_session != nullptr ? m_session->activeRoute : EditorActionRoute::None); + EditorEditCommandRoute* route = ResolveEditCommandRoute(ResolveActiveEditRoute()); if (route == nullptr) { result.message = "Edit command route is unavailable."; return result; @@ -215,6 +220,22 @@ UIEditorHostCommandEvaluationResult EditorHostCommandBridge::EvaluateUnsupported return BuildDisabledResult("Host command has no owner in the current shell."); } +EditorActionRoute EditorHostCommandBridge::ResolveActiveEditRoute() const { + if (m_session == nullptr) { + return EditorActionRoute::None; + } + + const EditorActionRoute fallbackRoute = + ResolveEditorActionRoute(m_session->activePanelId); + if (m_commandFocusService != nullptr) { + return m_commandFocusService->ResolveRoute(fallbackRoute); + } + + return m_session->activeRoute != EditorActionRoute::None + ? m_session->activeRoute + : fallbackRoute; +} + EditorEditCommandRoute* EditorHostCommandBridge::ResolveEditCommandRoute( EditorActionRoute route) const { switch (route) { diff --git a/new_editor/src/App/EditorSession.cpp b/new_editor/src/App/EditorSession.cpp index 080e976a..712bb2cc 100644 --- a/new_editor/src/App/EditorSession.cpp +++ b/new_editor/src/App/EditorSession.cpp @@ -77,7 +77,6 @@ void SyncEditorSessionFromWorkspace( EditorSession& session, const UIEditorWorkspaceController& controller) { session.activePanelId = controller.GetWorkspace().activePanelId; - session.activeRoute = ResolveEditorActionRoute(session.activePanelId); } } // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 79144eae..c54abfbd 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -118,6 +118,7 @@ if(TARGET XCUIEditorAppLib) list(APPEND EDITOR_APP_FEATURE_TEST_SOURCES test_editor_host_command_bridge.cpp test_editor_shell_asset_validation.cpp + test_editor_window_workspace_store.cpp test_structured_editor_shell.cpp test_editor_window_input_routing.cpp test_ui_editor_panel_registry.cpp 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 d5a5ed99..e5939507 100644 --- a/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp +++ b/tests/UI/Editor/unit/test_editor_host_command_bridge.cpp @@ -1,12 +1,15 @@ #include +#include "Composition/EditorPanelIds.h" #include "Commands/EditorEditCommandRoute.h" #include "Commands/EditorHostCommandBridge.h" +#include "State/EditorCommandFocusService.h" #include "State/EditorSession.h" namespace { using XCEngine::UI::Editor::App::EditorActionRoute; +using XCEngine::UI::Editor::App::EditorCommandFocusService; using XCEngine::UI::Editor::App::EditorEditCommandRoute; using XCEngine::UI::Editor::App::EditorHostCommandBridge; using XCEngine::UI::Editor::App::EditorSession; @@ -51,7 +54,8 @@ public: TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { EditorSession session = {}; - session.activeRoute = EditorActionRoute::Hierarchy; + EditorCommandFocusService commandFocus = {}; + commandFocus.ClaimFocus(EditorActionRoute::Hierarchy); StubEditCommandRoute hierarchyRoute = {}; hierarchyRoute.evaluationResult.executable = true; @@ -61,6 +65,7 @@ TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { EditorHostCommandBridge bridge = {}; bridge.BindSession(session); + bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr); const UIEditorHostCommandEvaluationResult evaluation = @@ -78,7 +83,6 @@ TEST(EditorHostCommandBridgeTest, HierarchyEditCommandsDelegateToBoundRoute) { TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { EditorSession session = {}; - session.activeRoute = EditorActionRoute::None; EditorHostCommandBridge bridge = {}; bridge.BindSession(session); @@ -100,7 +104,6 @@ TEST(EditorHostCommandBridgeTest, UnsupportedHostCommandsUseHonestMessages) { TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { EditorSession session = {}; - session.activeRoute = EditorActionRoute::Hierarchy; StubEditCommandRoute projectRoute = {}; projectRoute.assetEvaluationResult.executable = true; @@ -127,7 +130,8 @@ TEST(EditorHostCommandBridgeTest, AssetCommandsDelegateToProjectRoute) { TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { EditorSession session = {}; - session.activeRoute = EditorActionRoute::Scene; + EditorCommandFocusService commandFocus = {}; + commandFocus.ClaimFocus(EditorActionRoute::Scene); StubEditCommandRoute sceneRoute = {}; sceneRoute.evaluationResult.executable = true; @@ -137,6 +141,7 @@ TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { EditorHostCommandBridge bridge = {}; bridge.BindSession(session); + bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(nullptr, nullptr, &sceneRoute); const UIEditorHostCommandEvaluationResult evaluation = @@ -154,7 +159,8 @@ TEST(EditorHostCommandBridgeTest, SceneEditCommandsDelegateToBoundSceneRoute) { TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorRoute) { EditorSession session = {}; - session.activeRoute = EditorActionRoute::Inspector; + EditorCommandFocusService commandFocus = {}; + commandFocus.ClaimFocus(EditorActionRoute::Inspector); StubEditCommandRoute inspectorRoute = {}; inspectorRoute.evaluationResult.executable = true; @@ -164,6 +170,7 @@ TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorR EditorHostCommandBridge bridge = {}; bridge.BindSession(session); + bridge.BindCommandFocusService(commandFocus); bridge.BindEditCommandRoutes(nullptr, nullptr, nullptr, &inspectorRoute); const UIEditorHostCommandEvaluationResult evaluation = @@ -179,4 +186,50 @@ TEST(EditorHostCommandBridgeTest, InspectorEditCommandsDelegateToBoundInspectorR EXPECT_EQ(inspectorRoute.lastDispatchedCommandId, "edit.delete"); } +TEST(EditorHostCommandBridgeTest, ActivePanelRouteIsUsedAsFallbackWhenNoExplicitCommandFocusExists) { + EditorSession session = {}; + session.activePanelId = XCEngine::UI::Editor::App::kHierarchyPanelId; + + StubEditCommandRoute hierarchyRoute = {}; + hierarchyRoute.evaluationResult.executable = true; + hierarchyRoute.evaluationResult.message = "Hierarchy route owns rename."; + + EditorHostCommandBridge bridge = {}; + bridge.BindSession(session); + bridge.BindEditCommandRoutes(&hierarchyRoute, nullptr, nullptr); + + const UIEditorHostCommandEvaluationResult evaluation = + bridge.EvaluateHostCommand("edit.rename"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Hierarchy route owns rename."); +} + +TEST(EditorHostCommandBridgeTest, ExplicitCommandFocusOverridesActivePanelFallback) { + EditorSession session = {}; + session.activePanelId = XCEngine::UI::Editor::App::kProjectPanelId; + + EditorCommandFocusService commandFocus = {}; + commandFocus.ClaimFocus(EditorActionRoute::Scene); + + StubEditCommandRoute projectRoute = {}; + projectRoute.evaluationResult.executable = true; + projectRoute.evaluationResult.message = "Project route."; + + StubEditCommandRoute sceneRoute = {}; + sceneRoute.evaluationResult.executable = true; + sceneRoute.evaluationResult.message = "Scene route."; + + EditorHostCommandBridge bridge = {}; + bridge.BindSession(session); + bridge.BindCommandFocusService(commandFocus); + bridge.BindEditCommandRoutes(nullptr, &projectRoute, &sceneRoute); + + const UIEditorHostCommandEvaluationResult evaluation = + bridge.EvaluateHostCommand("edit.undo"); + EXPECT_TRUE(evaluation.executable); + EXPECT_EQ(evaluation.message, "Scene route."); + EXPECT_EQ(sceneRoute.lastEvaluatedCommandId, "edit.undo"); + EXPECT_TRUE(projectRoute.lastEvaluatedCommandId.empty()); +} + } // namespace diff --git a/tests/UI/Editor/unit/test_editor_project_runtime.cpp b/tests/UI/Editor/unit/test_editor_project_runtime.cpp index f9a32990..fd01840d 100644 --- a/tests/UI/Editor/unit/test_editor_project_runtime.cpp +++ b/tests/UI/Editor/unit/test_editor_project_runtime.cpp @@ -1,4 +1,5 @@ #include "Project/EditorProjectRuntime.h" +#include "State/EditorSelectionService.h" #include @@ -168,5 +169,26 @@ TEST(EditorProjectRuntimeTests, RenameSelectedItemRemapsSelectionAndDeleteClears EXPECT_EQ(runtime.GetSelection().kind, EditorSelectionKind::None); } +TEST(EditorProjectRuntimeTests, BoundSelectionServiceBecomesTheSingleProjectSelectionSource) { + TemporaryRepo repo = {}; + ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scripts")); + ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs")); + + EditorSelectionService selectionService = {}; + EditorProjectRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(repo.Root())); + runtime.BindSelectionService(&selectionService); + ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts")); + + ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs")); + EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::ProjectItem); + EXPECT_EQ(selectionService.GetSelection().itemId, "Assets/Scripts/Player.cs"); + EXPECT_EQ(runtime.GetSelection().itemId, "Assets/Scripts/Player.cs"); + + runtime.ClearSelection(); + EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::None); + EXPECT_FALSE(runtime.HasSelection()); +} + } // namespace } // namespace XCEngine::UI::Editor::App diff --git a/tests/UI/Editor/unit/test_editor_window_workspace_store.cpp b/tests/UI/Editor/unit/test_editor_window_workspace_store.cpp new file mode 100644 index 00000000..2622c51f --- /dev/null +++ b/tests/UI/Editor/unit/test_editor_window_workspace_store.cpp @@ -0,0 +1,160 @@ +#include + +#include "Platform/Win32/WindowManager/EditorWindowWorkspaceStore.h" + +#include +#include +#include + +namespace { + +using XCEngine::UI::Editor::App::Internal::EditorWindowWorkspaceStore; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; +using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel; +using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus; +using XCEngine::UI::Editor::UIEditorWorkspaceCommand; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", {}, true, true, true }, + { "doc-b", "Document B", {}, true, true, true }, + { "inspector", "Inspector", {}, true, true, true }, + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root-split", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.7f, + BuildUIEditorWorkspaceTabStack( + "document-tabs", + { + BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true), + BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true), + }, + 0u), + BuildUIEditorWorkspaceSingleTabStack( + "inspector-panel", + "inspector", + "Inspector", + true)); + workspace.activePanelId = "doc-a"; + return workspace; +} + +} // namespace + +TEST(EditorWindowWorkspaceStoreTest, RegistersPrimaryWindowProjectionAsAuthoritativeState) { + EditorWindowWorkspaceStore store(BuildPanelRegistry()); + auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + + ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error)) + << error; + ASSERT_TRUE(store.HasState()); + ASSERT_EQ(store.GetWindowSet().primaryWindowId, "main"); + ASSERT_EQ(store.GetWindowSet().activeWindowId, "main"); + ASSERT_EQ(store.GetWindowSet().windows.size(), 1u); + + const auto* mainWindow = FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "main"); + ASSERT_NE(mainWindow, nullptr); + EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-a"); + EXPECT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b")); +} + +TEST(EditorWindowWorkspaceStoreTest, CommitsWindowLocalProjectionBackIntoCentralStore) { + EditorWindowWorkspaceStore store(BuildPanelRegistry()); + auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error)) + << error; + + const UIEditorWorkspaceCommandResult activateResult = workspaceController.Dispatch( + UIEditorWorkspaceCommand{ + UIEditorWorkspaceCommandKind::ActivatePanel, + "doc-b", + }); + ASSERT_EQ(activateResult.status, UIEditorWorkspaceCommandStatus::Changed); + + ASSERT_TRUE(store.CommitWindowProjection("main", workspaceController, error)) + << error; + + const auto* mainWindow = FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "main"); + ASSERT_NE(mainWindow, nullptr); + EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-b"); +} + +TEST(EditorWindowWorkspaceStoreTest, AppliesCrossWindowMutationAgainstCentralStore) { + EditorWindowWorkspaceStore store(BuildPanelRegistry()); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error)) + << error; + + auto mutationController = store.BuildMutationController(); + const auto detachResult = mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window"); + ASSERT_EQ(detachResult.status, UIEditorWindowWorkspaceOperationStatus::Changed); + ASSERT_TRUE(store.ValidateWindowSet(mutationController.GetWindowSet(), error)) + << error; + store.ReplaceWindowSet(mutationController.GetWindowSet()); + + ASSERT_EQ(store.GetWindowSet().windows.size(), 2u); + EXPECT_EQ(store.GetWindowSet().activeWindowId, "doc-b-window"); + + const auto* detachedWindow = + FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "doc-b-window"); + ASSERT_NE(detachedWindow, nullptr); + EXPECT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b")); +} + +TEST(EditorWindowWorkspaceStoreTest, RemovingDetachedWindowRepairsActiveWindowReference) { + EditorWindowWorkspaceStore store(BuildPanelRegistry()); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + std::string error = {}; + ASSERT_TRUE(store.RegisterWindowProjection("main", true, workspaceController, error)) + << error; + + auto mutationController = store.BuildMutationController(); + ASSERT_EQ( + mutationController.DetachPanelToNewWindow( + "main", + "document-tabs", + "doc-b", + "doc-b-window").status, + UIEditorWindowWorkspaceOperationStatus::Changed); + ASSERT_TRUE(store.ValidateWindowSet(mutationController.GetWindowSet(), error)) + << error; + store.ReplaceWindowSet(mutationController.GetWindowSet()); + + store.RemoveWindow("doc-b-window", false); + + EXPECT_EQ(store.GetWindowSet().windows.size(), 1u); + EXPECT_EQ(store.GetWindowSet().primaryWindowId, "main"); + EXPECT_EQ(store.GetWindowSet().activeWindowId, "main"); + EXPECT_EQ( + FindUIEditorWindowWorkspaceState(store.GetWindowSet(), "doc-b-window"), + nullptr); +} diff --git a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp index fc6b616d..88fda852 100644 --- a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp +++ b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp @@ -2,7 +2,7 @@ #include "Features/Scene/SceneViewportController.h" #include "Features/Inspector/InspectorSubject.h" #include "Rendering/Viewport/ViewportHostService.h" -#include "State/EditorSelectionStamp.h" +#include "State/EditorSelectionService.h" #include "Composition/EditorPanelIds.h" #include @@ -399,30 +399,35 @@ TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) { EXPECT_GT(runtime.GetSelectionStamp(), clearedStamp); } -TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsLatestSelectionDomain) { +TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsUnifiedSelectionSnapshot) { ScopedSceneManagerReset reset = {}; TemporaryProjectRoot projectRoot = {}; SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + EditorSelectionService selectionService = {}; EditorSceneRuntime runtime = {}; ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + runtime.BindSelectionService(&selectionService); runtime.EnsureSceneSelection(); ASSERT_TRUE(runtime.HasSceneSelection()); + + EditorSession session = {}; + session.selection = selectionService.GetSelection(); EXPECT_EQ( - ResolveInspectorSelectionSource(EditorSession{}, runtime), + ResolveInspectorSelectionSource(session, runtime), InspectorSelectionSource::Scene); const InspectorSubject sceneSubject = - BuildInspectorSubject(EditorSession{}, runtime); + BuildInspectorSubject(session, runtime); EXPECT_EQ(sceneSubject.kind, InspectorSubjectKind::SceneObject); EXPECT_EQ(sceneSubject.source, InspectorSelectionSource::Scene); EXPECT_EQ(sceneSubject.sceneObject.displayName, "Target"); - EditorSession session = {}; - session.selection.kind = EditorSelectionKind::ProjectItem; - session.selection.itemId = "asset:scene"; - session.selection.displayName = "Main"; - session.selection.absolutePath = projectRoot.MainScenePath(); - session.selection.stamp = GenerateEditorSelectionStamp(); + selectionService.SetProjectSelection( + "asset:scene", + "Main", + projectRoot.MainScenePath(), + false); + session.selection = selectionService.GetSelection(); EXPECT_EQ( ResolveInspectorSelectionSource(session, runtime), InspectorSelectionSource::Project); @@ -432,24 +437,34 @@ TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsLatestSelection EXPECT_EQ(projectSubject.source, InspectorSelectionSource::Project); EXPECT_EQ(projectSubject.projectAsset.selection.itemId, "asset:scene"); + runtime.EnsureSceneSelection(); + session.selection = selectionService.GetSelection(); + EXPECT_EQ( + ResolveInspectorSelectionSource(session, runtime), + InspectorSelectionSource::Project); + EXPECT_EQ( + BuildInspectorSubject(session, runtime).kind, + InspectorSubjectKind::ProjectAsset); + runtime.ClearSelection(); + session.selection = selectionService.GetSelection(); EXPECT_EQ( ResolveInspectorSelectionSource(session, runtime), InspectorSelectionSource::None); - EXPECT_EQ( - BuildInspectorSubject(session, runtime).kind, - InspectorSubjectKind::None); - runtime.EnsureSceneSelection(); + session.selection = selectionService.GetSelection(); EXPECT_EQ( ResolveInspectorSelectionSource(session, runtime), InspectorSelectionSource::Scene); - session.selection = {}; - session.selection.stamp = GenerateEditorSelectionStamp(); - EXPECT_EQ( - ResolveInspectorSelectionSource(session, runtime), - InspectorSelectionSource::None); + selectionService.SetProjectSelection( + "asset:scene", + "Main", + projectRoot.MainScenePath(), + false); + runtime.EnsureSceneSelection(); + EXPECT_EQ(selectionService.GetSelection().kind, EditorSelectionKind::ProjectItem); + EXPECT_EQ(selectionService.GetSelection().itemId, "asset:scene"); } TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportController) {