From 838f676fa6937f015270a6c0c692c0cbcd24148b Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 12 Apr 2026 01:29:00 +0800 Subject: [PATCH] Refactor new editor app context and workspace shell --- ...itor对标旧Editor迁移重建计划_2026-04-12.md | 600 ++++++++++++++++++ ...ock统一与Tab拖拽停靠收口计划_2026-04-10.md | 0 new_editor/CMakeLists.txt | 2 + new_editor/app/Application.cpp | 343 ++-------- new_editor/app/Application.h | 28 +- new_editor/app/Core/ProductEditorContext.cpp | 187 ++++++ new_editor/app/Core/ProductEditorContext.h | 55 ++ .../app/Workspace/ProductEditorWorkspace.cpp | 210 ++++++ .../app/Workspace/ProductEditorWorkspace.h | 55 ++ 9 files changed, 1177 insertions(+), 303 deletions(-) create mode 100644 docs/plan/NewEditor对标旧Editor迁移重建计划_2026-04-12.md rename docs/plan/{ => used}/XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md (100%) create mode 100644 new_editor/app/Core/ProductEditorContext.cpp create mode 100644 new_editor/app/Core/ProductEditorContext.h create mode 100644 new_editor/app/Workspace/ProductEditorWorkspace.cpp create mode 100644 new_editor/app/Workspace/ProductEditorWorkspace.h diff --git a/docs/plan/NewEditor对标旧Editor迁移重建计划_2026-04-12.md b/docs/plan/NewEditor对标旧Editor迁移重建计划_2026-04-12.md new file mode 100644 index 00000000..993e16f3 --- /dev/null +++ b/docs/plan/NewEditor对标旧Editor迁移重建计划_2026-04-12.md @@ -0,0 +1,600 @@ +# NewEditor 对标旧 Editor 迁移重建计划 + +日期: `2026-04-12` + +## 1. 文档定位 + +这份计划用于正式切换主线: + +- 旧 `editor/` 继续作为参考实现与行为基线。 +- `new_editor/` 从“XCEditor 基础层试验宿主”升级为“新编辑器正式重建宿主”。 +- 后续工作重点不再是单独补 `XCEditor` 的孤立控件,而是参考旧 `editor/`,在 `new_editor/` 内逐步重建完整编辑器应用。 + +但有一个硬约束必须始终遵守: + +- 如果在重建 `new_editor` 的过程中暴露出 `XCEditor`、宿主层或输入/布局/viewport 基础设施的结构问题,必须先回到底层修根因,再继续往上迁移。 +- 不允许在 `new_editor` 业务层堆临时兼容逻辑来掩盖底层问题。 + +这份计划替代此前“先单独收口 dock/UI 基础层,再暂停业务”的阶段性计划。当前主线已经进入 `new_editor` 正式重建阶段。 + +## 2. 不可违背的设计原则 + +### 2.1 根因优先 + +- 任何交互、布局、DPI、capture、拖拽、文本测量、渲染清晰度问题,优先在 `XCEditor` / `Host` / shared editor infrastructure 修。 +- 禁止在某个 panel 内写只为绕过去的局部 hack。 + +### 2.2 旧 Editor 是参考基线,不是直接复制目标 + +- 旧 `editor/` 的行为、工作流、信息架构、可用性和视觉结果是当前最可靠的参考。 +- 旧 `editor/` 里凡是 ImGui 特有的壳代码、临时 helper、历史包袱,不应机械照搬。 +- 迁移过程中允许顺手做架构收敛,但必须保持行为兼容,不允许一边“优化”一边偏离旧版交互。 + +### 2.3 `XCEditor` 只承载 Editor UI 基础层,不承载 Editor 业务 + +- `new_editor/include/XCEditor/**` + `new_editor/src/**` 负责 editor-only 的 UI primitive、shell、dock、field、tree/list、menu、viewport slot、workspace 等基础能力。 +- `new_editor` 的正式编辑器业务必须放在应用层,不得继续把业务逻辑塞回 `XCEditor` 库层。 + +### 2.4 Editor 样式固定写死在代码里 + +- `Runtime UI` 保留资源化/主题化能力,服务游戏开发者。 +- `Editor UI` 当前不走资源驱动主线,样式、palette、metrics、chrome 由代码固定维护。 +- 不再给 `Editor UI` 扩一套厚重的主题资源解释系统。 + +### 2.5 测试体系与产品宿主分离 + +- `tests/UI/Core|Runtime|Editor` 继续作为 UI 模块验证入口。 +- `new_editor/` 是产品宿主,不是测试场景堆放区。 +- 某项能力先在对应层的 `tests/UI` 里做 unit/integration 回归,再接入 `new_editor`。 + +## 3. 当前基线判断 + +## 3.1 已经完成并可复用的部分 + +当前 `new_editor` 已经具备一套可继续向上承接的 Editor UI 基础层: + +- `XCEditor` 库层已经有: + - `Fields` + - `Tree/List/Scroll/TabStrip` + - `MenuBar/MenuPopup/StatusBar` + - `DockHost/Workspace/PanelHost` + - `ViewportSlot/ViewportShell` + - shortcut、command registry、workspace persistence +- `Host` 层已经有: + - Win32 宿主 + - Direct2D/DirectWrite 文本与图像绘制 + - DPI 感知 + - 自动截图 + - 自有 exe 截图输出 +- `new_editor` 当前已经验证过: + - Unity 风格的基础 chrome + - tab 拖拽重排与停靠 + - project 左树与右侧浏览器的基础外壳 + - hierarchy / project 图标与文本对齐 + - 多项 `tests/UI/Editor` unit / integration 回归 + +结论: + +- `XCEditor` 不再是“完全不能承接产品”的状态。 +- 当前真正缺的不是几个零散 widget,而是 `new_editor` 应用层本身还没有长成旧 `editor/` 那样完整的编辑器架构。 + +## 3.2 当前仍然缺失的关键能力 + +距离“可以真正替代旧 `editor/` 开始长期迭代”的状态,当前还缺: + +1. `new_editor` 自己的应用层骨架还不完整。 +2. 缺少类似旧 `editor/src/Core + Managers + Actions + Commands + Panels + Viewport + ComponentEditors + Scripting` 的正式分层。 +3. `Hierarchy` 仍是 demo/static 数据,不是场景真实数据。 +4. `Project` 虽然外壳已成型,但还不是旧 editor 的完整资产工作流。 +5. `Inspector` 及 `ComponentEditorRegistry` 体系尚未接回。 +6. `Console` 与日志桥接尚未接回。 +7. `Scene/Game viewport` 还没有接回旧 editor 的真实渲染/交互主链。 +8. project / scene / undo / selection / action route / runtime mode 这些编辑器全局状态在 `new_editor` 里还没有正式 owner。 +9. 脚本、运行模式、保存/打开、项目切换、Library/Asset 流程都还未接回。 + +## 4. 旧 Editor 与 NewEditor 的真实差距 + +## 4.1 旧 Editor 的实际分层 + +旧 `editor/` 当前已经形成的主干是: + +`Application -> EditorLayer -> EditorWorkspace -> Panels/Viewport -> Actions -> Commands -> Managers/Core` + +横向还包含: + +- `UI` +- `ComponentEditors` +- `Layout` +- `Scripting` +- `Platform` + +这意味着旧 editor 已经不是“只有一堆 panel”,而是一套完整的编辑器应用结构。 + +## 4.2 NewEditor 当前的真实分层 + +`new_editor/` 当前更接近: + +`Application + Host + XCEditorLib + 少量 Product 面板` + +其中: + +- `include/XCEditor/**` + `src/**` 是 Editor UI 库层。 +- `Host/**` 是平台与绘制宿主。 +- `app/**` 目前还只是轻量应用壳与少量试制 panel。 + +缺口在于: + +- 旧 editor 的“应用层中间层”几乎还没完整迁过来。 +- 当前 `new_editor` 的 panel 仍然偏 prototype,不是正式产品模块。 + +## 4.3 模块映射与结论 + +| 旧 editor 模块 | 当前状态 | new_editor 目标落点 | +| --- | --- | --- | +| `editor/src/UI/**` | 大部分语义已被 `XCEditor` 吸收 | 继续沉淀在 `include/XCEditor/**` + `src/**` | +| `editor/src/Layout/**` | 旧版依赖 ImGui docking | 由 `XCEditor/Shell/**` 的 workspace/dock host 接替 | +| `editor/src/Application.*` | 旧版完整 | `new_editor` 应保留为宿主壳,避免塞业务 | +| `editor/src/Core/**` | 旧版完整 | `new_editor` 需要建立对应 app-level Core | +| `editor/src/Managers/**` | 旧版完整 | `new_editor` 需要建立对应 app-level Managers | +| `editor/src/Actions/**` | 旧版完整 | `new_editor` 需要正式迁入 | +| `editor/src/Commands/**` | 旧版完整 | `new_editor` 需要正式迁入 | +| `editor/src/panels/**` | 旧版完整 | `new_editor` 需要按新 shell 重写,但行为对齐旧版 | +| `editor/src/ComponentEditors/**` | 旧版完整 | `new_editor` 需要迁入并适配 `XCEditor` property grid/field | +| `editor/src/Viewport/**` | 旧版完整 | `new_editor` 需要重新桥接到 `XCEditor` viewport slot/shell | +| `editor/src/Scripting/**` | 旧版完整 | `new_editor` 后期需要接回 | + +结论: + +- `XCEditor` 已经覆盖了大量旧 `editor/src/UI/**` 与 `Layout` 的职责。 +- 但 `new_editor` 还没有真正补上旧 editor 的应用层与工作流层。 +- 因此接下来的重点不是“继续造更多基础控件”,而是“用旧 editor 的结构和行为为参照,把新 editor 的应用层补齐”。 + +## 5. 迁移总策略 + +迁移策略固定为两条线并行推进: + +### 5.1 主线:重建 NewEditor 应用层 + +- 以旧 `editor/` 为参考,在 `new_editor` 下逐步建立正式的: + - `Core` + - `Managers` + - `Actions` + - `Commands` + - `Panels` + - `Viewport` + - `ComponentEditors` + - `Scripting` + +### 5.2 副线:按需回补 XCEditor / Host + +- 当主线重建暴露底层能力缺口时,再回补: + - `XCEditor` + - `Host` + - `tests/UI/Editor` + +副线只为解决主线遇到的真实阻塞,不再无边界扩 editor-only widget。 + +## 6. 目标架构 + +## 6.1 `XCEditor` 的职责边界 + +保留在库层的内容: + +- fixed-code editor theme / metrics / palette +- menu / popup / dock / workspace / tab strip +- tree / list / scroll / inline rename / property grid / fields +- viewport slot / viewport shell +- editor UI input model、session、interaction state machine + +不放进库层的内容: + +- project 资产语义 +- scene/entity/component 语义 +- console/log bridge +- project/scene open/save 工作流 +- inspector 组件注册表 +- editor 菜单业务动作 + +## 6.2 `new_editor` 应用层目标结构 + +应用层建议逐步收敛到与旧 `editor/src` 接近的形态: + +```text +new_editor/ + Host/ + include/XCEditor/ + src/ + Core/ + Managers/ + Actions/ + Commands/ + Panels/ + Viewport/ + ComponentEditors/ + Scripting/ + Shell/ + Icons/ + Application/ +``` + +说明: + +- `include/XCEditor/**` + `src/**` 中的库层文件继续保留并演进。 +- `new_editor` 的产品应用代码不应长期散落在轻量 `app/*` 原型目录里。 +- 迁移早期可以暂时复用现有 `app/*`,但必须尽快收敛到正式应用层目录,避免“基础层代码”和“产品层代码”混杂。 + +## 7. 分阶段执行计划 + +## Phase 0:文档与目录主线切换 + +### 目标 + +把主线明确切到 `new_editor` 正式重建,清理过期计划入口。 + +### 任务 + +- 归档旧的阶段性 dock 收口计划。 +- 保留 `Editor架构说明.md` 作为旧 editor 参考文档,不归档。 +- 新建本迁移计划,作为当前唯一 editor 重建主线。 +- 在计划中正式写死: + - `old editor = reference` + - `new_editor = product host` + - `XCEditor = editor ui library` + - `tests/UI = module verification entry` + +### 完成标准 + +- 后续不再以旧 dock 收口计划作为主线驱动文档。 + +## Phase 1:收敛 NewEditor 应用层骨架 + +### 目标 + +先把 `new_editor` 从“壳 + 若干 product panel”收敛成“真正的编辑器应用结构”。 + +### 任务 + +- 建立 `new_editor` 自己的应用层目录与命名边界。 +- 提炼 `Application` 的纯宿主职责: + - window / DPI / renderer / input pump / screenshot / main loop +- 建立 `EditorWorkspace` 式的应用装配层: + - panel registry + - workspace compose + - panel attach/detach/update/render +- 建立 `EditorContext` / `IEditorContext` 式的全局状态容器。 +- 定义新 editor 的状态所有权: + - selection owner + - scene owner + - project owner + - undo owner + - active action route owner + - runtime mode owner + +### 根因要求 + +- 不允许继续让 `Application` 直接知道越来越多 project/hierarchy 细节。 +- 任何产品语义都必须逐步从 `Application` 下沉到 context / managers / panels。 + +### 完成标准 + +- `new_editor` 具备清晰的 app-level `Core/Workspace/Panels` 骨架。 +- 现有 `ProductHierarchyPanel` / `ProductProjectPanel` 不再是孤立 demo 对象,而是正式 panel 接口的一部分。 + +## Phase 2:迁入全局动作路由与命令层 + +### 目标 + +把旧 editor 的 “menu / shortcut / context menu / toolbar -> action/router -> command” 主链迁进来。 + +### 任务 + +- 对照旧 `editor/src/Actions/**` 梳理新 editor 的动作集: + - `File` + - `Edit` + - `Assets` + - `Run` + - `Scripts` + - `View` + - `Help` +- 建立新的 action route 机制: + - global actions + - focused panel actions + - active route dispatch +- 将真正修改数据的行为放入 `Commands/**`。 +- 让菜单、快捷键、右键菜单、toolbar 共享同一套动作定义。 + +### 根因要求 + +- 不允许 menu、shortcut、context menu 各自复制一套业务逻辑。 +- 不允许 panel 直接偷偷修改 scene/project 状态绕开 command。 + +### 完成标准 + +- 所有 editor 主动作都有稳定 owner。 +- UI 入口与业务执行不再混杂。 + +## Phase 3:接回 Hierarchy 正式数据模型 + +### 目标 + +把 `Hierarchy` 从 demo tree 改成真实 scene-backed 面板。 + +### 任务 + +- 用真实场景层级替换当前静态 `m_treeItems`。 +- 建立 hierarchy 展平与增量同步路径。 +- 接回 selection 同步。 +- 接回 rename。 +- 接回 create/delete/duplicate。 +- 接回拖拽 reparent: + - drop on entity => 成为子节点 + - drop on empty area => 回到 root +- 接回 context menu。 +- 接回旧 editor 的层级树图标/展开/焦点/多选策略。 + +### 优先参考文件 + +- `editor/src/panels/HierarchyPanel.*` +- `editor/src/Actions/HierarchyActionRouter.h` +- `editor/src/Commands/EntityCommands.h` +- `editor/src/Managers/SceneManager.*` + +### 根因要求 + +- 先建立可变 hierarchy source model,再做拖拽。 +- 如拖拽/capture 冲突,需要在 shared hosted-content capture 机制上统一修复,不允许再写 panel 特判。 + +### 完成标准 + +- `Hierarchy` 具备旧 editor 的核心编辑语义,而不是展示假数据。 + +## Phase 4:接回 Project 正式资产工作流 + +### 目标 + +把已经有外壳的 `Project` 面板接回真实 project/Assets 工作流。 + +### 任务 + +- 用正式 `ProjectManager`/Asset data source 替换当前轻量扫描逻辑。 +- 保持已完成的外观与交互风格。 +- 接回: + - folder tree + - breadcrumb navigation + - asset grid selection + - open / double click + - rename + - create folder + - delete + - drag move + - context menu + - refresh / watcher +- 对齐旧 editor 的 `.meta` / `Library` / 资源管理语义。 + +### 优先参考文件 + +- `editor/src/panels/ProjectPanel.*` +- `editor/src/Actions/ProjectActionRouter.h` +- `editor/src/Commands/ProjectCommands.h` +- `editor/src/Managers/ProjectManager.*` + +### 根因要求 + +- Project 右侧 grid 的事件必须继续保持语义化输出,不要退回 raw hit test 乱写业务。 +- 资产操作必须收敛到 command / manager,不能把文件系统操作塞在 UI 绘制逻辑里。 + +### 完成标准 + +- `Project` 成为真正可用的资产浏览与操作入口。 + +## Phase 5:接回 Inspector 与 ComponentEditorRegistry + +### 目标 + +把 `XCEditor` 已有的 field/property grid 真正用于 Inspector,而不是停留在基础控件演示。 + +### 任务 + +- 建立新的 `ComponentEditors/**` 注册表。 +- 接回 `InspectorPanel`。 +- 首批接回: + - Transform + - Camera + - Light + - MeshFilter + - MeshRenderer + - Script +- 接回 object/asset reference field。 +- 处理 interactive undo 边界。 +- 对齐旧 editor 的 Add Component 入口与规则。 + +### 优先参考文件 + +- `editor/src/panels/InspectorPanel.*` +- `editor/src/ComponentEditors/**` +- `editor/src/Commands/ComponentCommands.h` + +### 根因要求 + +- 通用字段行为沉淀在 `XCEditor/Fields/**`。 +- 组件语义停留在 app-level `ComponentEditors/**`。 +- 不允许把具体组件判断重新塞回 shared field 层。 + +### 完成标准 + +- `Inspector` 可以对真实选中对象执行稳定编辑。 + +## Phase 6:接回 Console 与编辑器日志桥接 + +### 目标 + +补齐旧 editor 的 console 工作流。 + +### 任务 + +- 迁入 console data model、过滤状态与 sink。 +- 接回日志分级、清空、过滤、点击定位等基础行为。 +- 与菜单/快捷键/上下文操作对齐。 + +### 优先参考文件 + +- `editor/src/panels/ConsolePanel.*` +- `editor/src/UI/ConsoleFilterState.h` +- `editor/src/Core/EditorConsoleSink.*` +- `editor/src/Actions/ConsoleActionRouter.h` + +### 完成标准 + +- `Console` 不再是空 panel,能承担真实调试反馈。 + +## Phase 7:接回 Scene / Game Viewport 正式主链 + +### 目标 + +这是替代旧 editor 的真正硬门槛。 + +### 任务 + +- 将旧 editor 的 viewport host/render path 重新桥接到 `XCEditorViewportSlot / XCEditorViewportShell`。 +- 接回: + - scene viewport 离屏渲染 + - game viewport 渲染 + - viewport resize + - navigation + - gizmo + - overlay + - selection / picking + - edit / play mode 切换 +- 处理 viewport 与 shell 输入焦点、capture、cursor 的统一。 + +### 优先参考文件 + +- `editor/src/panels/SceneViewPanel.*` +- `editor/src/panels/GameViewPanel.*` +- `editor/src/Viewport/**` +- `editor/src/Application.*` +- `editor/src/Platform/D3D12WindowRenderer*` + +### 根因要求 + +- viewport 相关问题优先在 app viewport layer / host bridge 修。 +- 不允许在 panel 里私拼第二套渲染路径。 + +### 完成标准 + +- `Scene/Game` 具备旧 editor 的基础可用性。 + +## Phase 8:接回项目生命周期、运行模式与脚本工作流 + +### 目标 + +把编辑器从“几个面板能显示”推进到“有完整项目工作流”。 + +### 任务 + +- project 打开 / 切换 / 保存状态 +- scene 打开 / 保存 / Save As +- runtime mode / play session controller +- 脚本程序集重建与状态显示 +- Library/bootstrap/import 状态展示 +- window title 与 dirty state + +### 优先参考文件 + +- `editor/src/Core/PlaySessionController.*` +- `editor/src/Core/EditorWindowTitle.h` +- `editor/src/Scripting/**` +- `editor/src/Application.*` + +### 完成标准 + +- `new_editor` 能跑通真实项目编辑主流程,而不是只展示 UI 壳。 + +## Phase 9:视觉一致性、回归与旧 Editor 退场条件 + +### 目标 + +在核心工作流都接回后,统一做收口。 + +### 任务 + +- 对关键界面做逐项比对: + - menu bar + - dock/tab + - hierarchy + - project + - inspector + - console + - scene/game +- 修正与旧 editor 的视觉与交互偏差。 +- 清理 `new_editor` 中所有 demo 数据、临时状态、过渡适配层。 +- 给每一块关键能力补齐: + - `tests/UI/Editor` + - product-level smoke validation + +### 最终收口标准 + +满足以下条件后,才算真正达到“可替代旧 editor”的程度: + +1. `new_editor` 已接回旧 editor 的核心面板与主工作流。 +2. `Scene/Game` viewport 可稳定工作。 +3. `Hierarchy/Project/Inspector/Console` 都是正式数据源,不再有 demo 假数据。 +4. menu / shortcut / context / toolbar 已共用动作层。 +5. undo / selection / runtime mode / active route 有清晰 owner。 +6. 没有为了赶进度堆出来的 panel-local hack。 + +## 8. 并行拆分建议 + +可以并行,但必须分主从关系: + +### 主依赖顺序 + +1. `Phase 1` 应用层骨架 +2. `Phase 2` 动作与命令层 +3. `Phase 3/4/5/6` 面板业务回接 +4. `Phase 7` viewport 主链 +5. `Phase 8/9` 生命周期与总收口 + +### 可并行子线 + +1. `Core/Managers/Context` 收敛 +2. `Actions/Commands` 迁移 +3. `Hierarchy + SceneManager` 接回 +4. `Project + ProjectManager` 接回 +5. `Inspector + ComponentEditors` 接回 +6. `Console + tests/UI/Editor` 回归补齐 + +说明: + +- `Viewport` 尽量单独作为高风险主线,不和别的大改混在一起。 +- 一旦并行子线发现 `XCEditor` 基础问题,优先暂停子线、回底层修复。 + +## 9. 立即执行顺序 + +当前建议的第一批执行顺序是: + +1. 先收敛 `new_editor` 应用层骨架与目录边界。 +2. 立刻建立新的 `EditorContext / Managers / Workspace`。 +3. 然后先接 `Hierarchy` 和 `Project` 的真实数据源,因为这两块当前已有可见 UI 外壳。 +4. 再接 `Inspector` 与 `Console`。 +5. 最后接 `Scene/Game viewport` 与运行模式主链。 + +原因: + +- `Hierarchy` / `Project` 已经有可见壳层,最适合先把产品语义接回来。 +- `Viewport` 风险最高,应在应用层骨架稳定后单独推进。 + +## 10. 当前需要归档的旧计划 + +本轮建议归档到 `docs/plan/used/` 的文档: + +- `XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md` + +保留在 `docs/plan/` 的参考文档: + +- `Editor架构说明.md` + +原因: + +- 前者属于阶段性基础层收口计划,主线已被当前迁移重建计划替代。 +- 后者不是过期计划,而是旧 editor 的分层参考资料,后续迁移时仍需频繁对照。 diff --git a/docs/plan/XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md b/docs/plan/used/XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md similarity index 100% rename from docs/plan/XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md rename to docs/plan/used/XCEditor_Dock统一与Tab拖拽停靠收口计划_2026-04-10.md diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 388d3a97..1fca5c3f 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -147,10 +147,12 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) add_executable(XCUIEditorApp WIN32 app/main.cpp app/Application.cpp + app/Core/ProductEditorContext.cpp app/Icons/ProductBuiltInIcons.cpp app/Panels/ProductHierarchyPanel.cpp app/Panels/ProductProjectPanel.cpp app/Shell/ProductShellAsset.cpp + app/Workspace/ProductEditorWorkspace.cpp ) target_include_directories(XCUIEditorApp PRIVATE diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index c28917f0..58654718 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -1,7 +1,5 @@ #include "Application.h" -#include "Shell/ProductShellAsset.h" - #include #include @@ -33,53 +31,12 @@ using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; -using App::BuildProductShellAsset; -using App::BuildProductShellInteractionDefinition; constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; -UIEditorShellComposeModel BuildShellComposeModelFromFrame( - const UIEditorShellInteractionFrame& frame) { - UIEditorShellComposeModel model = {}; - model.menuBarItems = frame.request.menuBarItems; - model.toolbarButtons = frame.model.toolbarButtons; - model.statusSegments = frame.model.statusSegments; - model.workspacePresentations = frame.model.workspacePresentations; - return model; -} - -void AppendShellPopups( - UIDrawList& drawList, - const UIEditorShellInteractionFrame& frame, - const UIEditorShellInteractionPalette& palette, - const UIEditorShellInteractionMetrics& metrics) { - const std::size_t popupCount = - (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); - for (std::size_t index = 0; index < popupCount; ++index) { - const UIEditorShellInteractionPopupRequest& popupRequest = - frame.request.popupRequests[index]; - const UIEditorShellInteractionPopupFrame& popupFrame = - frame.popupFrames[index]; - Widgets::AppendUIEditorMenuPopupBackground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - Widgets::AppendUIEditorMenuPopupForeground( - drawList, - popupRequest.layout, - popupRequest.widgetItems, - popupFrame.popupState, - palette.popupPalette, - metrics.popupMetrics); - } -} - Application* GetApplicationFromWindow(HWND hwnd) { return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); } @@ -387,27 +344,6 @@ std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& eve return stream.str(); } -std::vector FilterShellInputEventsForHostedContentCapture( - const std::vector& inputEvents) { - std::vector filteredEvents = {}; - filteredEvents.reserve(inputEvents.size()); - for (const UIInputEvent& event : inputEvents) { - switch (event.type) { - case UIInputEventType::PointerMove: - case UIInputEventType::PointerEnter: - case UIInputEventType::PointerLeave: - case UIInputEventType::PointerButtonDown: - case UIInputEventType::PointerButtonUp: - case UIInputEventType::PointerWheel: - break; - default: - filteredEvents.push_back(event); - break; - } - } - return filteredEvents; -} - } // namespace int Application::Run(HINSTANCE hInstance, int nCmdShow) { @@ -441,28 +377,13 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { InitializeUIEditorRuntimeTrace(logRoot); SetUnhandledExceptionFilter(&Application::HandleUnhandledException); LogRuntimeTrace("app", "initialize begin"); - m_shellAsset = BuildProductShellAsset(repoRoot); - m_shellValidation = ValidateEditorShellAsset(m_shellAsset); - m_validationMessage = m_shellValidation.message; - if (!m_shellValidation.IsValid()) { - LogRuntimeTrace("app", "shell asset validation failed: " + m_validationMessage); + if (!m_editorContext.Initialize(repoRoot, *this)) { + LogRuntimeTrace( + "app", + "shell asset validation failed: " + m_editorContext.GetValidationMessage()); return false; } - m_workspaceController = UIEditorWorkspaceController( - m_shellAsset.panelRegistry, - m_shellAsset.workspace, - m_shellAsset.workspaceSession); - m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); - m_shortcutManager.SetHostCommandHandler(this); - m_shellServices = {}; - m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); - m_shellServices.shortcutManager = &m_shortcutManager; - m_shellServices.textMeasurer = &m_renderer; - m_lastStatus = "Ready"; - m_lastMessage = "Old editor shell baseline loaded."; - LogRuntimeTrace("app", "workspace initialized: " + DescribeWorkspaceState()); - WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW; @@ -505,24 +426,23 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { LogRuntimeTrace("app", "renderer initialization failed"); return false; } - m_builtInIcons.Initialize(m_renderer, repoRoot); - if (!m_builtInIcons.GetLastError().empty()) { - LogRuntimeTrace("icons", m_builtInIcons.GetLastError()); + m_editorContext.AttachTextMeasurer(m_renderer); + m_editorWorkspace.Initialize(repoRoot, m_renderer); + if (!m_editorWorkspace.GetBuiltInIconError().empty()) { + LogRuntimeTrace("icons", m_editorWorkspace.GetBuiltInIconError()); } - m_hierarchyPanel.SetBuiltInIcons(&m_builtInIcons); - m_projectPanel.SetBuiltInIcons(&m_builtInIcons); - m_hierarchyPanel.Initialize(); - m_projectPanel.SetTextMeasurer(&m_renderer); - m_projectPanel.Initialize(repoRoot); + LogRuntimeTrace( + "app", + "workspace initialized: " + + m_editorContext.DescribeWorkspaceState(m_editorWorkspace.GetShellInteractionState())); ShowWindow(m_hwnd, nCmdShow); UpdateWindow(m_hwnd); - m_autoScreenshot.Initialize(m_shellAsset.captureRootPath); + m_autoScreenshot.Initialize(m_editorContext.GetShellAsset().captureRootPath); if (IsAutoCaptureOnStartupEnabled()) { m_autoScreenshot.RequestCapture("startup"); - m_lastStatus = "Capture"; - m_lastMessage = "Startup capture requested."; + m_editorContext.SetStatus("Capture", "Startup capture requested."); } LogRuntimeTrace("app", "initialize completed"); return true; @@ -535,7 +455,7 @@ void Application::Shutdown() { } m_autoScreenshot.Shutdown(); - m_builtInIcons.Shutdown(); + m_editorWorkspace.Shutdown(); m_renderer.Shutdown(); if (m_hwnd != nullptr && IsWindow(m_hwnd)) { @@ -572,80 +492,50 @@ void Application::RenderFrame() { UIRect(0.0f, 0.0f, width, height), UIColor(0.10f, 0.10f, 0.10f, 1.0f)); - if (m_shellValidation.IsValid()) { - const auto& metrics = ResolveUIEditorShellInteractionMetrics(); - const auto& palette = ResolveUIEditorShellInteractionPalette(); - const UIEditorShellInteractionDefinition definition = BuildShellDefinition(); + if (m_editorContext.IsValid()) { std::vector frameEvents = std::move(m_pendingInputEvents); m_pendingInputEvents.clear(); - const std::vector hostedContentEvents = frameEvents; - const std::vector shellEvents = - m_projectPanel.HasActivePointerCapture() - ? FilterShellInputEventsForHostedContentCapture(frameEvents) - : frameEvents; if (!frameEvents.empty()) { LogRuntimeTrace( "input", - DescribeInputEvents(frameEvents) + " | " + DescribeWorkspaceState()); + DescribeInputEvents(frameEvents) + " | " + + m_editorContext.DescribeWorkspaceState( + m_editorWorkspace.GetShellInteractionState())); } - m_shellFrame = UpdateUIEditorShellInteraction( - m_shellInteractionState, - m_workspaceController, + m_editorWorkspace.Update( + m_editorContext, UIRect(0.0f, 0.0f, width, height), - definition, - shellEvents, - m_shellServices, - metrics); - if (!shellEvents.empty() || - m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged || - m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted) { + frameEvents, + BuildCaptureStatusText()); + const UIEditorShellInteractionFrame& shellFrame = + m_editorWorkspace.GetShellFrame(); + if (!frameEvents.empty() || + shellFrame.result.workspaceResult.dockHostResult.layoutChanged || + shellFrame.result.workspaceResult.dockHostResult.commandExecuted) { std::ostringstream frameTrace = {}; frameTrace << "result consumed=" - << (m_shellFrame.result.consumed ? "true" : "false") + << (shellFrame.result.consumed ? "true" : "false") << " layoutChanged=" - << (m_shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false") + << (shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false") << " commandExecuted=" - << (m_shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") + << (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") << " active=" - << m_workspaceController.GetWorkspace().activePanelId + << m_editorContext.GetWorkspaceController().GetWorkspace().activePanelId << " message=" - << m_shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; + << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; LogRuntimeTrace( "frame", frameTrace.str()); } - ApplyHostCaptureRequests(m_shellFrame.result); - UpdateLastStatus(m_shellFrame.result); - m_hierarchyPanel.Update( - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, - hostedContentEvents, - !m_shellFrame.result.workspaceInputSuppressed, - m_workspaceController.GetWorkspace().activePanelId == "hierarchy"); - m_projectPanel.Update( - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, - hostedContentEvents, - !m_shellFrame.result.workspaceInputSuppressed, - m_workspaceController.GetWorkspace().activePanelId == "project"); - for (const App::ProductProjectPanel::Event& event : m_projectPanel.GetFrameEvents()) { + ApplyHostCaptureRequests(shellFrame.result); + for (const App::ProductProjectPanel::Event& event : m_editorWorkspace.GetProjectPanelEvents()) { LogRuntimeTrace("project", DescribeProjectPanelEvent(event)); - m_lastStatus = "Project"; - m_lastMessage = DescribeProjectPanelEvent(event); + m_editorContext.SetStatus("Project", DescribeProjectPanelEvent(event)); } ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); - const UIEditorShellComposeModel shellComposeModel = - BuildShellComposeModelFromFrame(m_shellFrame); - AppendUIEditorShellCompose( - drawList, - m_shellFrame.shellFrame, - shellComposeModel, - m_shellInteractionState.composeState, - palette.shellPalette, - metrics.shellMetrics); - m_hierarchyPanel.Append(drawList); - m_projectPanel.Append(drawList); - AppendShellPopups(drawList, m_shellFrame, palette, metrics); + m_editorWorkspace.Append(drawList); } else { drawList.AddText( UIPoint(28.0f, 28.0f), @@ -654,7 +544,9 @@ void Application::RenderFrame() { 16.0f); drawList.AddText( UIPoint(28.0f, 54.0f), - m_validationMessage.empty() ? std::string("Unknown validation error.") : m_validationMessage, + m_editorContext.GetValidationMessage().empty() + ? std::string("Unknown validation error.") + : m_editorContext.GetValidationMessage(), UIColor(0.72f, 0.72f, 0.72f, 1.0f), 12.0f); } @@ -695,7 +587,7 @@ bool Application::IsPointerInsideClientArea() const { } LPCWSTR Application::ResolveCurrentCursorResource() const { - switch (m_projectPanel.GetCursorKind()) { + switch (m_editorWorkspace.GetHostedContentCursorKind()) { case App::ProductProjectPanel::CursorKind::ResizeEW: return IDC_SIZEWE; case App::ProductProjectPanel::CursorKind::Arrow: @@ -703,8 +595,7 @@ LPCWSTR Application::ResolveCurrentCursorResource() const { break; } - switch (Widgets::ResolveUIEditorDockHostCursorKind( - m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout)) { + switch (m_editorWorkspace.GetDockCursorKind()) { case Widgets::UIEditorDockHostCursorKind::ResizeEW: return IDC_SIZEWE; case Widgets::UIEditorDockHostCursorKind::ResizeNS: @@ -735,38 +626,28 @@ UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const { PixelsToDips(static_cast(y))); } +std::string Application::BuildCaptureStatusText() const { + if (m_autoScreenshot.HasPendingCapture()) { + return "Shot pending..."; + } + + if (!m_autoScreenshot.GetLastCaptureError().empty()) { + return TruncateText(m_autoScreenshot.GetLastCaptureError(), 38u); + } + + if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { + return TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 38u); + } + + return {}; +} + void Application::LogRuntimeTrace( std::string_view channel, std::string_view message) const { AppendUIEditorRuntimeTrace(channel, message); } -std::string Application::DescribeWorkspaceState() const { - std::ostringstream stream = {}; - stream << "active=" << m_workspaceController.GetWorkspace().activePanelId; - const auto visiblePanels = - CollectUIEditorWorkspaceVisiblePanels( - m_workspaceController.GetWorkspace(), - m_workspaceController.GetSession()); - stream << " visible=["; - for (std::size_t index = 0; index < visiblePanels.size(); ++index) { - if (index > 0u) { - stream << ','; - } - stream << visiblePanels[index].panelId; - } - stream << ']'; - - const auto& dockState = - m_shellInteractionState.workspaceInteractionState.dockHostInteractionState; - stream << " dragNode=" << dockState.activeTabDragNodeId; - stream << " dragPanel=" << dockState.activeTabDragPanelId; - if (dockState.dockHostState.dropPreview.visible) { - stream << " dropTarget=" << dockState.dockHostState.dropPreview.targetNodeId; - } - return stream.str(); -} - std::string Application::DescribeInputEvents( const std::vector& events) const { std::ostringstream stream = {}; @@ -825,116 +706,19 @@ void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& } void Application::ApplyHostedContentCaptureRequests() { - if (m_projectPanel.WantsHostPointerCapture() && GetCapture() != m_hwnd) { + if (m_editorWorkspace.WantsHostPointerCapture() && GetCapture() != m_hwnd) { SetCapture(m_hwnd); } - if (m_projectPanel.WantsHostPointerRelease() && + if (m_editorWorkspace.WantsHostPointerRelease() && GetCapture() == m_hwnd && - !HasShellInteractiveCaptureState()) { + !m_editorWorkspace.HasShellInteractiveCapture()) { ReleaseCapture(); } } -bool Application::HasShellInteractiveCaptureState() 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; -} - bool Application::HasInteractiveCaptureState() const { - return HasShellInteractiveCaptureState() || m_projectPanel.HasActivePointerCapture(); -} - -UIEditorShellInteractionDefinition Application::BuildShellDefinition() const { - std::string statusText = m_lastStatus; - if (!m_lastMessage.empty()) { - statusText += statusText.empty() ? m_lastMessage : ": " + m_lastMessage; - } - - std::string captureText = {}; - if (m_autoScreenshot.HasPendingCapture()) { - captureText = "Shot pending..."; - } else if (!m_autoScreenshot.GetLastCaptureError().empty()) { - captureText = TruncateText(m_autoScreenshot.GetLastCaptureError(), 38u); - } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { - captureText = TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 38u); - } - - return BuildProductShellInteractionDefinition( - m_shellAsset, - m_workspaceController, - statusText, - captureText); -} - -void Application::UpdateLastStatus(const UIEditorShellInteractionResult& result) { - if (result.commandDispatched) { - m_lastStatus = std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)); - m_lastMessage = result.commandDispatchResult.message.empty() - ? result.commandDispatchResult.displayName - : result.commandDispatchResult.message; - return; - } - - if (result.workspaceResult.dockHostResult.layoutChanged) { - m_lastStatus = "Layout"; - m_lastMessage = result.workspaceResult.dockHostResult.layoutResult.message; - return; - } - - if (result.workspaceResult.dockHostResult.commandExecuted) { - m_lastStatus = "Workspace"; - m_lastMessage = result.workspaceResult.dockHostResult.commandResult.message; - return; - } - - if (!result.viewportPanelId.empty()) { - m_lastStatus = result.viewportPanelId; - if (result.viewportInputFrame.captureStarted) { - m_lastMessage = "Viewport capture started."; - } else if (result.viewportInputFrame.captureEnded) { - m_lastMessage = "Viewport capture ended."; - } else if (result.viewportInputFrame.focusGained) { - m_lastMessage = "Viewport focused."; - } else if (result.viewportInputFrame.focusLost) { - m_lastMessage = "Viewport focus lost."; - } else if (result.viewportInputFrame.pointerPressedInside) { - m_lastMessage = "Viewport pointer down."; - } else if (result.viewportInputFrame.pointerReleasedInside) { - m_lastMessage = "Viewport pointer up."; - } else if (result.viewportInputFrame.pointerMoved) { - m_lastMessage = "Viewport pointer move."; - } else if (result.viewportInputFrame.wheelDelta != 0.0f) { - m_lastMessage = "Viewport wheel."; - } - return; - } - - if (result.menuMutation.changed) { - if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { - m_lastStatus = "Menu"; - m_lastMessage = result.itemId + " opened child popup."; - } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { - m_lastStatus = "Menu"; - m_lastMessage = result.menuId + " opened."; - } else { - m_lastStatus = "Menu"; - m_lastMessage = "Popup chain dismissed."; - } - } + return m_editorWorkspace.HasInteractiveCapture(); } void Application::QueuePointerEvent( @@ -1082,8 +866,7 @@ UIEditorHostCommandDispatchResult Application::DispatchHostCommand( if (commandId == "help.about") { result.commandExecuted = true; result.message = "About dialog will be wired after modal layer lands."; - m_lastStatus = "About"; - m_lastMessage = result.message; + m_editorContext.SetStatus("About", result.message); return result; } diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index dc5a3829..46367fa9 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -8,14 +8,10 @@ #include #include -#include "Icons/ProductBuiltInIcons.h" -#include "Panels/ProductHierarchyPanel.h" -#include "Panels/ProductProjectPanel.h" +#include "Core/ProductEditorContext.h" +#include "Workspace/ProductEditorWorkspace.h" -#include -#include #include -#include #include #include @@ -53,14 +49,11 @@ private: float GetDpiScale() const; float PixelsToDips(float pixels) const; ::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const; + std::string BuildCaptureStatusText() const; void LogRuntimeTrace(std::string_view channel, std::string_view message) const; void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); void ApplyHostedContentCaptureRequests(); - bool HasShellInteractiveCaptureState() const; bool HasInteractiveCaptureState() const; - UIEditorShellInteractionDefinition BuildShellDefinition() const; - void UpdateLastStatus(const UIEditorShellInteractionResult& result); - std::string DescribeWorkspaceState() const; std::string DescribeInputEvents( const std::vector<::XCEngine::UI::UIInputEvent>& events) const; void QueuePointerEvent( @@ -82,21 +75,10 @@ private: ::XCEngine::UI::Editor::Host::NativeRenderer m_renderer = {}; ::XCEngine::UI::Editor::Host::AutoScreenshotController m_autoScreenshot = {}; ::XCEngine::UI::Editor::Host::InputModifierTracker m_inputModifierTracker = {}; - EditorShellAsset m_shellAsset = {}; - EditorShellAssetValidationResult m_shellValidation = {}; - UIEditorWorkspaceController m_workspaceController = {}; - UIEditorShortcutManager m_shortcutManager = {}; - App::ProductBuiltInIcons m_builtInIcons = {}; - App::ProductHierarchyPanel m_hierarchyPanel = {}; - App::ProductProjectPanel m_projectPanel = {}; - UIEditorShellInteractionServices m_shellServices = {}; - UIEditorShellInteractionState m_shellInteractionState = {}; - UIEditorShellInteractionFrame m_shellFrame = {}; + App::ProductEditorContext m_editorContext = {}; + App::ProductEditorWorkspace m_editorWorkspace = {}; std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {}; bool m_trackingMouseLeave = false; - std::string m_validationMessage = {}; - std::string m_lastStatus = {}; - std::string m_lastMessage = {}; UINT m_windowDpi = 96u; float m_dpiScale = 1.0f; }; diff --git a/new_editor/app/Core/ProductEditorContext.cpp b/new_editor/app/Core/ProductEditorContext.cpp new file mode 100644 index 00000000..9dc2420a --- /dev/null +++ b/new_editor/app/Core/ProductEditorContext.cpp @@ -0,0 +1,187 @@ +#include "ProductEditorContext.h" + +#include "Shell/ProductShellAsset.h" + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager; + +std::string ComposeStatusText( + std::string_view status, + std::string_view message) { + if (status.empty()) { + return std::string(message); + } + + if (message.empty()) { + return std::string(status); + } + + return std::string(status) + ": " + std::string(message); +} + +} // namespace + +bool ProductEditorContext::Initialize( + const std::filesystem::path& repoRoot, + UIEditorHostCommandHandler& hostCommandHandler) { + m_shellAsset = BuildProductShellAsset(repoRoot); + m_shellValidation = ValidateEditorShellAsset(m_shellAsset); + if (!m_shellValidation.IsValid()) { + return false; + } + + m_workspaceController = UIEditorWorkspaceController( + m_shellAsset.panelRegistry, + m_shellAsset.workspace, + m_shellAsset.workspaceSession); + m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); + m_shortcutManager.SetHostCommandHandler(&hostCommandHandler); + m_shellServices = {}; + m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); + m_shellServices.shortcutManager = &m_shortcutManager; + SetReadyStatus(); + return true; +} + +void ProductEditorContext::AttachTextMeasurer( + const UIEditorTextMeasurer& textMeasurer) { + m_shellServices.textMeasurer = &textMeasurer; +} + +bool ProductEditorContext::IsValid() const { + return m_shellValidation.IsValid(); +} + +const std::string& ProductEditorContext::GetValidationMessage() const { + return m_shellValidation.message; +} + +const EditorShellAsset& ProductEditorContext::GetShellAsset() const { + return m_shellAsset; +} + +UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() { + return m_workspaceController; +} + +const UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() const { + return m_workspaceController; +} + +const UIEditorShellInteractionServices& ProductEditorContext::GetShellServices() const { + return m_shellServices; +} + +UIEditorShellInteractionDefinition ProductEditorContext::BuildShellDefinition( + std::string_view captureText) const { + return BuildProductShellInteractionDefinition( + m_shellAsset, + m_workspaceController, + ComposeStatusText(m_lastStatus, m_lastMessage), + captureText); +} + +void ProductEditorContext::SetReadyStatus() { + SetStatus("Ready", "Old editor shell baseline loaded."); +} + +void ProductEditorContext::SetStatus( + std::string status, + std::string message) { + m_lastStatus = std::move(status); + m_lastMessage = std::move(message); +} + +void ProductEditorContext::UpdateStatusFromShellResult( + const UIEditorShellInteractionResult& result) { + if (result.commandDispatched) { + SetStatus( + std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)), + result.commandDispatchResult.message.empty() + ? result.commandDispatchResult.displayName + : result.commandDispatchResult.message); + return; + } + + if (result.workspaceResult.dockHostResult.layoutChanged) { + SetStatus("Layout", result.workspaceResult.dockHostResult.layoutResult.message); + return; + } + + if (result.workspaceResult.dockHostResult.commandExecuted) { + SetStatus("Workspace", result.workspaceResult.dockHostResult.commandResult.message); + return; + } + + if (!result.viewportPanelId.empty()) { + std::string message = {}; + if (result.viewportInputFrame.captureStarted) { + message = "Viewport capture started."; + } else if (result.viewportInputFrame.captureEnded) { + message = "Viewport capture ended."; + } else if (result.viewportInputFrame.focusGained) { + message = "Viewport focused."; + } else if (result.viewportInputFrame.focusLost) { + message = "Viewport focus lost."; + } else if (result.viewportInputFrame.pointerPressedInside) { + message = "Viewport pointer down."; + } else if (result.viewportInputFrame.pointerReleasedInside) { + message = "Viewport pointer up."; + } else if (result.viewportInputFrame.pointerMoved) { + message = "Viewport pointer move."; + } else if (result.viewportInputFrame.wheelDelta != 0.0f) { + message = "Viewport wheel."; + } + + if (!message.empty()) { + SetStatus(result.viewportPanelId, std::move(message)); + } + return; + } + + if (result.menuMutation.changed) { + if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { + SetStatus("Menu", result.itemId + " opened child popup."); + } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { + SetStatus("Menu", result.menuId + " opened."); + } else { + SetStatus("Menu", "Popup chain dismissed."); + } + } +} + +std::string ProductEditorContext::DescribeWorkspaceState( + const UIEditorShellInteractionState& interactionState) const { + std::ostringstream stream = {}; + stream << "active=" << m_workspaceController.GetWorkspace().activePanelId; + const auto visiblePanels = + CollectUIEditorWorkspaceVisiblePanels( + m_workspaceController.GetWorkspace(), + m_workspaceController.GetSession()); + stream << " visible=["; + for (std::size_t index = 0; index < visiblePanels.size(); ++index) { + if (index > 0u) { + stream << ','; + } + stream << visiblePanels[index].panelId; + } + stream << ']'; + + const auto& dockState = + interactionState.workspaceInteractionState.dockHostInteractionState; + stream << " dragNode=" << dockState.activeTabDragNodeId; + stream << " dragPanel=" << dockState.activeTabDragPanelId; + if (dockState.dockHostState.dropPreview.visible) { + stream << " dropTarget=" << dockState.dockHostState.dropPreview.targetNodeId; + } + return stream.str(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Core/ProductEditorContext.h b/new_editor/app/Core/ProductEditorContext.h new file mode 100644 index 00000000..f80794ef --- /dev/null +++ b/new_editor/app/Core/ProductEditorContext.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +class UIEditorHostCommandHandler; +struct UIEditorTextMeasurer; + +} // namespace XCEngine::UI::Editor + +namespace XCEngine::UI::Editor::App { + +class ProductEditorContext { +public: + bool Initialize( + const std::filesystem::path& repoRoot, + UIEditorHostCommandHandler& hostCommandHandler); + void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer); + + bool IsValid() const; + const std::string& GetValidationMessage() const; + const EditorShellAsset& GetShellAsset() const; + + UIEditorWorkspaceController& GetWorkspaceController(); + const UIEditorWorkspaceController& GetWorkspaceController() const; + const UIEditorShellInteractionServices& GetShellServices() const; + + UIEditorShellInteractionDefinition BuildShellDefinition( + std::string_view captureText) const; + + void SetReadyStatus(); + void SetStatus(std::string status, std::string message); + void UpdateStatusFromShellResult(const UIEditorShellInteractionResult& result); + std::string DescribeWorkspaceState( + const UIEditorShellInteractionState& interactionState) const; + +private: + EditorShellAsset m_shellAsset = {}; + EditorShellAssetValidationResult m_shellValidation = {}; + UIEditorWorkspaceController m_workspaceController = {}; + UIEditorShortcutManager m_shortcutManager = {}; + UIEditorShellInteractionServices m_shellServices = {}; + std::string m_lastStatus = {}; + std::string m_lastMessage = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.cpp b/new_editor/app/Workspace/ProductEditorWorkspace.cpp new file mode 100644 index 00000000..f7946fba --- /dev/null +++ b/new_editor/app/Workspace/ProductEditorWorkspace.cpp @@ -0,0 +1,210 @@ +#include "ProductEditorWorkspace.h" + +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; + +UIEditorShellComposeModel BuildShellComposeModelFromFrame( + const UIEditorShellInteractionFrame& frame) { + UIEditorShellComposeModel model = {}; + model.menuBarItems = frame.request.menuBarItems; + model.toolbarButtons = frame.model.toolbarButtons; + model.statusSegments = frame.model.statusSegments; + model.workspacePresentations = frame.model.workspacePresentations; + return model; +} + +void AppendShellPopups( + UIDrawList& drawList, + const UIEditorShellInteractionFrame& frame, + const UIEditorShellInteractionPalette& palette, + const UIEditorShellInteractionMetrics& metrics) { + const std::size_t popupCount = + (std::min)(frame.request.popupRequests.size(), frame.popupFrames.size()); + for (std::size_t index = 0; index < popupCount; ++index) { + const UIEditorShellInteractionPopupRequest& popupRequest = + frame.request.popupRequests[index]; + const UIEditorShellInteractionPopupFrame& popupFrame = + frame.popupFrames[index]; + Widgets::AppendUIEditorMenuPopupBackground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + Widgets::AppendUIEditorMenuPopupForeground( + drawList, + popupRequest.layout, + popupRequest.widgetItems, + popupFrame.popupState, + palette.popupPalette, + metrics.popupMetrics); + } +} + +std::vector FilterShellInputEventsForHostedContentCapture( + const std::vector& inputEvents) { + std::vector filteredEvents = {}; + filteredEvents.reserve(inputEvents.size()); + for (const UIInputEvent& event : inputEvents) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + break; + default: + filteredEvents.push_back(event); + break; + } + } + return filteredEvents; +} + +} // namespace + +void ProductEditorWorkspace::Initialize( + const std::filesystem::path& repoRoot, + Host::NativeRenderer& renderer) { + m_builtInIcons.Initialize(renderer, repoRoot); + m_hierarchyPanel.SetBuiltInIcons(&m_builtInIcons); + m_projectPanel.SetBuiltInIcons(&m_builtInIcons); + m_projectPanel.SetTextMeasurer(&renderer); + m_hierarchyPanel.Initialize(); + m_projectPanel.Initialize(repoRoot); +} + +void ProductEditorWorkspace::Shutdown() { + m_shellFrame = {}; + m_shellInteractionState = {}; + m_builtInIcons.Shutdown(); +} + +void ProductEditorWorkspace::Update( + ProductEditorContext& context, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + std::string_view captureText) { + const auto& metrics = ResolveUIEditorShellInteractionMetrics(); + const UIEditorShellInteractionDefinition definition = + context.BuildShellDefinition(captureText); + const std::vector hostedContentEvents = inputEvents; + const std::vector shellEvents = + HasHostedContentCapture() + ? FilterShellInputEventsForHostedContentCapture(inputEvents) + : inputEvents; + + m_shellFrame = UpdateUIEditorShellInteraction( + m_shellInteractionState, + context.GetWorkspaceController(), + bounds, + definition, + shellEvents, + context.GetShellServices(), + metrics); + context.UpdateStatusFromShellResult(m_shellFrame.result); + + const std::string& activePanelId = + context.GetWorkspaceController().GetWorkspace().activePanelId; + m_hierarchyPanel.Update( + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + !m_shellFrame.result.workspaceInputSuppressed, + activePanelId == "hierarchy"); + m_projectPanel.Update( + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + !m_shellFrame.result.workspaceInputSuppressed, + activePanelId == "project"); +} + +void ProductEditorWorkspace::Append(UIDrawList& drawList) const { + const auto& metrics = ResolveUIEditorShellInteractionMetrics(); + const auto& palette = ResolveUIEditorShellInteractionPalette(); + const UIEditorShellComposeModel shellComposeModel = + BuildShellComposeModelFromFrame(m_shellFrame); + AppendUIEditorShellCompose( + drawList, + m_shellFrame.shellFrame, + shellComposeModel, + m_shellInteractionState.composeState, + palette.shellPalette, + metrics.shellMetrics); + m_hierarchyPanel.Append(drawList); + m_projectPanel.Append(drawList); + AppendShellPopups(drawList, m_shellFrame, palette, metrics); +} + +const UIEditorShellInteractionFrame& ProductEditorWorkspace::GetShellFrame() const { + return m_shellFrame; +} + +const UIEditorShellInteractionState& ProductEditorWorkspace::GetShellInteractionState() const { + return m_shellInteractionState; +} + +const std::vector& ProductEditorWorkspace::GetProjectPanelEvents() const { + return m_projectPanel.GetFrameEvents(); +} + +const std::string& ProductEditorWorkspace::GetBuiltInIconError() const { + return m_builtInIcons.GetLastError(); +} + +ProductProjectPanel::CursorKind ProductEditorWorkspace::GetHostedContentCursorKind() const { + return m_projectPanel.GetCursorKind(); +} + +Widgets::UIEditorDockHostCursorKind ProductEditorWorkspace::GetDockCursorKind() const { + return Widgets::ResolveUIEditorDockHostCursorKind( + m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout); +} + +bool ProductEditorWorkspace::WantsHostPointerCapture() const { + return m_projectPanel.WantsHostPointerCapture(); +} + +bool ProductEditorWorkspace::WantsHostPointerRelease() const { + return m_projectPanel.WantsHostPointerRelease(); +} + +bool ProductEditorWorkspace::HasHostedContentCapture() const { + return m_projectPanel.HasActivePointerCapture(); +} + +bool ProductEditorWorkspace::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; +} + +bool ProductEditorWorkspace::HasInteractiveCapture() const { + return HasShellInteractiveCapture() || HasHostedContentCapture(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.h b/new_editor/app/Workspace/ProductEditorWorkspace.h new file mode 100644 index 00000000..83ede2b5 --- /dev/null +++ b/new_editor/app/Workspace/ProductEditorWorkspace.h @@ -0,0 +1,55 @@ +#pragma once + +#include "Core/ProductEditorContext.h" +#include "Icons/ProductBuiltInIcons.h" +#include "Panels/ProductHierarchyPanel.h" +#include "Panels/ProductProjectPanel.h" + +#include + +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { + +class ProductEditorWorkspace { +public: + void Initialize( + const std::filesystem::path& repoRoot, + Host::NativeRenderer& renderer); + void Shutdown(); + + void Update( + ProductEditorContext& context, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + std::string_view captureText); + void Append(::XCEngine::UI::UIDrawList& drawList) const; + + const UIEditorShellInteractionFrame& GetShellFrame() const; + const UIEditorShellInteractionState& GetShellInteractionState() const; + const std::vector& GetProjectPanelEvents() const; + const std::string& GetBuiltInIconError() const; + + ProductProjectPanel::CursorKind GetHostedContentCursorKind() const; + Widgets::UIEditorDockHostCursorKind GetDockCursorKind() const; + bool WantsHostPointerCapture() const; + bool WantsHostPointerRelease() const; + bool HasHostedContentCapture() const; + bool HasShellInteractiveCapture() const; + bool HasInteractiveCapture() const; + +private: + ProductBuiltInIcons m_builtInIcons = {}; + ProductHierarchyPanel m_hierarchyPanel = {}; + ProductProjectPanel m_projectPanel = {}; + UIEditorShellInteractionState m_shellInteractionState = {}; + UIEditorShellInteractionFrame m_shellFrame = {}; +}; + +} // namespace XCEngine::UI::Editor::App