diff --git a/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md b/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md new file mode 100644 index 00000000..6dd72739 --- /dev/null +++ b/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md @@ -0,0 +1,814 @@ +# XCUI NewEditor 收口重构计划 +日期: `2026-04-17` + +## 当前执行状态(更新于 `2026-04-17`) + +### 总体判断 + +当前计划**未完成**,但已经完成了一批高价值收口项,状态更准确地说是: + +- `Phase 0`:基本完成 +- `Phase 1`:部分完成 +- `Phase 2`:部分完成 +- `Phase 3`:部分完成 +- `Phase 4`:部分完成 +- `Phase 5`:基本完成 +- `Phase 6`:大体完成但未完全收尾 +- `Phase 7`:未完成 + +### 已完成事项 + +1. `workspace` 关键校验已补齐一项核心缺口: + - 已补 `duplicate nodeId` 校验 + - 已补对应单元测试 + +2. 历史测试残留已清理: + - `tests/NewEditor` 已删除 + - 失效的 `XCNewEditorLib` 测试入口已退出工作树 + +3. 测试边界已有明显改善: + - `tests/UI/Editor` 不再直接编译 `new_editor/app` 私有 `.cpp` + - `EditorHostCommandBridge / EditorSession / EditorEditCommandRoute` 已提升为正式公共 app-level API + +4. shell 语义命名已收口: + - foundation shell 与 application shell 已分开命名 + - 旧的 `default shell` 歧义已明显降低 + +5. panel 单一事实源已有第一步落地: + - 核心 panel id 已集中到统一常量入口 + - `workspace / menu / command / route / viewport` 的一部分散落字符串已收敛 + +6. `PanelRegistry` 已从纯元数据表前进到“带装配信息的注册表”: + - 已承载 viewport 默认 shell 配置 + - `PanelContentHost` 已直接从 registry 派生外部 host 装配,不再依赖额外 bindings 列表 + +7. target graph 已完成第一步收口: + - `XCUIEditorAppLib` 已从 `XCUIEditorApp` 可执行目标中拆出 + - app 业务层不再全部内联在 exe target 中 + +8. `window-workspace` 全局约束已再补一刀: + - 已补跨窗口 `panelId` 唯一性校验 + - 已补对应窗口工作区单元测试 + +### 当前仍未完成的关键缺口 + +1. `Milestone A` 未完成: + - `XCUIEditorLib` 仍然 `PUBLIC` 依赖整个 `XCEngine` + - UI `Core / Runtime / Editor / App` 还未真正拆成清晰 target graph + +2. `Milestone B` 未完成: + - `PanelRegistry` 仍未成为完整的 runtime factory / lifecycle 入口 + - `EditorShellRuntime` 仍手工持有具体 panel runtime 实例 + - `window-workspace / cross-window / transfer` 仍有剩余一致性约束未补齐,当前已补到跨窗口 `panelId` 唯一性 + +3. `Milestone C` 未完成: + - `XCUIEditorLib / XCUIEditorHost / XCUIEditorApp` 等 target 命名尚未统一收口 + - 文档体系尚未全面同步当前结构 + +### 最近一次已验证通过 + +- `cmake --build . --config Debug --target editor_ui_tests` +- `cmake --build . --config Debug --target XCUIEditorApp` +- `.\tests\UI\Editor\unit\Debug\editor_ui_tests.exe --gtest_filter=UIEditorWindowWorkspaceControllerTest.*` +- `.\tests\UI\Editor\unit\Debug\editor_ui_tests.exe` +- 多组 `editor_ui_tests` 定向用例通过: + - `UIEditorWorkspaceModelTest.*` + - `EditorHostCommandBridgeTest.*` + - `EditorShellAssetValidationTest.*` + - `EditorUIStructuredShellTest.*` + - `UIEditorPanelRegistryTest.*` + - `UIEditorPanelContentHostTest.*` + - `UIEditorWorkspaceComposeTest.*` + - `UIEditorWindowWorkspaceControllerTest.*` +- 全量 `editor_ui_tests` 已通过:`337` 个测试全部通过 + +### 接下来优先级 + +为尽快收口,后续优先顺序调整为: + +1. 优先继续压缩 `XCUIEditorLib` 的对外依赖面 +2. 继续补齐 `window-workspace / transfer` 剩余不变式和状态机约束 +3. 最后统一 target/public API 命名和文档口径 + +## 1. 文档定位 + +这份文档是针对当前 `new_editor/` 主线的一次正式收口计划。 + +它不讨论“要不要继续做 XCUI 新编辑器”,而是默认这条线继续推进,并专门解决当前已经暴露出的以下问题: + +- UI `Core / Runtime / Editor` 三层只停留在目录语义,缺少真实构建边界 +- `new_editor` 公共库层与 `app` 宿主层边界开始混杂 +- panel id、workspace、menu、command、route 等关键标识分散硬编码 +- panel registry 只是元数据表,不是可扩展运行时入口 +- workspace/window-workspace 不变量不完整,校验与运行时主键不一致 +- `default shell` 与真实应用 shell 已经分叉 +- 测试入口、测试目录与目标命名存在历史残留 +- 命名体系没有收口,长期沟通成本偏高 + +这份计划的目标是把 `new_editor` 从“已经能工作的一套实现”收成“边界稳定、可继续扩展、可维护、可验证的正式主线”。 + +## 2. 当前问题总览 + +### 2.1 三层分层没有真正落到 target graph + +当前 UI 三层在概念上已经存在,但依赖图上并没有被真正约束。 + +现状: + +- `engine/CMakeLists.txt` 中 UI Core/Runtime 只是 `XCEngine` 大静态库中的一部分 +- `new_editor/CMakeLists.txt` 中 `XCUIEditorLib` 直接 `PUBLIC` 依赖整个 `XCEngine` +- Editor 层没有被限制只能依赖 UI Core/Runtime 的最小子集 + +结果: + +- 分层更多靠约定,不靠构建系统保证 +- Editor 层未来极易直接吃到 engine 里任意模块 +- 后续如果要拆库、裁剪、做更细的测试边界,代价会越来越高 + +### 2.2 面板系统是 stringly-typed,单一事实源缺失 + +当前 `hierarchy / scene / game / inspector / console / project` 这些 panel id 分散存在于多处: + +- panel registry +- workspace 默认布局 +- shell presentation +- menu checked state +- command registry +- active route 映射 +- viewport 特判逻辑 + +结果: + +- 新增一个 panel 需要同时修改多处 +- 某一处漏改不会立刻在编译期暴露 +- 系统长期演进后极易出现名称漂移和行为不一致 + +### 2.3 PanelRegistry 不是正式扩展点 + +当前 registry 只描述: + +- `panelId` +- `defaultTitle` +- `presentationKind` +- `canHide / canClose` + +但真正的运行时对象和生命周期仍然由 `EditorShellRuntime` 显式持有和手工更新: + +- `HierarchyPanel` +- `ProjectPanel` +- `InspectorPanel` +- `ConsolePanel` +- 视口相关宿主对象 + +结果: + +- registry 和真实运行时是两套系统 +- 想做“注册一个面板即可接入”的能力时,现有结构撑不住 +- detached window、多 panel host、后续插件化面板都缺少干净入口 + +### 2.4 Workspace / WindowWorkspace 不变量不完整 + +当前 workspace 校验存在关键缺口: + +- 校验 panel id 唯一 +- 但没有校验 `nodeId` 唯一 + +而运行时很多操作都把 `nodeId` 当主键: + +- 查找 node +- 删除 node +- tab 拖拽 +- dock/transfer +- cross-window drop + +结果: + +- 坏布局数据可能“通过校验但行为不稳定” +- 一旦 layout persistence 或跨窗口拖拽写入异常数据,调试会很困难 + +### 2.5 公共默认 shell 与真实 app shell 已经分叉 + +当前存在两套默认构造逻辑: + +- `BuildDefaultEditorShellAsset()`:只有一个 placeholder root +- `BuildEditorShellAsset()`:真实六面板应用 shell + +结果: + +- `default` 这个命名已经失真 +- 公共 API、测试、应用入口对“默认 shell”理解不一致 +- 后面继续扩展时容易把 demo/default/baseline/app shell 混在一起 + +### 2.6 测试入口与测试命名已经出现历史残留 + +当前测试结构存在明显不一致: + +- `tests/UI` 被当作正式 XCUI 入口 +- `tests/NewEditor` 仍然残留,但没有接入总测试树 +- `tests/NewEditor/CMakeLists.txt` 还引用不存在的 `XCNewEditorLib` +- 还有测试引用当前工作树中已经不存在的 `new_editor/ui/...` + +结果: + +- 新人很难判断哪组测试才是正式入口 +- 历史死分支会持续误导后续重构 +- CI 或本地构建规则很难收口 + +### 2.7 Editor UI 测试开始穿透到 app 私有实现 + +当前 `tests/UI/Editor/unit` 已经直接编译 `new_editor/app/...` 私有实现。 + +结果: + +- 库层与宿主层边界开始被测试反向打穿 +- 原本应该可复用的 `include + src` 层将越来越依赖 `app` +- 未来拆分模块、复用库、做轻量 host 都会受影响 + +### 2.8 命名体系未收口 + +当前命名并存: + +- `XCEditor` +- `XCUIEditorLib` +- `XCUIEditorApp` +- `XCNewEditorLib` 历史残留 +- `Old editor shell baseline loaded.` 这类旧文案 + +结果: + +- 概念层级不清 +- API、构建目标、测试目标之间映射困难 +- 文档与实现更容易继续分叉 + +## 3. 重构目标 + +本次收口的正式目标如下。 + +### 3.1 构建边界收口 + +把 UI 三层和宿主层边界真正落到 target graph: + +- `UI Core` +- `UI Runtime` +- `UI Editor` +- `Editor Host/App` + +至少做到: + +- 依赖方向清晰 +- 低层不依赖高层 +- `new_editor/include + src` 不默认暴露整个 `XCEngine` + +### 3.2 单一事实源收口 + +把 panel、workspace、menu、command、route 的关键事实源统一起来,减少分散硬编码。 + +最终目标: + +- 新增 panel 时,有且仅有一处主要注册入口 +- 其余信息从注册结果派生 + +### 3.3 运行时扩展点收口 + +让 `PanelRegistry` 从描述表升级成真正的运行时接入点,逐步承载: + +- panel descriptor +- panel presentation kind +- panel host binding +- panel factory / lifecycle +- panel capability metadata + +### 3.4 状态模型收口 + +为 workspace、window-workspace、layout persistence 补齐完整不变量: + +- `panelId` 唯一 +- `nodeId` 唯一 +- `windowId` 唯一 +- transfer 前后 session/workspace 一致性可验证 +- 布局读写格式与运行时约束一致 + +### 3.5 测试体系收口 + +明确: + +- 哪些测试验证 UI Core/Runtime/Editor 库层 +- 哪些测试验证 `new_editor/app` 宿主层 +- 哪些目录是正式入口 +- 哪些历史目录要删除或迁移 + +### 3.6 命名与文档收口 + +统一: + +- public API 命名 +- CMake target 命名 +- 测试目标命名 +- 文档中的主线术语 + +## 4. 非目标 + +这次重构不以以下事项为目标: + +- 不重写所有 widget 的视觉实现 +- 不替换 Win32/D3D12 宿主技术路线 +- 不把旧 `editor/` 直接删掉 +- 不在这一轮做插件市场级别的面板系统 +- 不在这一轮做完整多后端 UI runtime 切换 + +本次重点是收边界、收命名、收状态模型、收测试入口。 + +## 5. 总体原则 + +### 5.1 先收结构,再扩功能 + +本次优先级不是“继续堆新面板功能”,而是先让已有结构进入可维护状态。 + +### 5.2 先建真实边界,再谈目录美化 + +只有目录重排但不改变依赖图,不算收口完成。 + +### 5.3 先建立单一事实源,再减少重复代码 + +重复代码本身不是第一问题;更大的问题是重复定义同一个业务事实。 + +### 5.4 先收测试入口,再扩测试规模 + +在测试入口不清楚之前,继续加更多测试只会放大混乱。 + +### 5.5 以增量迁移替代一次性推翻 + +当前 `new_editor` 已经有大量实现和测试,必须分阶段迁移,避免“全量返工式重构”。 + +## 6. 分阶段实施计划 + +## Phase 0:基线冻结与问题建档 + +### 目标 + +先把当前正式主线、历史残留、迁移边界标清楚,避免后续边改边迷路。 + +### 任务 + +1. 建立 `new_editor` 当前模块清单: + - 公共库层 + - app 宿主层 + - Win32/D3D12 host 层 + - 测试层 + +2. 明确当前正式入口: + - 正式 public API 入口 + - 正式 app 入口 + - 正式测试入口 + +3. 列出必须迁移和必须删除的历史残留: + - `tests/NewEditor` + - `XCNewEditorLib` + - 已失效文档引用 + - 已不存在资源路径引用 + +### 产出 + +- 模块边界表 +- 历史残留清单 +- 迁移映射表 + +### 验收标准 + +- 团队可以用一页文档回答“哪个目录是正式入口、哪个不是” + +## Phase 1:构建图与模块边界收口 + +### 目标 + +把“概念分层”改成“真实 target 分层”。 + +### 任务 + +1. 从 `XCEngine` 中把 UI 相关内容拆成明确层级: + - `XCEngineUICore` + - `XCEngineUIRuntime` + - 如有必要,再提供兼容聚合 target + +2. 把 `new_editor/include + src` 整理成明确 editor 库层: + - 只依赖 UI Core/Runtime 和必要基础模块 + - 不默认对外公开整个 engine 大库 + +3. 明确 `new_editor/app` 仅作为宿主与应用组合层: + - 依赖 editor 库层 + - 依赖平台与渲染宿主 + - 不反向成为 editor 库层的隐式依赖 + +4. 对 `tests/UI/Editor` 和未来 app 测试分别绑定到正确 target + +### 涉及范围 + +- `engine/CMakeLists.txt` +- `new_editor/CMakeLists.txt` +- `tests/UI/**/CMakeLists.txt` + +### 风险 + +- target 拆分会先暴露出大量原本被大库掩盖的 include/依赖穿透 + +### 验收标准 + +- editor 公共库层可以不依赖整个 `XCEngine` 大 target +- app 层和库层的依赖方向可在 CMake 上直接读出来 + +## Phase 2:Panel 单一事实源收口 + +### 目标 + +把 panel 相关定义统一到一个正式注册入口。 + +### 任务 + +1. 建立统一的 panel schema/registration 源: + - `panelId` + - `defaultTitle` + - `presentationKind` + - `routeKind` + - `hostingKind` + - `menu visibility` + - `command exposure` + +2. 让以下内容从同一注册源派生,而不是各自手写: + - 默认 workspace + - shell presentation + - View 菜单 checked state + - activate/view commands + - active route 映射 + - viewport panel 分类 + +3. 消灭散落在各处的 panel id 字符串常量 + +### 涉及范围 + +- `new_editor/app/Composition/*` +- `new_editor/app/State/*` +- `new_editor/app/Composition/EditorShellRuntimeViewport.cpp` + +### 验收标准 + +- 新增一个 panel 时,核心业务定义只需要修改一处主注册入口 +- 其余生成逻辑只做派生,不重复声明事实 + +## Phase 3:PanelRegistry 升级为正式运行时扩展点 + +### 目标 + +让 panel registry 真正承载运行时接入,而不是只有元数据。 + +### 任务 + +1. 区分 panel descriptor 与 panel runtime binding: + - 描述信息 + - host capability + - factory/lifecycle + - externally hosted / viewport hosted / internal compose + +2. 逐步把 `EditorShellRuntime` 里硬编码持有的 panel 运行时对象迁移到统一注册/装配流程 + +3. 明确 panel lifecycle: + - create + - mount + - update + - append/render + - unmount + - destroy + +4. 把 `PanelContentHost` 从“仅同步 bounds”升级到“和注册系统协同工作的 host layer” + +### 涉及范围 + +- `new_editor/include/XCEditor/Panels/*` +- `new_editor/src/Panels/*` +- `new_editor/app/Composition/EditorShellRuntime*` +- `new_editor/app/Features/*` + +### 风险 + +- 一次性全迁移风险较大,建议先从 `Hierarchy`、`Project` 这类 HostedContent 面板开始 + +### 验收标准 + +- `EditorShellRuntime` 不再需要手工知道所有具体面板类型 +- registry 可以回答“这个 panel 如何创建、如何更新、如何被 host” + +## Phase 4:Workspace 与 WindowWorkspace 不变量补齐 + +### 目标 + +让 workspace 成为真正可靠的状态模型,而不是“多数情况下可工作”的树结构。 + +### 任务 + +1. 为 workspace validation 补充: + - duplicate `nodeId` + - invalid detached root shape + - invalid transfer result + - invalid active panel after mutation + +2. 为 window-workspace 补充跨窗口一致性校验: + - panel 是否在多个窗口重复出现 + - transfer 前后 session/state 是否完整 + - primary window/active window 与真实窗口集是否一致 + +3. 明确 layout persistence 协议与模型不变量一致: + - 读入后 canonicalize + - 校验失败时的回退策略 + - 跨版本兼容策略 + +4. 为 cross-window drag/drop 增加更明确的状态机约束 + +### 当前进展(更新于 `2026-04-17`) + +- 已完成 `workspace duplicate nodeId` 校验与单元测试 +- 已完成 `window-workspace` 跨窗口 `panelId` 重复校验与单元测试 +- 尚未完成 transfer 前后 session/state 完整性约束 +- 尚未完成 cross-window drag/drop 状态机约束收口 + +### 涉及范围 + +- `new_editor/include/XCEditor/Workspace/*` +- `new_editor/src/Workspace/*` +- `new_editor/app/Platform/Win32/WindowManager/*` + +### 验收标准 + +- 所有运行时依赖的主键都有校验 +- transfer/dock/move/detach 的结果状态可以被稳定验证 + +## Phase 5:Default Shell 与 App Shell 语义收口 + +### 目标 + +解决“默认 shell”命名与真实行为不一致的问题。 + +### 任务 + +1. 重新定义公共 API 语义: + - 哪个是 minimal/default baseline shell + - 哪个是 production app shell + - 哪个是 test fixture shell + +2. 重命名现有构造函数与测试用语,避免 `default` 误导 + +3. 让测试明确依赖: + - baseline shell fixture + - full editor shell fixture + - application shell build path + +### 涉及范围 + +- `new_editor/src/Shell/*` +- `new_editor/app/Composition/*` +- 相关测试 + +### 验收标准 + +- 外部调用者可以仅靠名称判断该 shell 构造函数的语义 +- 不再存在“默认只有 placeholder,但 app 默认是六面板”这种歧义 + +## Phase 6:测试体系与目录入口收口 + +### 目标 + +建立清晰的测试层级和单一入口。 + +### 任务 + +1. 明确保留与删除策略: + - `tests/UI/Core` + - `tests/UI/Runtime` + - `tests/UI/Editor` + - app/smoke/integration 单独归位 + +2. 清理 `tests/NewEditor`: + - 若继续保留,必须接入总测试树并修复目标名 + - 若不保留,迁移内容后删除目录 + +3. 杜绝 editor 库层测试直接编译 app 私有实现 + - 如果确有需要,应拆出正式可测接口或单独建立 app test target + +4. 修复失效资源路径与文档中的错误测试入口描述 + +### 涉及范围 + +- `tests/CMakeLists.txt` +- `tests/UI/**` +- `tests/NewEditor/**` +- 测试 README 与相关文档 + +### 验收标准 + +- 团队能明确说出“库层测试在哪,app 层测试在哪” +- 不再存在无法编译或未接入的历史测试死目录 + +## Phase 7:命名与文档收口 + +### 目标 + +统一命名,降低认知负担。 + +### 任务 + +1. 统一 public API、target、test target 的命名规则 + +建议方向: + +- `XCEditor*` 用于 editor 公共 API 与库层 +- `XCEditorHost*` 用于平台/渲染宿主层 +- `XCEditorApp*` 用于最终应用层 + +2. 清理历史残留命名: + - `XCUIEditorLib` + - `XCUIEditorApp` + - `XCNewEditorLib` + - 旧文案中的 `Old editor shell` + +3. 更新 README、测试入口文档、架构文档 + +### 验收标准 + +- 从 public header、CMake target、测试目标名可以直接看出层级 +- 文档不再引用失效 target 或失效目录 + +## 7. 推荐实施顺序 + +建议按以下顺序推进,而不是并行乱改。 + +### 第一批:先收结构骨架 + +1. Phase 0 +2. Phase 1 +3. Phase 2 + +原因: + +- 这三步决定后面所有代码应该往哪里落 +- 如果不先收这三步,后面 panel/runtime/test 清理会持续返工 + +### 第二批:再收状态模型 + +1. Phase 3 +2. Phase 4 +3. Phase 5 + +原因: + +- panel runtime 接口和 workspace 不变量必须一起收 +- 否则会出现“接口改了,但状态模型还是旧的”这种半重构状态 + +### 第三批:最后收测试与命名 + +1. Phase 6 +2. Phase 7 + +原因: + +- 测试入口和命名要依赖前面的结构调整结果 +- 太早改测试命名,后面还得再改一轮 + +## 8. 目录与文件层面的建议落点 + +这部分不是最终目录定案,而是建议的收口方向。 + +### 8.1 engine 层 + +建议逐步形成: + +- `engine/include/XCEngine/UI/Core/*` +- `engine/include/XCEngine/UI/Runtime/*` +- `engine/src/UI/Core/*` +- `engine/src/UI/Runtime/*` + +并在 CMake 上对应真实 target。 + +### 8.2 editor 公共库层 + +建议逐步形成: + +- `new_editor/include/XCEditor/Foundation/*` +- `new_editor/include/XCEditor/Workspace/*` +- `new_editor/include/XCEditor/Panels/*` +- `new_editor/include/XCEditor/Shell/*` +- `new_editor/include/XCEditor/Collections/*` +- `new_editor/include/XCEditor/Fields/*` +- `new_editor/include/XCEditor/Viewport/*` + +以及对应的 `new_editor/src/*` 实现。 + +### 8.3 app 宿主层 + +建议限定为: + +- `new_editor/app/Bootstrap/*` +- `new_editor/app/Platform/*` +- `new_editor/app/Rendering/*` +- `new_editor/app/Composition/*` +- `new_editor/app/Features/*` +- `new_editor/app/State/*` + +其中: + +- `Composition` 负责应用装配 +- `Features` 负责具体面板业务 +- `State` 负责 app session/context + +### 8.4 测试层 + +建议最终明确分成: + +- `tests/UI/Core` +- `tests/UI/Runtime` +- `tests/UI/Editor` +- `tests/App/Editor` 或 `tests/EditorApp` + +不要再保留语义不清的 `tests/NewEditor`。 + +## 9. 风险与应对 + +### 风险 1:边界拆分后暴露大量 include/依赖穿透 + +应对: + +- 分阶段拆 target +- 允许先引入过渡 target +- 每一阶段都先保证能编译,再继续下拆 + +### 风险 2:panel 系统迁移时功能回退 + +应对: + +- 先迁移 HostedContent 面板 +- 再迁移 viewport shell +- 最后迁移多窗口与 cross-window drag + +### 风险 3:测试在重构中大面积失效 + +应对: + +- 先保留现有有效测试 +- 对失效测试先停用/迁移,不要假装它们还有效 +- 建立明确的 fixture 层级 + +### 风险 4:命名修改带来大规模 churn + +应对: + +- 先完成结构收口再统一 rename +- 对外 API 可以短期保留兼容别名 +- 文档在 rename 同一阶段一次性更新 + +## 10. 阶段验收标准 + +### Milestone A:结构边界完成 + +满足以下条件即可判定通过: + +- UI Core/Runtime/Editor 的 target 边界已经建立 +- `new_editor` 公共库层不再默认依赖整个大 engine target +- app 层与库层依赖方向清晰 + +### Milestone B:panel 与 workspace 模型完成 + +满足以下条件即可判定通过: + +- panel 关键事实源统一 +- registry 成为正式运行时接入点 +- workspace/window-workspace 校验补齐主键与一致性约束 + +### Milestone C:测试与命名完成 + +满足以下条件即可判定通过: + +- 测试入口单一且自洽 +- 历史死目录已迁移或删除 +- 命名体系统一,文档同步完成 + +## 11. 建议的执行策略 + +建议执行方式如下: + +1. 先做一轮只改 CMake、命名清单、边界文档和最小适配层的“骨架提交” +2. 再做 panel schema 与 workspace 校验的“核心结构提交” +3. 再做 panel runtime/lifecycle 的“运行时迁移提交” +4. 最后做测试迁移、目录清理、命名统一的“收尾提交” + +不建议一次性大改所有目录和所有实现。 + +## 12. 结论 + +当前 `new_editor` 最大的问题不是“某个 widget 写得不够好”,而是: + +- 分层没有真正体现在依赖图上 +- panel 事实源不统一 +- registry 不是正式扩展点 +- workspace 不变量不完整 +- 测试与命名存在历史残留 + +因此这次收口的正确方向不是继续堆功能,而是按本文的阶段顺序先把结构收稳。 + +只有完成这一轮收口,`new_editor` 才适合作为未来正式编辑器主线继续扩展。 diff --git a/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceModel.h b/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceModel.h index 40c1b08c..e35441cc 100644 --- a/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceModel.h +++ b/new_editor/include/XCEditor/Workspace/UIEditorWindowWorkspaceModel.h @@ -27,6 +27,7 @@ enum class UIEditorWindowWorkspaceValidationCode : std::uint8_t { InvalidPanelRegistry, EmptyWindowId, DuplicateWindowId, + DuplicatePanelAcrossWindows, MissingPrimaryWindow, MissingActiveWindow, InvalidWorkspace, diff --git a/new_editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp b/new_editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp index a7a466e5..69802730 100644 --- a/new_editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp +++ b/new_editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -87,6 +88,7 @@ UIEditorWindowWorkspaceValidationResult ValidateUIEditorWindowWorkspaceSet( } std::unordered_set seenWindowIds = {}; + std::unordered_map panelOwners = {}; bool hasPrimaryWindow = false; bool hasActiveWindow = false; for (const UIEditorWindowWorkspaceState& state : windowSet.windows) { @@ -126,6 +128,17 @@ UIEditorWindowWorkspaceValidationResult ValidateUIEditorWindowWorkspaceSet( "Window '" + state.windowId + "' session invalid: " + sessionValidation.message); } + + for (const UIEditorPanelSessionState& panelState : state.session.panelStates) { + const auto [ownerIt, inserted] = + panelOwners.emplace(panelState.panelId, state.windowId); + if (!inserted) { + return MakeValidationError( + UIEditorWindowWorkspaceValidationCode::DuplicatePanelAcrossWindows, + "Panel '" + panelState.panelId + "' is present in both window '" + + ownerIt->second + "' and window '" + state.windowId + "'."); + } + } } if (!hasPrimaryWindow) { diff --git a/tests/UI/Editor/unit/test_ui_editor_window_workspace_controller.cpp b/tests/UI/Editor/unit/test_ui_editor_window_workspace_controller.cpp index 5c53dd65..1c61e4e2 100644 --- a/tests/UI/Editor/unit/test_ui_editor_window_workspace_controller.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_window_workspace_controller.cpp @@ -8,6 +8,8 @@ namespace { using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent; using XCEngine::UI::Editor::AreUIEditorWorkspaceSessionsEquivalent; using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceController; +using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceSet; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; @@ -20,10 +22,13 @@ using XCEngine::UI::Editor::UIEditorPanelRegistry; using XCEngine::UI::Editor::UIEditorWindowWorkspaceController; using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus; using XCEngine::UI::Editor::UIEditorWindowWorkspaceSet; +using XCEngine::UI::Editor::UIEditorWindowWorkspaceState; +using XCEngine::UI::Editor::UIEditorWindowWorkspaceValidationCode; using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement; using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; +using XCEngine::UI::Editor::ValidateUIEditorWindowWorkspaceSet; UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry registry = {}; @@ -448,3 +453,31 @@ TEST(UIEditorWindowWorkspaceControllerTest, RejectedCrossWindowMoveFromMissingSo EXPECT_EQ(controller.GetWindowSet().primaryWindowId, "main-window"); EXPECT_TRUE(AreWindowSetsEquivalent(controller.GetWindowSet(), windowSetBefore)); } + +TEST(UIEditorWindowWorkspaceControllerTest, ValidationRejectsDuplicatePanelAcrossWindows) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + + UIEditorWindowWorkspaceSet windowSet = + BuildDefaultUIEditorWindowWorkspaceSet(registry, BuildWorkspace()); + windowSet.activeWindowId = "doc-a-window"; + + UIEditorWindowWorkspaceState duplicateWindow = {}; + duplicateWindow.windowId = "doc-a-window"; + duplicateWindow.workspace.root = BuildUIEditorWorkspaceSingleTabStack( + "doc-a-window-root", + "doc-a", + "Document A", + true); + duplicateWindow.workspace.activePanelId = "doc-a"; + duplicateWindow.session = + BuildDefaultUIEditorWorkspaceSession(registry, duplicateWindow.workspace); + windowSet.windows.push_back(std::move(duplicateWindow)); + + const auto validation = ValidateUIEditorWindowWorkspaceSet(registry, windowSet); + EXPECT_EQ( + validation.code, + UIEditorWindowWorkspaceValidationCode::DuplicatePanelAcrossWindows); + EXPECT_NE(validation.message.find("doc-a"), std::string::npos); + EXPECT_NE(validation.message.find("main-window"), std::string::npos); + EXPECT_NE(validation.message.find("doc-a-window"), std::string::npos); +}