# XCUIEditor Windowing Architecture Plan 更新日期: `2026-04-26` 状态: `Phase 1 completed, Phase 2 completed, Subplan 3A planned` ## 1. Plan 管理规则 - `docs/plan` 现在只保留总 plan。 - 已完成 subplan 的结论直接并回总 plan,不再保留阶段性执行文档。 - 新的 subplan 必须保持小切口;一次只收口一条主链,不把 projection、destroy、drag/drop 等多条链路捆在同一阶段。 ## 2. 顶层架构问题 当前 `XCUIEditorApp` 最严重的架构问题仍然是: 多窗口工作区状态存在双真相源。 这两个真相源目前分别落在: - `editor/app/Windowing/System/EditorWindowSystem.*` - `editor/app/Composition/EditorWindowWorkspaceStore.*` - live `EditorWindow` - `editor/app/Windowing/Content/EditorWorkspaceWindowContentController.*` - `editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.*` 虽然已经把“同步 diff 规划”和“标题策略”从 `Win32` 层切回了 app 层,但当前仍然存在以下事实: - live `EditorWindow` / `EditorWorkspaceWindowContentController` 里的 `UIEditorWorkspaceController` 仍然是可变状态容器。 - `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 已经产出显式 `workspaceMutation` request,但 request 仍然来自 live controller 的本地副本。 - `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 收到 `workspaceMutation` 后,仍通过 `CommitLiveWindowMutation(sourceWindow)` 回读 `window.GetWorkspaceController()`,而不是直接消费 request payload。 - `EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan(...)` 仍同时承担 host 执行与 authoritative commit 的时序编排。 这会带来四个直接后果: 1. app 层和 host 层都能改 `UIEditorWindowWorkspaceSet`,单一真相源名义存在、实际不存在。 2. frame 内部交互修改依赖平台回写兜底,边界方向不稳定。 3. 关闭、拖拽、分离、primary 切换这些跨窗口时序很难纯粹按领域规则推导。 4. `editor/app/Platform/Win32` 继续承担了不属于 host adapter 的领域编排责任。 ## 3. 已完成收口 当前已完成两阶段收口,结果如下: - 新增 app 层同步模型: - `EditorWindowSynchronizationPlan` - `EditorWindowSynchronizationPlanner` - `EditorWindowPresentationPolicy` - `EditorWindowSystem` 新增 `BuildSynchronizationPlan(...)`,开始由 app 层产出 authoritative sync plan。 - `EditorWindowWorkspaceCoordinator` 不再持有窗口集 diff 算法和标题策略定义,只保留 host snapshot 采集与 plan 执行。 - `EditorWindowTransferRequests` 新增显式 workspace mutation request,frame 内工作区变化不再依赖平台隐式回写 authority。 - `EditorWindowSystem` 收口 live window mutation / native destroy observation / authoritative commit 语义入口。 - `EditorWindowLifecycleCoordinator::HandleNativeWindowDestroyed(...)` 不再直接删除 authority store entry,而是回到 app 层做 destroy reconciliation。 - `editor/app/Platform/Win32/**` 已移除对 authority raw write 接口的直接依赖。 - 新增聚焦验证: - `tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp` - `editor_windowing_phase1_tests` - editor 本体已完成构建与 `12s` 启动冒烟验证。 - editor 测试入口回到 `tests/UI/Editor`,legacy `tests/editor` 已从当前活动构建图移除。 这两阶段的目标分别是: - Phase 1: 先把“领域决策”和“Win32 执行”切开。 - Phase 2: 删除“平台回写 authority”链路,把 authority mutation 收口回 `editor/app/Windowing`。 这两个目标都已经达成。 ## 4. 当前剩余缺口 原计划中针对 `authority writeback` 的核心缺口已经完成收口,但还有一段“半收口”链路仍然存在: - `EditorWindowTransferRequests` 已有 `EditorWindowWorkspaceMutationRequest`,但 `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 目前只填 `workspace/session`,没有把稳定的 `windowId` 一并带出。 - `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 目前拿到 request 后并不直接消费它,而是再次回读 `sourceWindow` 上的 live projection。 - 这意味着 frame 内交互虽然已经显式化为 request,但 request 还没有真正成为这条链路的唯一语义输入。 这一段正好适合作为下一个小 subplan;在它完成之前,不适合继续展开更大的 live projection 去状态化。 ## 5. 目标架构 目标状态下,窗口系统的数据流应当是单向的: ```text authoritative window set (app/windowing) -> mutation validation -> synchronization plan -> Win32 execution -> host observation / lifecycle event -> app-layer semantic callback ``` 这里的关键约束是: - authority 只在 `editor/app/Windowing` 侧维护。 - `Win32` 层只能消费 plan、上报 observation/event,不能直接写 authority store。 - live content controller 只能是 projection 或受控 mutation producer,不能再充当隐式 authority 副本。 ## 6. 后续路线 后续按“小 subplan”推进,每个 subplan 只解决一条主链。下一个 subplan 只收口 frame 内显式 `workspaceMutation` request,不同时处理 live projection 去状态化、native destroy reconciliation 或 global tab drag。 ### 6.1 Subplan 3A: 显式 workspace mutation request 收口 #### 6.1.1 阶段目标 - 让 frame 内 workspace 变更通过 `EditorWindowWorkspaceMutationRequest` 进入 app 层,而不是由 coordinator 再回读 live `UIEditorWorkspaceController`。 - 把“显式 request 已存在但未真正成为主输入”的半收口状态补完整。 - 保持本阶段足够小,只处理 live workspace mutation commit 这条链。 #### 6.1.2 建议改动范围 - `editor/app/Windowing/Frame/EditorWindowTransferRequests.h` - `editor/app/Windowing/Content/EditorWorkspaceWindowContentController.*` - `editor/app/Windowing/System/EditorWindowSystem.*` - `editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.*` - `tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp` - 必要时补 `tests/UI/Editor/unit` 下新的 focused test;若必须经过 host 协调器,再补 `editor_app_feature_tests` #### 6.1.3 详细执行步骤 1. 先把 `workspaceMutation` request payload 补完整。 - `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 在生成 `EditorWindowWorkspaceMutationRequest` 时补齐 `windowState.windowId`。 - request 的有效性以“`windowId` 非空且 workspace state 可用”为准,避免继续依赖 source window 隐式补信息。 2. 再把 app 层计划入口改成显式 request 驱动。 - 在 `EditorWindowSystem` 中新增或收口一个直接消费 `EditorWindowWorkspaceMutationRequest` 的 plan builder。 - 该入口只做三件事: 校验 `windowId`、把 request 中的 `workspace/session` 合并进当前 authoritative window set、复用 `BuildPlanForWindowSet(...)`。 - `BuildPlanForLiveWindowMutation(...)` 如果还有旧调用方,可暂时降级成薄封装;本阶段不再新增新的调用点依赖它。 3. 最后切换 `EditorWindowWorkspaceCoordinator` 的 live commit 路径。 - `HandleWindowFrameTransferRequests(...)` 直接把 `transferRequests.workspace.workspaceMutation` 传给 commit 路径。 - `CommitLiveWindowMutation(...)` 改成基于 request 的实现,或者删除旧的 `EditorWindow&` 回读版本。 - 本阶段完成后,live workspace mutation commit 不再依赖 `window.GetWorkspaceController()`;该接口只允许继续服务 projection / title / UI 查询。 4. 补 focused 验证,保证这是小收口而不是行为漂移。 - planner/system 侧至少覆盖“显式 request 改 active panel 后可生成并提交有效 plan”。 - 增加“unknown windowId request 被拒绝”的负向验证。 - 如果 unit 无法覆盖 coordinator 消费 request 的行为,再补一条最小 host-side focused test,而不是直接上重型运行时验证。 #### 6.1.4 本阶段验收标准 - `workspaceMutation` request 自身携带稳定 `windowId`,不再需要 source window 兜底补齐。 - `EditorWindowWorkspaceCoordinator::HandleWindowFrameTransferRequests(...)` 不再忽略 request payload。 - live frame mutation 的 authoritative commit 路径不再回读 `sourceWindow.GetWorkspaceController()`。 - 现有 detach / dock / close / primary switch 行为不因本阶段发生语义漂移。 - `editor_windowing_phase1_tests` 继续通过;若新增 host-side focused test,对应测试目标也应通过。 #### 6.1.5 本阶段非目标 - 不在本阶段删除 `EditorWindow` / content controller 上的 `TryGetWorkspaceController()` 或 `ReplaceWorkspaceController()`。 - 不在本阶段处理 native destroy reconciliation。 - 不在本阶段重做 global tab drag / detach 相关编排。 - 不在本阶段把 live content controller 改成完全只读 projection。 ## 7. 验收标准 整个计划完成后,应至少满足: 1. `editor/app/Platform/Win32/**` 不再直接调用 authority store 原始写接口。 2. `EditorWindowSystem` 成为窗口工作区 authoritative mutation 的唯一 app 层入口。 3. 任意多窗口变化都先形成 app 层语义上的 mutation / transition,再形成 sync plan,再由 host 执行。 4. native close / destroy 不再通过平台层“直接删 store entry”完成状态收口。 5. `EditorWorkspaceWindowContentController` 不再被当作 authority 副本,而只是 projection 或受控 mutation source。 6. detach / dock / close / primary switch 至少具备稳定 unit coverage,并对关键 host 时序具备 focused integration coverage。 ## 8. 验证策略 验证入口仍然以 `tests/UI/Editor` 为主,不重新启用旧 `tests/editor`。 优先级如下: - `tests/UI/Editor/unit` - 必要时补 `tests/UI/Editor/smoke` - 只有在 unit / focused integration 无法覆盖的 host 时序下,才考虑更重的运行时验证 当前已建立的验证基线: - `cmake --build build --config Debug --target editor_windowing_phase1_tests` - `build/tests/UI/Editor/unit/Debug/editor_windowing_phase1_tests.exe` ## 9. 非目标 本计划当前不直接解决以下事项: - 把 `XCUIEditorApp` 立即改造成跨平台窗口系统 - 全量重写 shell / viewport / message dispatcher - 顺手重构所有 inspector、panel、runtime 业务逻辑 - 在 authority 收口前提前合并全部 Win32 coordinator 这些都必须排在“窗口领域 authority 真正收口”之后。