Add borderless host migration plan
This commit is contained in:
283
docs/plan/NewEditor_方案1无边框宿主采用计划_2026-04-14.md
Normal file
283
docs/plan/NewEditor_方案1无边框宿主采用计划_2026-04-14.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# NewEditor 方案1无边框宿主采用计划
|
||||
|
||||
日期: `2026-04-14`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 `new_editor` 从当前的原生 `WS_OVERLAPPEDWINDOW + DWM + HWND swapchain` 宿主模式,重构为:
|
||||
|
||||
- 无边框顶层窗口
|
||||
- 自绘标题栏与边框
|
||||
- 应用自己接管 move / resize / maximize / restore / hit-test
|
||||
- 宿主渲染链与窗口尺寸变化由同一套状态机驱动
|
||||
|
||||
核心目的只有一个:
|
||||
|
||||
尽可能消除当前原生窗口 live resize 期间那种“旧帧被系统先拉伸,再等新帧补上”的视觉变形。
|
||||
|
||||
## 2. 为什么必须走这条路
|
||||
|
||||
当前路径的问题不是 XCUI 布局树本身,而是宿主显示链顺序决定的:
|
||||
|
||||
1. Windows 先改变原生窗口外框尺寸。
|
||||
2. DWM 立即需要一张图去填新窗口区域。
|
||||
3. `new_editor` 再收到 `WM_SIZE`,之后才开始:
|
||||
- `ResizeBuffers`
|
||||
- backbuffer / interop target 处理
|
||||
- UI + viewport 合成
|
||||
- `Present`
|
||||
4. 这中间只要慢一个 compositor tick,就会看到旧尺寸帧被拉伸。
|
||||
|
||||
只要继续使用原生 non-client resize,这个问题就只能减轻,不能彻底归零。
|
||||
|
||||
## 3. 方案1的总思路
|
||||
|
||||
不要再让系统驱动 resize 交互。
|
||||
|
||||
改为:
|
||||
|
||||
1. 窗口本身使用无边框样式。
|
||||
2. 顶部标题栏、拖动区、8 个 resize grip 全部放到 client area。
|
||||
3. 鼠标拖动边界时,不进入系统的 `WM_ENTERSIZEMOVE` 模态循环。
|
||||
4. 应用自己维护一套 `WindowFrameController`:
|
||||
- 记录拖动起点
|
||||
- 计算目标外框矩形
|
||||
- 先驱动宿主渲染链切到目标尺寸
|
||||
- 再提交窗口矩形变化与新帧 present
|
||||
|
||||
这样窗口尺寸变化、swapchain 尺寸变化、UI 布局变化、present 节奏都由应用自己掌控,而不是被 Windows 原生外框拆成两段。
|
||||
|
||||
## 4. 重构边界
|
||||
|
||||
这次重构只动 `new_editor/app/Host` 宿主层,不把业务逻辑塞进去。
|
||||
|
||||
### 4.1 保留不动的层
|
||||
|
||||
- `XCEditor` 基础 UI 组件层
|
||||
- `new_editor/app/Workspace`
|
||||
- `new_editor/app/Panels`
|
||||
- `Viewport` 业务层
|
||||
|
||||
### 4.2 主要改造层
|
||||
|
||||
- [Application.cpp](D:/Xuanchi/Main/XCEngine/new_editor/app/Application.cpp)
|
||||
- [Application.h](D:/Xuanchi/Main/XCEngine/new_editor/app/Application.h)
|
||||
- [WindowMessageDispatcher.cpp](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/WindowMessageDispatcher.cpp)
|
||||
- [WindowMessageDispatcher.h](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/WindowMessageDispatcher.h)
|
||||
- [HostRuntimeState.h](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/HostRuntimeState.h)
|
||||
- [D3D12WindowRenderLoop.cpp](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/D3D12WindowRenderLoop.cpp)
|
||||
- [D3D12WindowRenderer.cpp](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/D3D12WindowRenderer.cpp)
|
||||
- [D3D12WindowSwapChainPresenter.cpp](D:/Xuanchi/Main/XCEngine/new_editor/app/Host/D3D12WindowSwapChainPresenter.cpp)
|
||||
|
||||
### 4.3 新增宿主对象
|
||||
|
||||
建议新增以下文件:
|
||||
|
||||
- `new_editor/app/Host/WindowFrameMetrics.h`
|
||||
- `new_editor/app/Host/WindowFrameController.h`
|
||||
- `new_editor/app/Host/WindowFrameController.cpp`
|
||||
- `new_editor/app/Host/WindowFrameInteractionState.h`
|
||||
- `new_editor/app/Host/BorderlessWindowStyle.h`
|
||||
|
||||
职责划分:
|
||||
|
||||
- `WindowFrameMetrics`
|
||||
- 统一管理标题栏高度、resize 边缘厚度、阴影扩展、caption button 区域
|
||||
- `WindowFrameInteractionState`
|
||||
- 记录当前是否在 move / resize
|
||||
- 记录激活边、起始窗口矩形、起始鼠标屏幕坐标
|
||||
- `WindowFrameController`
|
||||
- 处理 hit-test、开始拖动、更新拖动、结束拖动
|
||||
- 计算目标窗口矩形
|
||||
- 输出“本帧宿主需要切到什么尺寸”
|
||||
- `BorderlessWindowStyle`
|
||||
- 集中处理 `WS_POPUP / WS_THICKFRAME / WS_CAPTION` 等样式组合与 DWM 扩展
|
||||
|
||||
## 5. 分阶段执行
|
||||
|
||||
## 阶段 A:把窗口宿主从原生外框切到无边框
|
||||
|
||||
### 目标
|
||||
|
||||
先完成“视觉上还是一个正常窗口,但 non-client 已经不再由系统绘制”。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 创建窗口时移除 `WS_OVERLAPPEDWINDOW`,改为适合无边框的样式组合。
|
||||
2. 顶部工具栏正式承担标题栏职责。
|
||||
3. 处理:
|
||||
- 最小化
|
||||
- 最大化
|
||||
- 还原
|
||||
- 关闭
|
||||
4. 保留 Windows 阴影、任务栏行为、Alt+Tab 行为。
|
||||
|
||||
### 验收
|
||||
|
||||
1. 窗口外框由 XCUI 自己绘制。
|
||||
2. 顶部区域可拖动移动窗口。
|
||||
3. 基本窗口控制按钮可用。
|
||||
|
||||
## 阶段 B:接管 hit-test 与 8 向 resize 手势
|
||||
|
||||
### 目标
|
||||
|
||||
不再依赖系统 non-client resize。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 在 client area 内定义:
|
||||
- 左 / 右 / 上 / 下
|
||||
- 左上 / 右上 / 左下 / 右下
|
||||
的 resize 热区。
|
||||
2. 鼠标 hover 时稳定切换对应 cursor。
|
||||
3. 鼠标按下后进入 `WindowFrameInteractionState`。
|
||||
4. 鼠标移动时由 `WindowFrameController` 计算目标外框矩形。
|
||||
|
||||
### 验收
|
||||
|
||||
1. 8 个方向 resize 都能工作。
|
||||
2. cursor 不闪烁。
|
||||
3. 不再进入 `WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE` 原生模态链。
|
||||
|
||||
## 阶段 C:把窗口尺寸变化改成“应用驱动”
|
||||
|
||||
### 目标
|
||||
|
||||
让窗口矩形变化和新尺寸帧提交进入同一条应用控制链。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 取消当前“完全依赖 `WM_SIZE` 才处理 resize”的模式。
|
||||
2. 鼠标拖动过程中,先由 `WindowFrameController` 产出目标 client size。
|
||||
3. 宿主渲染层按目标 size:
|
||||
- resize swapchain
|
||||
- rebuild backbuffer view
|
||||
- 重新布局 UI
|
||||
- render + present
|
||||
4. 然后再提交窗口矩形更新。
|
||||
|
||||
这里的关键不是“永远先后顺序绝对固定”,而是宿主要从“被动响应 Windows resize”改成“主动推进目标尺寸帧”。
|
||||
|
||||
### 验收
|
||||
|
||||
1. live resize 期间不再有明显的整窗拉伸。
|
||||
2. 内部 UI 不再跟随旧帧一起被整体放大缩小。
|
||||
3. resize 时不黑屏、不闪退。
|
||||
|
||||
## 阶段 D:把 Scene / Game viewport 也纳入同一节奏
|
||||
|
||||
### 目标
|
||||
|
||||
避免窗口外层尺寸变化和 viewport target 更新脱节。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 宿主 resize 状态机输出统一尺寸变更事件。
|
||||
2. `ProductViewportHostService` 消费同一目标尺寸。
|
||||
3. viewport render target 与 host swapchain 的 resize 节奏统一。
|
||||
|
||||
### 验收
|
||||
|
||||
1. 调外层窗口尺寸时,Scene / Game 内部画布不再滞后。
|
||||
2. 不再出现外层变了、内部蓝底画布慢一拍才追上的情况。
|
||||
|
||||
## 阶段 E:补齐无边框窗口的系统行为
|
||||
|
||||
### 目标
|
||||
|
||||
把“能用”补到“能长期替代正式编辑器宿主”。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 双击标题栏最大化 / 还原。
|
||||
2. 拖到屏幕顶部最大化。
|
||||
3. 屏幕边缘吸附。
|
||||
4. 多显示器 DPI / work area 正确处理。
|
||||
5. 最小窗口尺寸约束。
|
||||
6. 系统菜单与快捷键兼容。
|
||||
|
||||
### 验收
|
||||
|
||||
1. 宿主交互接近常规桌面应用。
|
||||
2. 不因无边框而丢失基本桌面体验。
|
||||
|
||||
## 6. 架构原则
|
||||
|
||||
### 6.1 不允许把窗口交互逻辑混回 `Application`
|
||||
|
||||
`Application` 只能做:
|
||||
|
||||
- 应用生命周期
|
||||
- 编辑器业务装配
|
||||
- 驱动一帧更新与渲染
|
||||
|
||||
窗口移动、边框 hit-test、resize 手势状态机都必须沉淀到 `Host`。
|
||||
|
||||
### 6.2 不允许为了“先跑通”堆事件补丁
|
||||
|
||||
禁止继续沿这些方向堆补丁:
|
||||
|
||||
- `WM_SIZE` 后疯狂补 render
|
||||
- `ValidateRect` / `InvalidateRect` 来回试
|
||||
- `DwmFlush` 之类的消息尾部补丁
|
||||
- 在 resize 过程中乱加全局 GPU idle
|
||||
|
||||
这些都只能缓解,不能把宿主控制权拿回来。
|
||||
|
||||
### 6.3 宿主渲染链必须单向清晰
|
||||
|
||||
目标链路必须收敛成:
|
||||
|
||||
`WindowFrameController -> Host resize state -> swapchain/backbuffer resize -> UI + viewport compose -> present`
|
||||
|
||||
而不是:
|
||||
|
||||
`Windows non-client -> WM_SIZE -> 若干临时补丁 -> 侥幸赶上 compositor`
|
||||
|
||||
## 7. 风险点
|
||||
|
||||
### 7.1 这不是小修
|
||||
|
||||
这是宿主交互模型切换,不是单点 bugfix。
|
||||
|
||||
### 7.2 需要额外补齐桌面窗口语义
|
||||
|
||||
无边框之后,很多系统行为都要自己接:
|
||||
|
||||
- caption drag
|
||||
- 双击最大化
|
||||
- hit-test
|
||||
- snap
|
||||
- monitor work area
|
||||
- DPI 变更
|
||||
|
||||
### 7.3 需要严格做阶段验收
|
||||
|
||||
每一阶段都必须编译并人工验证,不然很容易把宿主架构搞乱。
|
||||
|
||||
## 8. 推荐执行顺序
|
||||
|
||||
1. 阶段 A:无边框窗口切换
|
||||
2. 阶段 B:hit-test / resize 手势
|
||||
3. 阶段 C:应用驱动 resize 主链
|
||||
4. 阶段 D:viewport resize 节奏统一
|
||||
5. 阶段 E:补系统级桌面行为
|
||||
|
||||
## 9. 收口标准
|
||||
|
||||
满足以下条件,才算方案1真正落地:
|
||||
|
||||
1. `new_editor` 已彻底脱离原生 non-client resize。
|
||||
2. 窗口拖边 resize 时,整窗拉伸变形基本消失。
|
||||
3. Scene / Game viewport 尺寸变化与外层窗口同步。
|
||||
4. resize 过程中无黑屏、无崩溃、无 cursor 闪烁。
|
||||
5. 顶部标题栏、最大化、吸附、DPI 行为达到正式编辑器可用水平。
|
||||
|
||||
## 10. 结论
|
||||
|
||||
如果目标是“尽可能彻底消除 resize 变形”,就不该继续在当前原生外框链路上修修补补。
|
||||
|
||||
正确主线是:
|
||||
|
||||
把 `new_editor` 宿主改成无边框、自管标题栏、自管 resize、自管 present 节奏的 editor shell。
|
||||
Reference in New Issue
Block a user