diff --git a/docs/used/NewEditor_ColorPicker独立面板重构计划_2026-04-20.md b/docs/used/NewEditor_ColorPicker独立面板重构计划_2026-04-20.md new file mode 100644 index 00000000..0a814e00 --- /dev/null +++ b/docs/used/NewEditor_ColorPicker独立面板重构计划_2026-04-20.md @@ -0,0 +1,222 @@ +# NewEditor Color Picker 独立面板重构计划 +日期: `2026-04-20` + +## 1. 目标 + +本次只解决 `new_editor` 当前颜色选择器的根源性架构问题。 + +目标不是继续修补 `Inspector` 里的颜色字段弹窗,而是把 `Color Picker` 重建为一个真正的独立面板,并复用现有的分离标签页 / 分离窗口主线。 + +完成后应满足: + +- `Inspector` 里的颜色字段只负责显示当前值和发起打开请求 +- 颜色编辑 UI 只存在于独立的 `Color Picker` 面板中 +- `Color Picker` 通过现有 detached window 基础设施打开 +- 不再依赖 `PropertyGrid` 行内 popup 生命周期 +- 不再为了颜色选择器单开一套平行窗口系统 + +## 2. 根因结论 + +### 2.1 当前颜色选择器的宿主边界是错的 + +现在的颜色选择器被建模成了 `PropertyGrid ColorField` 的内联 popup: + +- 字段自己持有 popup 开关状态 +- 字段自己做 popup 布局 +- 字段自己做 popup 命中测试 +- 字段自己做 popup 拖动交互 +- `PropertyGrid` 自己参与 popup 关闭和分发 + +这说明当前颜色选择器不是一个独立工具面板,而是被硬塞进了字段控件层。 + +### 2.2 真正缺的不是窗口能力,而是 panel 身份 + +项目里已经有完整的 detached panel / detached window 主线: + +- `UIEditorPanelRegistry` +- `UIEditorPanelContentHost` +- `UIEditorWindowWorkspaceController` +- `EditorWindowWorkspaceCoordinator` +- `EditorWindowHostRuntime` + +所以问题不在“怎么开一个新窗口”,而在“颜色选择器根本没有被建模成一个 panel”。 + +### 2.3 不能靠把 Color Picker 塞回主 workspace 树解决 + +现有 `OpenPanel` 只适用于已经存在于 workspace 树和 session 里的 panel。 + +如果为了颜色选择器去伪造一个隐藏节点,再靠打开命令拉出来,本质上还是绕路: + +- 主 workspace 会被无关工具污染 +- session 约束会变复杂 +- 工具窗口语义会和主编辑布局耦合 + +正确做法是直接用 panel descriptor 构造一个单面板 detached workspace,并交给现有窗口协调器同步。 + +## 3. 重构原则 + +本次必须遵守: + +1. 不再继续扩展 `PropertyGrid` 的颜色 popup 分支 +2. 不再让 `ColorField` 拥有独立工具 UI 的完整生命周期 +3. 优先复用现有 panel / detached window 基础设施 +4. 不引入第二套平行“tool window”系统 +5. 不把 `Color Picker` 强行塞进默认主 workspace 布局 + +## 4. 目标结构 + +重构后应形成下面的边界: + +### 4.1 Inspector ColorField + +职责只保留: + +- 显示 swatch / 文本值 +- 响应点击 +- 发出“打开 Color Picker”的请求 +- 在目标匹配时读取共享颜色状态并回写组件字段 + +不再负责: + +- popup 布局 +- popup 绘制 +- popup 拖动 +- popup 关闭 + +### 4.2 EditorContext 中的共享工具状态 + +新增共享 `Color Picker` 状态,放在 `EditorContext`,作为 Inspector 与 Color Picker 面板之间的唯一数据桥: + +- 当前是否激活 +- 当前编辑颜色 +- 是否显示 alpha +- 当前 inspector 目标 +- 打开独立面板请求 +- 版本号 / 修改序号 + +### 4.3 ColorPickerPanel + +新增 `HostedContent panel`: + +- 作为真正的颜色编辑 UI 宿主 +- 读取并更新共享 `Color Picker` 状态 +- 不依赖 `PropertyGrid` 内联 popup + +### 4.4 Window / Workspace 层 + +新增一条“直接把某个 panel 打开到新 detached window”能力: + +- 输入 `panelId` +- 用 panel descriptor 直接构造单 panel detached workspace +- 不要求该 panel 先存在于主 workspace 树 +- 复用现有 detached window 同步链路 + +## 5. 执行步骤 + +### 步骤 1:固化共享状态模型 + +- 把 `EditorColorPickerToolState` 接入 `EditorContext` +- 提供统一 getter +- 在 `Initialize` 时正确重置 + +完成标准: + +- 颜色选择器状态不再挂在 inspector 局部状态或字段视觉状态里 + +### 步骤 2:注册正式的 Color Picker panel + +- 在 `EditorPanelIds` 中保留正式 panel id/title +- 在 `EditorShellAssetBuilder` 中把 `Color Picker` 注册为 `HostedContent` +- 不加入默认主 workspace tree + +完成标准: + +- `Color Picker` 在架构层面是一个真实 panel,而不是字段副产物 + +### 步骤 3:扩展 detached panel 打开链路 + +- 给 `UIEditorWindowWorkspaceController` 增加“按 panelId 打开独立新窗口”的操作 +- 用 panel descriptor 构造 `UIEditorWorkspaceExtractedPanel` +- 复用 + - `BuildUIEditorDetachedWorkspaceFromExtractedPanel` + - `BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel` +- 给 `EditorWindowTransferRequests` + - `EditorWindowFrameOrchestrator` + - `EditorWindowWorkspaceCoordinator` + 增加对应请求传递与处理 + +完成标准: + +- 任意已注册 panel 都可以直接被拉起为 detached window +- 这条能力不依赖 tab drag,也不依赖主 workspace 里预先存在节点 + +### 步骤 4:实现 ColorPickerPanel + +- 新增独立 `ColorPickerPanel` +- 接入 `EditorShellRuntime` +- 通过 hosted content 挂载 +- 面板内容直接消费共享 `Color Picker` 状态 + +实现策略: + +- 优先复用现有颜色编辑 UI 的绘制 / 交互能力 +- 但这些能力必须由 `ColorPickerPanel` 驱动,而不是由 `PropertyGrid` 行内字段驱动 + +完成标准: + +- 真正的颜色编辑 UI 已经迁移到独立 panel + +### 步骤 5:重构 Inspector 接线 + +- `InspectorPanel` 访问 `EditorContext` 中的共享 `Color Picker` 状态 +- 颜色字段点击时: + - 写入目标信息 + - 写入初始颜色 + - 请求打开 detached `Color Picker` panel +- 每帧按目标匹配把共享颜色回写到对应字段 +- 继续沿用原有组件 editor 的 `ApplyFieldValue` 落地逻辑 + +完成标准: + +- Inspector 只做请求与回写,不再拥有颜色选择器 popup + +### 步骤 6:清理 PropertyGrid 颜色字段逻辑 + +- 移除 `ProcessColorFieldEvent` 对 popup 生命周期的依赖 +- 停止让 `UIEditorColorFieldInteraction` 在 property grid 里开 popup +- 颜色字段只保留选择 / 激活 / 打开请求语义 + +完成标准: + +- `PropertyGrid` 不再承载颜色工具面板 + +### 步骤 7:编译与验证 + +- 更新 `new_editor/CMakeLists.txt` +- 编译 `XCUIEditorApp` +- 验证以下路径: + - Inspector 点击颜色字段后拉起独立窗口 + - 修改颜色可实时或按预期回写 + - 重复点击同一字段不会产生混乱状态 + - 切换对象后目标绑定不串 + - 关闭颜色窗口后状态正常 + +## 6. 非目标 + +本次明确不做: + +- 顺手重写整个 `Inspector` +- 顺手把其他 popup 全改成独立窗口 +- 为颜色选择器发明独立于 panel/window 体系之外的新框架 +- 继续保留“字段内 popup + 独立窗口”双轨实现 + +## 7. 完成判定 + +只有下面条件全部满足,才算这次工作完成: + +- `Color Picker` 已经是正式 panel +- `Color Picker` 通过现有 detached window 主线打开 +- `Inspector ColorField` 不再持有 popup 主逻辑 +- `PropertyGrid` 不再承载颜色工具 UI 生命周期 +- 颜色值同步路径清晰,归属明确 +- 工程可编译,可运行,可验证 diff --git a/docs/used/NewEditor_InspectorLayout_RootRefactorPlan_2026-04-21.md b/docs/used/NewEditor_InspectorLayout_RootRefactorPlan_2026-04-21.md new file mode 100644 index 00000000..2a546a1e --- /dev/null +++ b/docs/used/NewEditor_InspectorLayout_RootRefactorPlan_2026-04-21.md @@ -0,0 +1,89 @@ +# NewEditor Inspector Layout Root Refactor Plan +Date: `2026-04-21` + +## Context + +This plan targets the `new_editor` Inspector field layout path only. + +Current symptom: +- Inspector field controls do not follow one explicit column policy. +- Different field types shift or compress at different thresholds. +- `Transform` and the fields below it stop aligning once the Inspector becomes narrow. + +Current root cause: +- Shared row layout code in `UIEditorFieldRowLayout` is guessing when a caller is "Inspector-like" and silently replacing the caller's requested minimum control width. +- That guess is based on generic layout numbers such as `controlColumnStart` and `labelControlGap`, which is an invalid ownership boundary. +- Vector fields also still carry a dead prefix palette chain that no longer affects rendering after the visible background was moved to `componentRects`. + +## Files In Scope + +- `new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h` +- `new_editor/src/Widgets/UIEditorFieldRowLayout.cpp` +- `new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h` +- `new_editor/include/XCEditor/Fields/UIEditorFieldStyle.h` +- `new_editor/src/Fields/UIEditorFieldStyle.cpp` +- Hosted Inspector field headers under `new_editor/include/XCEditor/Fields/` +- Hosted Inspector field sources under `new_editor/src/Fields/` + +## Refactor Goals + +1. Remove all implicit Inspector detection from shared row layout. +2. Make shared control-column width an explicit metric owned by PropertyGrid and forwarded to hosted fields. +3. Keep hosted field layout behavior consistent by policy instead of per-field hidden overrides. +4. Remove dead vector prefix palette data that no longer drives rendering. +5. Rebuild `XCUIEditor.exe` after the refactor and keep the worktree changes scoped to `new_editor`. + +## Implementation Plan + +### Stage 1: Make The Contract Explicit + +Add one explicit metric for hosted shared-column reservation: +- `sharedControlColumnMinWidth` + +Propagation chain: +- `UIEditorPropertyGridMetrics` +- hosted field metrics (`Bool`, `Number`, `Text`, `Enum`, `Color`, `Object`, `Asset`, `Vector2`, `Vector3`, `Vector4`) +- `UIEditorFieldRowLayoutMetrics` + +Rules: +- Default value outside Inspector stays `0.0f` +- Inspector-owned PropertyGrid metrics set the shared width explicitly +- Shared row layout consumes this field directly and never infers Inspector mode from unrelated metrics + +### Stage 2: Remove Hidden Width Guessing + +Replace the current behavior in `UIEditorFieldRowLayout.cpp`: +- delete the implicit `ResolveHostedControlMinimumWidth(...)` path +- delete the `inspectorHostedControlMinWidth` token +- make reserved control width depend only on explicit row-layout metrics and the caller-provided value + +Expected result: +- the row-layout contract becomes deterministic +- hosted fields stop getting different fallback behavior because of hidden special-cases + +### Stage 3: Clean Vector Dead Code + +Remove `prefixColor` and `prefixBorderColor` from: +- inspector field tokens +- vector field palette structs +- vector palette resolution code +- PropertyGrid palette builder forwarding + +Reason: +- those fields no longer affect visible rendering after the component background moved to `componentRects` + +### Stage 4: Validation + +Validation steps: +- compile `XCUIEditorApp` in `Debug` +- if the editor process is locking the output exe, kill `XCUIEditor` first +- verify the rebuilt output is `build/new_editor/Debug/XCUIEditor.exe` + +## Done Criteria + +This refactor is complete when: +- shared row layout contains no Inspector guessing logic +- Inspector column width policy is explicit in metrics +- hosted field metric forwarding is consistent for all field types +- vector dead palette chain is removed +- `XCUIEditor.exe` rebuild succeeds diff --git a/docs/used/NewEditor_Inspector字段编辑内核重构计划_2026-04-20.md b/docs/used/NewEditor_Inspector字段编辑内核重构计划_2026-04-20.md new file mode 100644 index 00000000..f1e29f63 --- /dev/null +++ b/docs/used/NewEditor_Inspector字段编辑内核重构计划_2026-04-20.md @@ -0,0 +1,408 @@ +# NewEditor Inspector字段编辑内核重构计划 +日期: `2026-04-20` + +## 1. 文档定位 + +这份计划只解决 `new_editor` Inspector 内联字段编辑这条链路的根因问题,不再继续沿着当前 `number / text / vector` 各自修补的路线前进。 + +覆盖范围: +- `new_editor/include/XCEditor/Fields/UIEditorNumberField*.h` +- `new_editor/include/XCEditor/Fields/UIEditorTextField*.h` +- `new_editor/include/XCEditor/Fields/UIEditorVector{2,3,4}Field*.h` +- `new_editor/include/XCEditor/Fields/UIEditorPropertyGridInteraction.h` +- `new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp` +- `new_editor/src/Fields/PropertyGridInteractionInternal.h` +- `new_editor/src/Fields/VectorFieldInteractionInternal.h` +- `new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h` + +对标参考: +- [editor/src/UI/ScalarControls.h](/D:/Xuanchi/Main/XCEngine/editor/src/UI/ScalarControls.h) +- [editor/src/UI/VectorControls.h](/D:/Xuanchi/Main/XCEngine/editor/src/UI/VectorControls.h) +- [editor/src/UI/PropertyGrid.h](/D:/Xuanchi/Main/XCEngine/editor/src/UI/PropertyGrid.h) + +这次重构的目标不是“再把几个 bug 修掉”,而是把字段编辑从现在这套分裂的状态机里拔出来,收敛成一条统一主链。 + +## 2. 根因结论 + +### 2.1 当前没有共享的字段编辑内核 + +现在的 `number`、`text`、`vector` 都各自维护一套点击、双击、拖动、文本编辑、提交、取消、光标闪烁逻辑。 + +结果是: +- 同一个交互语义在多个文件里重复实现 +- 一个行为改动需要同时改多个状态机 +- 修一个字段,另外几个字段容易继续偏离 +- 小问题会不断演化成“再加一点同步代码” + +这不是单点 bug,而是缺少共享内核导致的结构性问题。 + +### 2.2 编辑状态的所有权被拆成了多份 + +当前至少存在下面几层同时持有“编辑相关状态”: +- `UIEditorPropertyGridInteractionState` +- `UIEditorNumberFieldInteractionState` +- `UIEditorTextFieldInteractionState` +- `UIEditorVector{2,3,4}FieldInteractionState` +- `UIEditor*FieldState` +- `UITextInputState` +- `UIPropertyEditModel` + +这会直接导致: +- `caret` 既在文本输入状态里存在,又在字段视觉状态里存在 +- `displayText`、`stagedValue`、实际字段值分散在不同对象里 +- 一个字段是否处于编辑态,不是单一真值源 +- 某些帧里逻辑已经进入编辑,但渲染层还没完全同步 + +“双击后光标不立刻出现”就是这种分裂所有权的典型症状。 + +### 2.3 PropertyGrid 现在不是统一编排,而是在按字段类型手工分发 + +`UIEditorPropertyGridInteraction.cpp` 当前的工作方式,本质上是: +- 先跑 number 分支 +- 再跑 text 分支 +- 再跑 vector 分支 +- 再补一套全局编辑逻辑 +- 再补焦点、弹窗、键盘导航收尾 + +这会带来两个严重问题: +- 行为依赖处理顺序,而不是依赖明确的领域规则 +- `PropertyGrid` 本身已经知道太多字段细节,逐渐变成新的 God Object + +### 2.4 各字段把“领域语义”和“交互语义”混在了一起 + +对于 number / vector,真正属于字段自身的职责只有这些: +- 格式化值 +- 解析文本 +- 步进规则 +- 范围裁剪 +- 组件数量与索引规则 + +但现在每个字段实现同时还承担了: +- 指针命中 +- 双击检测 +- 拖动启动阈值 +- 拖拽中的增量计算 +- 文本缓冲同步 +- 光标闪烁同步 +- 提交/取消时机 + +这导致“字段种类一变,整套交互都要重写”。 + +### 2.5 当前结构已经出现了“共享逻辑伪装成临时 Internal helper”的迹象 + +`VectorFieldInteractionInternal.h` 这种大模板式内部头文件,说明代码已经在强行复用,但复用方式不稳定: +- 共享的是实现细节,不是明确边界 +- 逻辑被放进 header,难以形成单一实现入口 +- vector 虽然共享了一部分,但 number / text 仍然各走各的 + +这不是正式内核,只是重复逻辑开始外溢后的中间形态。 + +## 3. 重构目标 + +### 3.1 一级目标 + +1. 建立一套共享的 Inspector 字段编辑内核,成为 `number / text / vector` 唯一的交互主链。 +2. 把“编辑会话”收敛成单一真值源,彻底消除多份 `caret / staged text / editing flag` 的并行持有。 +3. 让 `PropertyGrid` 退回到编排层,只负责路由、布局、结果回写,不再承载每种字段的完整交互细节。 +4. 让 `number / text / vector` 退回到字段语义层,只保留 parse / format / clamp / component adapter 等职责。 +5. 以旧版 editor 的 Inspector 行为为准,恢复交互一致性。 + +### 3.2 二级目标 + +1. 删除“为了同步而同步”的附加状态。 +2. 删除基于字段类型的重复点击/双击/拖动状态机。 +3. 删除隐藏在 `Internal` 头文件中的大块共享交互实现。 +4. 保持文件边界收敛,不再继续把一个控件拆成更多零碎文件。 + +## 4. 非目标 + +这次重构明确不做下面这些事: +- 不顺手重写整个 Inspector +- 不改 bool / enum / asset / color 的交互模型,除非它们必须配合新的编辑会话接口 +- 不为了“通用表单框架”去抽象一个过度泛化的 UI 库 +- 不引入额外的分层包装和空壳接口 +- 不再新增大模板 `Internal.h` 来继续堆共享代码 + +## 5. 目标结构 + +### 5.1 核心原则 + +新的结构必须满足三条原则: +- 一个字段在某一时刻只有一个编辑会话真值源 +- 交互内核统一,字段类型只提供语义适配 +- 渲染层纯消费状态,不再自己补齐编辑逻辑 + +### 5.2 建议落地形态 + +引入一组正式的共享核心类型,放在 `Fields` 层内部正式实现,而不是继续散落在各字段实现里。 + +建议新增: +- `UIEditorEditableFieldCore.h` +- `UIEditorEditableFieldCore.cpp` + +核心只负责这些事情: +- 单击聚焦 +- 双击进入文本编辑 +- 左键拖动数值 scrub +- 激活阈值与双击判定 +- 文本缓冲 +- caret 位置 +- caret blink 起点 +- 提交与取消 +- 鼠标释放后结束拖动 +- 字段级 active / hovered / editing 会话切换 + +字段适配层只负责这些事情: +- 当前字段的基础值读取 +- 当前字段的文本格式化 +- 输入文本解析 +- 数值步进与范围收敛 +- vector 组件索引映射 + +### 5.3 PropertyGrid 的目标职责 + +`UIEditorPropertyGridInteraction` 重构后的职责应当只有: +- 为每一帧构建布局 +- 把命中的字段行转成“字段编辑核心输入” +- 调用共享编辑核心 +- 把核心输出回写到对应字段值 +- 维护选择、展开、弹窗等与字段编辑无关的上层状态 + +它不应该继续持有: +- 多套字段类型 interaction state vector +- 一套全局文本编辑状态再加若干字段局部文本编辑状态 +- 针对不同字段类型手工复制的事件处理分支 + +### 5.4 字段控件的目标职责 + +`UIEditorNumberField` / `UIEditorTextField` / `UIEditorVector*Field` 最终都应降为两类职责: +- 布局与绘制 +- 语义适配 + +它们不应该再各自拥有完整的编辑状态机。 + +### 5.5 文件边界约束 + +这次重构后,字段编辑这条链路必须满足下面的文件约束: +- 一个公开头文件对应一个实现 cpp +- 不再新增“一个功能拆成多个零散 cpp”的结构 +- 不再保留 `VectorFieldInteractionInternal.h` 这类承载大量实现逻辑的内部模板头 + +允许保留的形态是: +- 一个正式共享 core 对 +- 每个字段一个公开定义对 +- `PropertyGrid` 一个编排实现对 + +## 6. 具体改造步骤 + +### 阶段 1: 固化参考行为 + +目标: +- 先把要对齐的旧 editor 行为写成明确规则,避免重构过程中再靠记忆实现 + +要固定下来的行为: +- number/vector 单击只聚焦,不直接进入文本编辑 +- number/vector 左键拖动优先是 scrub +- number/vector 双击进入文本编辑 +- text 双击进入文本编辑 +- 进入编辑态的首帧必须可见 caret +- 鼠标释放后必须立即退出拖动 +- vector 多组件字段的交互语义与单数值字段一致,只是多了组件选择 + +完成标准: +- 把这些规则整理成代码内核的输入输出约束,而不是散在各字段注释里 + +### 阶段 2: 建立共享编辑会话模型 + +目标: +- 用一个正式的共享状态结构替换当前分裂状态 + +建议收敛为一个统一会话: +- `activeFieldId` +- `activeComponentIndex` +- `hoveredFieldId` +- `mode` +- `pointerDownPosition` +- `lastClickTimestamp` +- `lastClickPosition` +- `dragArmed` +- `dragActive` +- `dragStartValue` +- `textBuffer` +- `caret` +- `caretBlinkStartNanoseconds` + +这一阶段要做的事: +- 定义共享 state / result / input 结构 +- 明确什么是会话真值源 +- 明确字段视觉状态中哪些字段可以删掉 + +必须删除的重复所有权: +- `UIEditor*FieldState` 里重复保存的编辑真值 +- 各字段 interaction state 里各自维护的 `UITextInputState` +- 各字段 interaction state 里各自维护的 `UIPropertyEditModel` + +### 阶段 3: 提取统一交互内核 + +目标: +- 把点击、双击、拖动、编辑、提交、取消统一放入共享 core + +共享 core 应该提供的固定流程: +1. 解析命中目标 +2. 更新 hover / focus / pressed +3. 识别双击与拖动启动 +4. 根据字段适配器执行 scrub 或进入文本编辑 +5. 处理键盘输入、左右移动 caret、Backspace/Delete +6. 处理 Enter 提交、Escape 取消、失焦提交或取消 +7. 输出统一的字段变更结果 + +这一阶段结束后: +- `number` 不再自带一套点击和拖动状态机 +- `vector` 不再通过模板 internal header 自己跑一套编辑流程 +- `text` 不再单独复制一套 caret 编辑流程 + +### 阶段 4: 重写字段适配层 + +目标: +- 让 `number / text / vector` 只提供语义,不再提供完整交互 + +建议收敛方式: +- `NumberFieldAdapter` +- `TextFieldAdapter` +- `VectorFieldAdapter` + +适配器要暴露的能力: +- `GetDisplayText` +- `BeginEditText` +- `TryCommitText` +- `ApplyScrubDelta` +- `GetComponentCount` +- `ResolveComponentValue` + +这一层不应该知道: +- 双击判定时间 +- 拖动阈值 +- caret 闪烁周期 +- 指针事件状态机 + +### 阶段 5: 收拢 PropertyGrid 状态 + +目标: +- 把 `UIEditorPropertyGridInteractionState` 从“六套字段状态仓库”收成“一个共享编辑会话 + 少量上层状态” + +必须删除的结构: +- `numberFieldInteractionStates` +- `textFieldInteractionStates` +- `vector2FieldInteractionStates` +- `vector3FieldInteractionStates` +- `vector4FieldInteractionStates` + +PropertyGrid 保留的状态只应包括: +- 选择 +- 键盘导航 +- 指针位置 +- 弹窗状态 +- 一个共享 editable field session + +这样做的直接收益: +- 行为不再依赖字段类型分支顺序 +- 同一时间只能有一个字段编辑会话 +- 失焦、提交、取消路径只剩一条主链 + +### 阶段 6: 收拢渲染状态 + +目标: +- 让字段渲染层只读取 core 提供的视图态,不再自己补同步代码 + +需要完成: +- caret 位置只从共享会话映射 +- blink 起点只从共享会话映射 +- `displayText` 规则统一 +- 编辑态背景、选中态边框、hover 态高亮都从共享视图态派生 + +必须避免的终态: +- 渲染层再次缓存一份独立的 caret 或 staged text +- 某字段在渲染前还要再“猜测自己是不是正在编辑” + +### 阶段 7: 删除旧逻辑和临时实现 + +目标: +- 在新主链跑通后,彻底删除旧分支,避免双轨并存 + +必须删除: +- `VectorFieldInteractionInternal.h` +- 各字段交互里重复的双击/拖动/编辑辅助函数 +- `PropertyGridInteraction.cpp` 内基于字段类型复制的同构流程 +- 为了修某个 bug 新增的局部同步字段 + +这一步不能省略,否则之后还会继续从旧逻辑里长出回归 bug。 + +## 7. 验收标准 + +### 7.1 行为验收 + +下面这些行为必须全部稳定: +- number 单击只聚焦 +- number 左键拖动可 scrub,松开立即停止 +- number 双击立即进入编辑态,首帧即可看到 caret +- text 双击进入编辑态,首帧即可看到 caret +- vector 单组件与多组件都遵守同一套交互规则 +- vector 组件拖动与文本编辑互不串状态 +- 失焦、Enter、Escape 的结果一致且可预测 + +### 7.2 结构验收 + +下面这些结构问题必须被消除: +- 不再有多套 `caret` 真值 +- 不再有多套 `editing` 真值 +- `PropertyGrid` 不再持有按字段类型拆开的多套 interaction state vector +- `number / text / vector` 不再分别维护完整状态机 +- 不再存在承载核心交互实现的大型 `Internal.h` + +### 7.3 对标验收 + +至少保证与旧 editor 对齐的点: +- number/vector 的 drag-first, text-edit-second 语义 +- Inspector 字段进入编辑的手感一致 +- 同类字段之间不会再出现“一个能用,一个不能用”的漂移 + +## 8. 实施顺序建议 + +建议按下面的顺序推进,避免继续在旧结构上打补丁: + +1. 先写共享会话模型和 shared core +2. 先迁移 number +3. 再迁移 vector 组件字段 +4. 最后迁移 text +5. 收掉 PropertyGrid 的多套字段状态 +6. 最后删旧逻辑 + +原因很直接: +- number 最简单,最适合作为第一块验证新主链 +- vector 是这次问题最多、重复逻辑最多的部分 +- text 最后迁移,可以直接复用已经稳定的 caret / input / commit 主链 + +## 9. 风险与约束 + +### 9.1 主要风险 + +- 如果新 core 和旧字段状态并存太久,会继续出现双轨同步问题 +- 如果先改渲染不改会话所有权,只会继续加补丁 +- 如果继续保留 per-type interaction state vector,PropertyGrid 仍然无法真正收口 + +### 9.2 必须坚持的约束 + +- 不通过增加更多同步字段来“暂时修好” +- 不通过新增更多 `Internal helper` 来掩盖重复状态机 +- 不把问题继续分散到更多碎文件 +- 不偏离旧 editor 的核心交互语义 + +## 10. 完成标志 + +这份计划完成时,应当达到下面的终态: +- Inspector 可编辑字段只有一套共享交互内核 +- caret、文本缓冲、编辑态、拖动态都有唯一真值源 +- `PropertyGrid` 是编排层,不是字段交互垃圾场 +- `number / text / vector` 是语义适配层,不是三套并行编辑器 +- 后续再改字段行为时,只需要改共享 core 或字段适配器,而不是到处补同步代码 + diff --git a/docs/used/NewEditor_Inspector性能与颜色预览链路重构计划_2026-04-20.md b/docs/used/NewEditor_Inspector性能与颜色预览链路重构计划_2026-04-20.md new file mode 100644 index 00000000..84f0c1e9 --- /dev/null +++ b/docs/used/NewEditor_Inspector性能与颜色预览链路重构计划_2026-04-20.md @@ -0,0 +1,164 @@ +# NewEditor Inspector性能与颜色预览链路重构计划 +日期: `2026-04-20` + +## 1. 问题定位 + +这次不是修一个颜色选择器控件 bug,而是重构 `new_editor` Inspector 的错误数据流。 + +当前根因: +- Inspector 没有稳定的展示模型,每帧都把 runtime 全量投影成一份新的 `InspectorPresentationModel` +- 分离颜色选择器的每一次拖动,都直接走 `ApplyChangedField -> SceneRuntime -> BuildInspectorPresentationModel` +- 结果是高频交互被错误绑定到“正式提交 + 全量重建 Inspector”这条重链路上 + +这不是单点实现失误,而是 Inspector 架构边界错了。 + +## 2. 重构目标 + +这次重构必须达到以下目标: + +1. Inspector 从“每帧全量投影页”改成“稳定结构模型” +2. 字段值同步与结构重建彻底分离 +3. 颜色选择器拖动只走“字段值更新 + scene 预览同步”,不再触发整页重建 +4. 只有真正影响字段结构的变更,才允许重建 Inspector +5. Scene 中的外部变更也能推动 Inspector 刷新,但刷新走轻量值同步,不走全量构建 + +## 3. 目标结构 + +### 3.1 Inspector 层 + +Inspector 维护三类状态: + +- `subject` + - 当前查看对象是谁 +- `presentation` + - 稳定的 section / field 结构 +- `sync state` + - 上次同步的 scene revision + - 上次结构签名 + - 结构失效标记 + +更新原则: + +- `subject` 变化: 重建 +- 结构签名变化: 重建 +- scene revision 变化但结构未变: 只同步字段值 +- 颜色选择器 revision 变化: 只更新目标字段并把值同步到 scene,不重建 + +### 3.2 SceneRuntime 层 + +SceneRuntime 提供明确的 Inspector 变更 revision。 + +凡是会影响 Inspector 展示值的场景变更,都必须推进 revision,包括: + +- 组件字段修改 +- Transform 修改 +- Gizmo 预览 +- Undo / Redo +- 组件删除 +- 可能影响 Inspector 结构的场景变更 + +这样 Inspector 不需要再靠“每帧重建”去撞对结果。 + +### 3.3 ComponentEditor 层 + +组件编辑器职责拆成三块: + +1. `BuildSections` + - 构建稳定结构 +2. `SyncFieldValue` + - 把 runtime 当前值回写到已存在字段 +3. `DoesFieldAffectStructure` + - 判断某个字段变更是否会影响当前组件的 Inspector 结构 + +必要时组件编辑器还要能提供结构签名,用于判断是否需要重建。 + +## 4. 实施步骤 + +### 阶段 1: 固化 Inspector 的新契约 + +要做: + +- 扩展 `InspectorPresentationModel`,加入结构签名 +- 新增 presentation 值同步入口 +- 明确 InspectorPanel 的“重建 / 同步 / 预览提交”三条路径 + +完成标准: + +- InspectorPanel 不再每帧直接调用全量构建函数 + +### 阶段 2: 建立 SceneRuntime revision + +要做: + +- 在 `EditorSceneRuntime` 中增加 Inspector revision +- 所有影响 Inspector 可见值的 mutation 路径统一推进 revision + +完成标准: + +- Inspector 可以基于 revision 判断是否需要同步值 + +### 阶段 3: 扩展组件编辑器接口 + +要做: + +- 给 `IInspectorComponentEditor` 增加字段值同步接口 +- 给 `IInspectorComponentEditor` 增加结构影响判断接口 +- 对所有已注册组件编辑器补齐实现 + +重点: + +- Camera +- Light +- AudioSource +- MeshRenderer + +这些组件存在明显的条件字段或动态结构,必须显式处理 + +### 阶段 4: 重构 InspectorPanel 主链 + +要做: + +- 删掉当前“每帧无条件 BuildInspectorPresentationModel”路径 +- 建立 `EnsurePresentationBuilt` +- 建立 `SyncPresentationValues` +- 建立 `InvalidatePresentationStructure` +- 颜色选择器更新改为: + - 更新目标字段本地值 + - 写 scene preview + - 仅在必要时标记结构失效 + +完成标准: + +- 拖动颜色时,不再发生整页 Inspector 重建 + +### 阶段 5: 验证主路径 + +要验证: + +- 颜色选择器连续拖动时 Inspector 与 Scene 都能实时反馈 +- Transform Gizmo 预览时 Inspector 数值同步更新 +- Camera / Light / AudioSource 等条件字段切换后结构正确刷新 +- 普通 number / bool / enum / asset 修改不出现回退或脏状态残留 + +## 5. 非目标 + +这次不做: + +- 重写整个 PropertyGrid +- 改颜色控件的绘制算法 +- 为了解决 Inspector 性能问题去新增另一套临时 Inspector +- 继续用“颜色选择器特殊判断”硬绕过 Inspector 主链 + +## 6. 验收标准 + +这次重构完成后,以下现象必须消失: + +- 拖动颜色时 Inspector 明显卡顿 +- 颜色变化每一帧都触发整页重建 +- Inspector 必须依赖全量 rebuild 才能保持值正确 + +同时必须成立: + +- Inspector 的结构变化与值变化在架构上分离 +- 高频交互走轻量同步路径 +- 重路径只在结构变化时触发 diff --git a/docs/used/NewEditor_Project面板重构计划_2026-04-20.md b/docs/used/NewEditor_Project面板重构计划_2026-04-20.md new file mode 100644 index 00000000..a2cb77d5 --- /dev/null +++ b/docs/used/NewEditor_Project面板重构计划_2026-04-20.md @@ -0,0 +1,721 @@ +# NewEditor Project 面板重构计划 +日期: `2026-04-20` + +## 1. 文档定位 + +这份计划针对 `new_editor` 当前 `Project` 面板的真实代码状态,覆盖以下范围: + +- `new_editor/app/Features/Project/ProjectPanel.*` +- `new_editor/app/Features/Project/ProjectBrowserModel.*` +- `new_editor/app/Project/EditorProjectRuntime.*` +- 与缩略图/内置图标推进直接相关的调用链 + +这次计划的目标不是“把一个大 cpp 拆成多个 cpp 伪装成重构”,而是: + +- 先把现有结构里的职责边界重新收紧 +- 把明显的副作用和重复状态修复逻辑收口 +- 只在确实独立、可复用、可测试的地方抽离模块 +- 保持每一步都能单独编译、单独验证、单独回退 + +## 2. 基本结论 + +### 2.1 先把话说死 + +`ProjectPanel` 不应该为了“看起来模块化”被拆成一堆 `Renderer`、`Interaction`、`Controller`、`Adapter` cpp 文件。 + +那种做法的主要问题是: + +- 外部类型数量暴涨,但真实依赖关系没有变清晰 +- 状态仍然纠缠,只是从一个文件变成多个文件互相来回传 +- 用户阅读入口变多,维护成本反而上升 +- 很容易把“一个类内部组织不清晰”伪装成“工程结构升级” + +这份新计划的前提是: + +- `ProjectPanel` 继续保持为一个公开类 +- 第一阶段继续保持 `ProjectPanel.h + ProjectPanel.cpp` +- 优先做类内部结构重组,而不是文件爆炸 +- 只有真正独立的逻辑才允许抽出去 + +### 2.2 真正有意义的重构方向 + +真正有价值的方向不是“拆文件”,而是下面几件事: + +- 让 `Append()` 变回纯绘制 +- 让 `Update()` 从“大杂烩过程函数”变成清晰的阶段编排 +- 把重复出现的“操作后 UI 修复逻辑”合并成统一路径 +- 把 `ProjectBrowserModel` 里真正独立的领域逻辑、路径逻辑、文件系统逻辑抽出来 +- 把错误返回从 `bool + catch (...)` 提升为可诊断的结果结构 + +## 3. 当前代码的主要结构问题 + +### 3.1 `ProjectPanel` 是明显的 God Object + +从当前声明和实现看,`ProjectPanel` 同时承担了: + +- 面板可见性和宿主绑定 +- 命令焦点管理 +- tree selection / expansion / interaction +- asset grid selection / double click / hover +- splitter 拖动 +- breadcrumb 命中与导航 +- inline rename 队列、启动、更新、提交 +- context menu 构建、弹出、派发 +- tree drag/drop 与 asset drag/drop +- runtime 选择同步 +- 布局计算 +- 绘制 +- 事件发射 + +这不是“功能多”本身的问题,而是这些职责没有明显的阶段边界,导致: + +- `Update()` 太长,读代码时必须同时追踪多个状态机 +- 任意新增一个交互,都容易误伤 rename / drag / context menu 的边界条件 +- 操作完成后的状态修复逻辑分散在多个分支 +- 某一类行为很难单独验证 + +### 3.2 `Append()` 有副作用,不符合渲染职责 + +当前 `ProjectPanel::Append()` 里直接执行: + +- `m_icons->BeginFrame()` + +这意味着渲染阶段不仅“消费状态”,还在“推进状态”。这会带来几个直接问题: + +- `Append()` 不是纯绘制,语义不干净 +- 调用顺序一旦调整,缩略图推进时机就会变化 +- 未来如果引入多次绘制、录制回放、离屏预览,行为会变得脆弱 + +这属于典型的“为了先跑通功能,把运行时推进塞进了绘制阶段”的临时实现。 + +### 3.3 `Update()` 的流程分层不清晰 + +当前 `Update()` 里串在一起的内容包括: + +- 面板挂载检测 +- 空模型时强制 refresh +- 输入过滤 +- 命令焦点抢占 +- layout 构建 +- context menu 重建 +- tree layout 构建 +- rename 特判 +- tree interaction +- tree drag/drop +- asset drag/drop +- splitter / breadcrumb / grid pointer 处理 + +这里的核心问题不是“行数很多”,而是没有形成稳定的阶段结构。典型症状包括: + +- 中途多次 `m_layout = BuildLayout(...)` +- 中途多次重建 tree layout +- rename 逻辑通过提前 `return` 切断后续流程 +- drag/drop 完成后各自手工做一遍状态修复 + +这会让后续维护者很难回答几个关键问题: + +- 哪些状态属于 frame 初始化 +- 哪些属于输入预处理 +- 哪些属于模型变更后的统一收尾 +- 哪些行为可以安全地提前返回 + +### 3.4 `ProjectBrowserModel` 混了四类完全不同的东西 + +当前 `ProjectBrowserModel.cpp` 里混在一起的内容包括: + +- path / string / itemId 工具函数 +- 文件系统扫描和变更 +- 资源类型识别与 preview 规则 +- UI 投影视图构建 + +这会造成几个结构性问题: + +- 工具函数只能躲在 file-scope,无法被独立复用或测试 +- 模型层和 UI 组件格式强耦合,尤其是 `UIEditorTreeViewItem` +- 文件系统操作只能返回 `bool`,上层没有诊断信息 +- 后续如果想做测试,只能绕过整个模型大对象 + +### 3.5 目录树构建存在重复扫描 + +`RefreshFolderTree()` 当前每个目录至少会经历两类扫描: + +- `HasChildDirectories(folderPath)` 用一次 +- `CollectSortedChildDirectories(folderPath)` 再用一次 + +也就是说,单个目录在构建树节点时,会为了 `forceLeaf` 和子目录列表做重复遍历。 + +在目录数量上来以后,这种实现会持续放大扫描成本,而且没有结构收益。 + +### 3.6 刷新策略偏“全量重扫” + +当前很多操作结束后都会走这一套: + +- `RefreshFolderTree()` +- `EnsureValidCurrentFolder()` +- `RefreshAssetList()` + +包括但不限于: + +- `CreateFolder` +- `CreateMaterial` +- `RenameItem` +- `DeleteItem` +- `MoveItemToFolder` +- `ReparentFolder` +- `MoveFolderToRoot` + +这说明目前的数据更新模型基本还是“改完以后整块重建”。它短期简单,但问题也很明确: + +- 小改动和大改动代价差不多 +- 难以回答某次变更实际影响了哪些视图 +- 后续做增量更新时,没有稳定的变更语义可以接入 + +### 3.7 错误模型过弱 + +当前 `ProjectBrowserModel` 多处直接: + +- `catch (...) { return false; }` + +这会导致: + +- 上层无法区分“名称非法”“目标已存在”“权限失败”“文件被占用”“路径越界” +- UI 无法给出可解释提示 +- 日志也很难追 + +在软件工程上,这种实现只适合非常早期的打通阶段,不适合作为长期结构保留。 + +### 3.8 `ProjectPanel` 里有明显的“临时接法”痕迹 + +当前实现中已经能看到几类典型信号: + +- 大量 file-scope helper 散在 `ProjectPanel.cpp` 顶部 +- `Update()` 内部嵌套局部 callback struct 适配 drag/drop +- 多处手工同步 selection / hovered / clicked / layout / treeFrame +- 多个交互分支各自决定什么时候 `CloseContextMenu()`、`ClearRenameState()`、`SyncCurrentFolderSelection()` + +这些都不是单点 bug,但它们叠在一起,说明这块代码已经过了“继续顺着堆功能”的安全区。 + +## 4. 重构目标 + +### 4.1 一级目标 + +1. 保持 `ProjectPanel` 为一个公开类,但让它重新回到“面板编排者”的职责。 +2. 让 `Append()` 只负责绘制,不再推进缩略图/图标缓存状态。 +3. 让 `Update()` 形成稳定、可阅读的内部阶段。 +4. 让变更后的 UI 修复逻辑走统一路径,而不是分散在各个分支。 +5. 让 `ProjectBrowserModel` 从“全都做”的实现收敛成更清晰的门面。 +6. 为后续增量刷新、错误提示、测试补全留出接口空间。 + +### 4.2 二级目标 + +1. 降低单函数理解成本。 +2. 降低重复代码密度。 +3. 提升可测试性。 +4. 保持现有 UI 行为不回退。 + +## 5. 非目标 + +这次重构明确不做下面这些事: + +- 不把 `ProjectPanel` 机械拆成多个 `cpp` +- 不重写整套资源系统 +- 不引入 Asset Database +- 不在第一阶段引入异步目录扫描 +- 不修改现有 Project 面板交互语义 +- 不为了“未来也许会有”而过度抽象 + +## 6. 目标结构 + +### 6.1 `ProjectPanel` 的目标形态 + +`ProjectPanel` 的目标不是拆文件,而是在同一个类、同一个主实现文件里形成稳定的内部结构: + +- 一组 frame 级准备函数 +- 一组输入/交互阶段函数 +- 一组变更后统一收尾函数 +- 一组纯布局函数 +- 一组纯绘制函数 + +换句话说,最终要做到: + +- 看 `Update()` 时能看出清晰阶段 +- 看 `Append()` 时只看到绘制 +- 看“某次变更后怎么修状态”时有统一入口 + +### 6.2 `ProjectPanel` 内部推荐收敛方式 + +这里的“拆分”只指类内部的责任分层,不是拆出一堆新类型。 + +建议逐步收敛为以下内部阶段: + +1. `PrepareFrameState` + - 清理 frame 级 transient 状态 + - 解析 panel 可见性 + - 处理未挂载 / 无 runtime 的早退路径 + +2. `PrepareModelAndLayout` + - 确保 browser model 已初始化 + - 同步 selection + - 构建 layout + - 构建 tree layout + - 推进当前帧需要的 visual resources + +3. `ProcessRenameFlow` + - 统一处理 queued rename / active rename + - 决定 rename 是否短路其余交互 + +4. `ProcessTreeFlow` + - tree selection + - tree navigation + - tree rename request + - tree drag/drop + +5. `ProcessAssetGridFlow` + - asset selection + - double click open + - asset drag/drop + - background clear selection + +6. `ProcessPanelChromeFlow` + - splitter + - breadcrumb + - context menu pointer routing + +7. `FinalizeFrameState` + - 整理 pointer capture / release 请求 + - 统一处理 mutation 后 layout/treeFrame 修复 + - 整理事件输出 + +最终 `Update()` 应该更像: + +```cpp +void ProjectPanel::Update(...) { + PrepareFrameState(...); + if (!m_visible) { + return; + } + + PrepareModelAndLayout(...); + if (ProcessRenameFlow(...)) { + FinalizeFrameState(...); + return; + } + + ProcessTreeFlow(...); + ProcessAssetGridFlow(...); + ProcessPanelChromeFlow(...); + FinalizeFrameState(...); +} +``` + +重点在“阶段清晰”,不在“类型数量变多”。 + +### 6.3 `ProjectPanel` 内部必须新增的统一收口 + +当前最值得马上收口的不是“命名”,而是下面两类公共修复路径: + +1. 变更后 UI 修复 + +建议新增一条统一入口,负责处理: + +- `CloseContextMenu()` +- `ClearRenameState()` 或按策略保留 +- `SyncCurrentFolderSelection()` +- `SyncAssetSelectionFromRuntime()` +- hover / last click / drag preview 清理 +- 必要时重建 `m_layout` +- 必要时重建 `m_treeFrame.layout` + +不要继续让 tree drop、asset drop、rename、command dispatch、context menu command 各自手工拼一遍。 + +2. 导航后同步 + +建议把“切 folder 后面板要怎么跟着修”收成一条公共路径,避免以下逻辑在多处重复散落: + +- folder selection 对齐 +- asset selection 清理或重同步 +- breadcrumb/layout 更新 +- 事件发射 + +### 6.4 `Append()` 的目标形态 + +`Append()` 最终只应该消费这些输入: + +- 已经准备好的 `Layout` +- tree/grid 当前可视状态 +- rename 当前 frame +- 已经可用的 icon/thumbnail handle + +不应该在这里做: + +- 缩略图缓存推进 +- 资源加载驱动 +- 模型刷新 +- 任意状态修复 + +`Append()` 保持纯绘制后,后续无论是调试录帧、离屏绘制还是多次重绘,都更稳定。 + +### 6.5 `ProjectBrowserModel` 的目标形态 + +`ProjectBrowserModel` 最终应该是一个“浏览数据门面”,而不是“工具函数大仓库 + 文件系统操作器 + UI builder”。 + +建议它最终主要承担: + +- 当前 folder / asset snapshot 的持有 +- folder/item 查找 +- 导航状态维护 +- 组织调用下层独立逻辑 + +而不是继续亲自堆满所有细节实现。 + +## 7. 可以抽离的内容,和不能乱抽的内容 + +### 7.1 可以抽离的,前提是它们真的独立 + +下面这些内容有明确的独立价值,后续可以逐步从 `ProjectBrowserModel.cpp` 中抽离: + +1. `ProjectPathUtils` + +- UTF-8 path 转换 +- 路径分隔符规范化 +- `itemId` 构建 +- project relative path 构建 +- 路径祖先判断 +- moved itemId remap + +这类代码不依赖 UI,也不依赖模型状态,适合作为独立工具层。 + +2. `ProjectAssetRules` + +- `ResolveItemKind` +- `CanOpenItemKind` +- `CanPreviewItem` +- 扩展名与资源类别规则 + +这类规则天然独立,也适合补单元测试。 + +3. `ProjectFileSystemOps` + +- create / rename / delete / move / reparent +- meta sidecar 处理 +- case-aware rename +- 结构化错误返回 + +这类代码跟 UI 没关系,独立后对测试和诊断都更有价值。 + +### 7.2 不该乱抽的内容 + +下面这些内容不应该为了“拆分”而硬抽: + +- 只是 `ProjectPanel` 内部流程的一小段 UI 逻辑 +- 强依赖 `ProjectPanel` 一堆成员状态的局部行为 +- 只是为了让文件变短,但抽出去以后仍然强耦合的逻辑 + +判断标准只有一个: + +抽出去之后,如果它没有更独立、更可测试、更可复用,那就别抽。 + +## 8. 分阶段实施方案 + +## Phase 0. 建基线 + +### 目标 + +在开始整理结构前,先把现有行为和验证口径固定下来,避免重构中出现“代码更好看了,但行为偷偷变了”。 + +### 任务 + +1. 固定最小回归清单: + +- tree 导航 +- breadcrumb 导航 +- grid 单击选择 +- grid 双击打开 +- 右键菜单 +- rename +- tree drag/drop +- asset drag/drop +- folder icon 显示 +- texture thumbnail 显示 + +2. 补最小 smoke 记录方式: + +- 编译验证 +- 打开工程 +- 切换几个目录 +- 查看一张贴图缩略图是否出现 + +3. 为后续要抽离的纯逻辑锁定测试入口: + +- `itemId` 生成 +- moved item remap +- preview 资格判断 +- 名称合法性判断 + +### 验收标准 + +- `XCUIEditorApp` 可编译 +- 有一份可重复执行的 Project 面板 smoke checklist + +## Phase 1. 先把渲染副作用清掉 + +### 目标 + +不改视觉效果,但把 `Append()` 恢复成真正的纯绘制。 + +### 任务 + +1. 把 `m_icons->BeginFrame()` 从 `ProjectPanel::Append()` 挪出去。 +2. 把缩略图/图标运行时推进放到明确的 update/pre-render 阶段。 +3. 保证 `Append()` 只读取结果,不推进状态。 +4. 检查缩略图首帧行为,避免因为推进时机变化导致首帧空白。 + +### 推荐落点 + +- 先仍然放在 `ProjectPanel` 内部 +- 作为 `Update()` 中的显式阶段,例如 `PrepareVisualResources()` +- 暂时不要为了这一步新建多个文件 + +### 风险 + +- 缩略图缓存推进时机变化后,可能出现一帧延迟 + +### 验收标准 + +- `Append()` 内不再出现资源推进调用 +- 图标和缩略图行为与现状一致 + +## Phase 2. 在 `ProjectPanel` 内部重组,而不是拆文件 + +### 目标 + +保留 `ProjectPanel` 现有公开边界,但把 `Update()` 和相关状态流整理成清晰阶段。 + +### 任务 + +1. 给 `Update()` 拆出成组私有 helper,按阶段编排。 +2. 统一 rename 短路路径,避免多个地方各自早退。 +3. 把 tree 和 asset drag/drop 的后处理逻辑收成公共函数。 +4. 把 splitter / breadcrumb / grid pointer 逻辑从主流程里压缩到清晰段落。 +5. 清理顶部 file-scope helper: + +- 纯绘制相关 helper 可以保留在同一 cpp 的匿名命名空间中 +- 纯数学/文本测量 helper 保持局部但命名更清晰 +- 强依赖成员状态的 helper 优先收回类私有方法 + +### 推荐结果 + +这一阶段结束后,`ProjectPanel.cpp` 仍然可能是大文件,但应该变成“结构清晰的大文件”,而不是“所有事情搅在一起的大文件”。 + +### 验收标准 + +- `Update()` 读起来能看出固定阶段 +- drag/drop、rename、导航后的修复不再多处重复 +- 不新增一堆没有独立价值的新类型 + +## Phase 3. 抽真正独立的 `ProjectBrowserModel` 下层逻辑 + +### 目标 + +把能独立测试、独立复用、与 UI 无关的逻辑从 `ProjectBrowserModel.cpp` 中抽出。 + +### 任务 + +1. 先抽 `ProjectPathUtils` + +- `BuildRelativeItemId` +- `BuildRelativeProjectPath` +- `NormalizePathSeparators` +- 路径祖先判断 +- moved id remap + +2. 再抽 `ProjectAssetRules` + +- `ResolveItemKind` +- `CanOpenItemKind` +- `CanPreviewItem` + +3. 最后整理 `ProjectFileSystemOps` + +- folder/file 创建 +- rename +- delete +- move / reparent +- meta sidecar 联动 + +### 这一阶段的关键约束 + +- 只抽纯工具和真正的服务逻辑 +- 不把 UI 投影逻辑拆出去凑文件数 +- `ProjectBrowserModel` 仍然可以先保留为门面,统一调下层 + +### 验收标准 + +- `ProjectBrowserModel.cpp` 明显变薄 +- 被抽离部分可单独测试 +- 行为不变 + +## Phase 4. 升级文件系统错误模型 + +### 目标 + +把“失败就返回 false”升级为“失败有原因,有诊断价值”。 + +### 任务 + +1. 为文件系统操作定义结构化结果,例如: + +```cpp +enum class ProjectFsError { + None = 0, + InvalidName, + SourceMissing, + TargetExists, + PermissionDenied, + PathOutOfScope, + IoFailure +}; + +struct ProjectFsResult { + bool ok = false; + ProjectFsError error = ProjectFsError::None; + std::string itemId = {}; + std::string message = {}; +}; +``` + +2. 消灭 `catch (...) { return false; }` +3. 使用 `std::error_code` 或明确异常转换,把失败原因保留下来 +4. 让上层至少能做: + +- 日志输出 +- 基础 UI 提示 +- 调试时快速定位 + +### 验收标准 + +- 关键文件系统操作不再只返回裸 `bool` +- 常见失败原因能被区分 + +## Phase 5. 优化刷新策略 + +### 目标 + +先把“全量重扫”收敛到更有语义的更新路径,不要求一步到位做真正增量缓存。 + +### 任务 + +1. 先定义变更影响面,而不是直接写死三连刷: + +- tree changed +- current folder changed +- asset list changed +- selection invalidated +- itemId remapped + +2. 把常见操作映射到影响面: + +- 创建文件夹 +- 创建材质 +- rename 文件 +- rename 文件夹 +- 删除 +- asset move +- folder reparent + +3. 基于影响面决定最小刷新范围。 +4. 优先修复 `RefreshFolderTree()` 的重复扫描: + +- 同一次遍历拿到 child folders +- 顺手确定 `forceLeaf` + +### 说明 + +这一阶段不追求立刻做复杂缓存系统,先把“刷新为什么发生”表达清楚。 + +### 验收标准 + +- 变更后的刷新路径有统一语义 +- 目录树重复扫描被消除 + +## Phase 6. 测试与回归补强 + +### 目标 + +把前面整理出的独立逻辑真正纳入测试覆盖。 + +### 建议优先级 + +1. 纯工具测试 + +- itemId/path 构造 +- 路径祖先判断 +- moved id remap +- 名称合法性判断 + +2. 规则测试 + +- 文件类型识别 +- preview 资格判断 + +3. 文件系统操作测试 + +- rename case-aware +- meta sidecar 联动 +- move/reparent 冲突路径 + +4. UI smoke + +- 目录导航 +- 缩略图展示 +- 右键菜单 +- rename +- 拖拽 + +### 验收标准 + +- 重构后的关键逻辑有稳定回归保护 + +## 9. 实施顺序建议 + +推荐严格按下面顺序推进: + +1. `Append()` 去副作用 +2. `ProjectPanel` 内部阶段化整理 +3. 统一 mutation 后 UI 修复路径 +4. `ProjectBrowserModel` 下层独立逻辑抽离 +5. 文件系统错误模型升级 +6. 刷新策略收敛 +7. 测试补强 + +这个顺序的原因很简单: + +- 先做渲染去副作用,风险最可控 +- 再做 `ProjectPanel` 内部收口,能马上降低维护复杂度 +- 再抽独立模块,避免一边抽一边在混乱状态流上反复返工 + +## 10. 重构完成后的判断标准 + +如果这次重构做对了,最终应该看到的是下面这些变化: + +- `ProjectPanel` 仍然是一个类,但内部阶段非常清楚 +- `Append()` 不再推进任何缓存/状态 +- 操作后的状态修复不再分散重复 +- `ProjectBrowserModel` 不再塞满 file-scope 杂项工具函数 +- 文件系统失败能知道为什么失败 +- 目录树构建不再对同一目录重复扫描 +- 代码结构是真的更清晰,而不是文件数量更多 + +## 11. 最后强调一次 + +这份计划的核心不是“把一个大文件拆成很多文件”。 + +这份计划真正追求的是: + +- 状态流清晰 +- 副作用位置正确 +- 变更后修复路径统一 +- 独立逻辑独立出去 +- 不独立的逻辑就老老实实留在原类里收口 + +只有这样,才叫对代码结构有帮助。 diff --git a/docs/used/NewEditor_严重问题最终收口计划_2026-04-20.md b/docs/used/NewEditor_严重问题最终收口计划_2026-04-20.md new file mode 100644 index 00000000..50a42520 --- /dev/null +++ b/docs/used/NewEditor_严重问题最终收口计划_2026-04-20.md @@ -0,0 +1,167 @@ +# NewEditor 严重问题最终收口计划 2026-04-20 + +## 说明 + +- 本轮开始前,`docs/plan` 中已经没有处于活动状态的 `new_editor` 计划;旧 `new_editor` 计划已在此前各轮归档到 `docs/used/`。 +- 本计划只覆盖当前静态审查中仍然属于根因级别的严重问题,不再重复已经完成归档的历史阶段任务。 +- 本计划的目标不是继续打补丁,而是把 `new_editor` 的模块 ownership、层间边界和主干交互契约真正收口。 + +## 当前仍然存在的严重问题 + +### 1. Scene 子系统 ownership 未闭合 + +- `new_editor` 的 scene 资源路径仍然直接指向旧 `editor/resources`。 +- `new_editor` 的 scene tool overlay 图标仍然从旧 `editor/resources/Icons` 读取。 +- `new_editor` 的 scene pass 仍然直接依赖 `engine/include/XCEngine/Rendering/Internal/*`。 + +这说明 `new_editor` 的 scene 子系统仍然处于半迁移状态: + +- 资源 ownership 不在 `new_editor` +- 渲染 helper 契约不在稳定公共层 +- 旧 editor 仍然是 `new_editor` 的隐式上游 + +### 2. 跨窗口 Tab Drag 的平台层边界反向耦合 + +- `WindowManager` 仍然直接读取 shell frame / dock layout 来推导 tab drag hotspot、drop preview 和 drop target。 +- 平台宿主层没有拿到稳定的“拖拽语义接口”,只能消费 UI 帧内部布局结果。 + +这会继续放大以下风险: + +- tab 拖拽与窗口拖动语义互相污染 +- cross-window merge / detach 行为难以稳定 +- UI 层实现细节一变,宿主层就连带失效 + +### 3. XCUIEditorLib 核心公共层仍有结构碎裂 + +- `UIEditorWorkspaceController` 仍然是一个公开类拆到多个 `.cpp`。 +- `Workspace` / `Shell` / `Fields` 仍保留多组 `*Internal.h + 多cpp` 的组织方式。 + +这不是简单的文件多少问题,而是公共层职责没有真正收束: + +- 一个类型的行为被按“主题”而不是按“类型”拆散 +- 私有算法和公共实现边界混杂 +- 后续维护 workspace / shell / property grid 时仍然会重复出现定位困难和改动扩散 + +### 4. Scene Viewport 与旧 Editor 仍是双份演进风险 + +- `new_editor` 的 gizmo / overlay / pass 体系已经形成自己的一份实现。 +- 旧 `editor` 中同时还保留同类 viewport/gizmo/pass 体系。 + +如果不收口 ownership 和共享边界,后果是: + +- 同一领域逻辑在两套实现中分叉演进 +- bug 修复与功能完善无法共享 +- `new_editor` 长期停留在“复制旧 editor 的一份分支”状态 + +## 执行策略 + +### Phase 1. 收口 Scene ownership + +1. 把 `new_editor` 直接依赖旧 `editor/resources` 的路径全部切断。 +2. 把 `scene` 所需资源迁入 `new_editor/resources` 的正式目录,或改为嵌入资源,不再通过旧 editor 路径加载。 +3. 审查 `SceneViewportGridPass` / `SceneViewportSelectionOutlinePass` 依赖的 `Rendering::Internal` helper。 +4. 对真正属于引擎稳定基础设施的 helper,提升到明确公共边界;不再让 `new_editor` 直接包含 internal 头。 +5. 保证最终路径关系为: + - `new_editor -> XCEditor public` + - `new_editor -> engine public` + - 不允许 `new_editor -> editor/resources` + - 不允许 `new_editor -> engine internal` + +### Phase 2. 重构跨窗口 Tab Drag 边界 + +1. 识别宿主层真正需要的拖拽语义数据: + - 是否允许开始跨窗口拖拽 + - 拖拽来源 panel / node + - 鼠标热点 + - 命中的 drop target + - preview 更新请求 +2. 在 UI/editor 公共层建立稳定的 drag transfer 语义接口。 +3. 让 `WindowManager` 消费语义结果,而不是直接窥探 shell frame 内部布局。 +4. 清理 Win32 层对 shell frame / dock layout 细节的反向依赖。 + +### Phase 3. 收口 XCUIEditorLib 结构碎裂 + +1. 以 `一个公开头对应一个实现单元` 为基本规则重新检查 `new_editor/src`。 +2. 优先收口核心模块: + - `Workspace` + - `Shell` + - `Fields` +3. 如果确实存在独立职责,拆成真正独立类型和独立 `.h/.cpp`,而不是继续用 `Internal + 多cpp` 组织一个类型。 +4. 最终目标: + - 公共类型边界稳定 + - 私有算法边界可定位 + - 文件组织与类型组织一致 + +### Phase 4. 评估并收口 Viewport 双份实现风险 + +1. 对比 `editor` 与 `new_editor` 的 viewport / gizmo / pass 责任划分。 +2. 判定哪些逻辑应成为: + - `engine` 公共基础设施 + - `XCEditor` 公共编辑器层 + - `new_editor app` 产品装配层 +3. 不再允许 `new_editor` 长期维持一份只靠复制旧 editor 演化的 scene 子系统。 + +## 本轮执行顺序 + +1. 先完成 Phase 1。 +2. Phase 1 收口并验证后,再进入 Phase 2。 +3. Phase 2 完成后,再清理 Phase 3。 +4. Phase 4 作为最终架构收口,不提前跳过前面三项。 + +## 本轮验收标准 + +- `new_editor` 不再引用旧 `editor/resources`。 +- `new_editor` 的 scene 渲染代码不再包含 `engine/include/XCEngine/Rendering/Internal/*`。 +- `new_editor` 的跨窗口 tab drag 不再依赖 shell frame 内部布局实现细节。 +- `XCUIEditorLib` 核心公共层不再保留明显的“一个公开类型拆多个 cpp”遗留。 +- `XCUIEditorApp` 能重新编译通过,相关 smoke 至少回归一轮。 + +## 当前进展 2026-04-20 + +### 已完成 + +- Phase 1 已完成。 + - `new_editor` 的 scene 资源路径已切回 `new_editor/resources`。 + - scene tool overlay 图标已迁入 `new_editor/resources/Icons`。 + - infinite grid shader 已迁入 `new_editor/resources/shaders/scene-viewport/infinite-grid/`。 + - `SceneViewportGridPass` / `SceneViewportSelectionOutlinePass` 已改为依赖 engine 公共渲染 helper 头,不再直接包含 `Rendering/Internal/*`。 + - engine 已新增正式公共头: + - `engine/include/XCEngine/Rendering/RenderSurfacePipelineUtils.h` + - `engine/include/XCEngine/Rendering/ShaderVariantUtils.h` + - `xcui_editor_app_smoke` 已通过。 + +- Phase 2 第一轮已完成。 + - 已新增 `XCEditor` 公共 dock transfer helper。 + - Win32 `WindowManager` 已不再直接读取 `tabHeaderRects`、`dockHostFrame.layout`、`TabDragDropTarget` 等布局细节来解析 tab drag hotspot / drop target。 + - app 侧临时 `TabDragDropTarget` 文件已删除。 + +- Phase 3 第一轮已完成。 + - `UIEditorWorkspaceController` 已从三个 cpp 合并回单一实现单元。 + - 当前自动核对结果下,`new_editor/include/XCEditor` 中未再发现“一个公开类拆多个 cpp”的直接遗留。 + +### 当前阻塞 + +- `XCUIEditorAppLib` 已成功编译。 +- `XCUIEditorApp` 最终 exe 重新链接时被仓库内无关变更阻塞,当前错误为: + - `ManagedScriptableRenderPipelineAsset::ConfigureCameraFramePlan` 未解析外部符号 +- 这个链接错误来自当前仓库其他改动带入的 engine/managed 主线,不是本轮 `new_editor` 改动引入的新编译错误。 + +### 下一步 + +1. 继续收 Phase 3 剩余碎裂点,重点是 `WorkspaceModel` / `Shell` / `Fields` 的 `Internal + 多cpp` 组织残留。 +2. 在不回退他人改动的前提下,等待或绕过当前 exe 链接阻塞,再补最终整体验证。 + +### չ 2026-04-20 +- Phase 3 ѽһտɣ + - `UIEditorWorkspaceModel` ѲصһʵֵԪ`WorkspaceModelMutation.cpp` / `WorkspaceModelQueries.cpp` / `WorkspaceModelValidation.cpp` ɾ + - `UIEditorShellInteraction` ѲصһʵֵԪ`ShellInteractionRequest.cpp` / `ShellInteractionRendering.cpp` ɾ + - `UIEditorPropertyGrid` ѲصһʵֵԪ`PropertyGridRendering.cpp` ɾ + - `UIEditorPropertyGridInteraction` ѲصһʵֵԪ`PropertyGridInteractionAsset.cpp` / `PropertyGridInteractionColor.cpp` / `PropertyGridInteractionEdit.cpp` / `PropertyGridInteractionHelpers.cpp` / `PropertyGridInteractionPopup.cpp` / `PropertyGridInteractionVector.cpp` ɾ + - `new_editor/CMakeLists.txt` ͬƳƬԴļ +- ǰ֤ + - `cmake --build build --config Debug --target XCUIEditorAppLib` ͨ + - `cmake --build build --config Debug --target XCUIEditorApp` ͨ`XCUIEditor.exe` ɡ + - `ctest --test-dir build -C Debug -R xcui_editor_app_smoke --output-on-failure` ͨ +- ǰע㣺 + - ߲ʣɨ裬ȷǷ񻹴µĽṹ⡣ + - ˶ԵǰƻǷ鵵 diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index e8673e72..f1cf4f5b 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -168,6 +168,7 @@ set(XCUI_EDITOR_HOST_PLATFORM_SOURCES set(XCUI_EDITOR_HOST_RENDERING_SOURCES app/Rendering/Native/AutoScreenshot.cpp app/Rendering/D3D12/D3D12HostDevice.cpp + app/Rendering/D3D12/D3D12UIRenderer.cpp app/Rendering/D3D12/D3D12ShaderResourceDescriptorAllocator.cpp app/Rendering/D3D12/D3D12WindowInteropContext.cpp app/Rendering/D3D12/D3D12WindowRenderer.cpp diff --git a/new_editor/app/Bootstrap/Application.cpp b/new_editor/app/Bootstrap/Application.cpp index 46a544a1..a7a2ef70 100644 --- a/new_editor/app/Bootstrap/Application.cpp +++ b/new_editor/app/Bootstrap/Application.cpp @@ -233,7 +233,11 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { break; } - m_windowManager->RenderAllWindows(); + if (m_windowManager->RenderScheduledWindows()) { + continue; + } + + WaitMessage(); } else { break; } diff --git a/new_editor/app/Features/Project/ProjectPanel.cpp b/new_editor/app/Features/Project/ProjectPanel.cpp index 8c3bc863..6112f460 100644 --- a/new_editor/app/Features/Project/ProjectPanel.cpp +++ b/new_editor/app/Features/Project/ProjectPanel.cpp @@ -41,8 +41,8 @@ inline constexpr float kBreadcrumbItemPaddingX = 4.0f; inline constexpr float kBreadcrumbItemPaddingY = 1.0f; inline constexpr float kBreadcrumbSpacing = 3.0f; inline constexpr float kTreeTopPadding = 0.0f; -inline constexpr float kGridInsetX = 16.0f; -inline constexpr float kGridInsetY = 12.0f; +inline constexpr float kGridInsetX = 0.0f; +inline constexpr float kGridInsetY = 0.0f; inline constexpr float kGridTileWidth = 92.0f; inline constexpr float kGridTileHeight = 92.0f; inline constexpr float kGridTileGapX = 12.0f; @@ -250,6 +250,87 @@ bool HasValidBounds(const UIRect& bounds) { constexpr auto kGridDoubleClickInterval = std::chrono::milliseconds(400); +Widgets::UIEditorScrollViewMetrics BuildProjectGridScrollMetrics() { + Widgets::UIEditorScrollViewMetrics metrics = ResolveUIEditorScrollViewMetrics(); + metrics.scrollbarWidth = 8.0f; + metrics.scrollbarInset = 3.0f; + metrics.minThumbHeight = 28.0f; + metrics.cornerRounding = 0.0f; + metrics.borderThickness = 0.0f; + metrics.focusedBorderThickness = 0.0f; + return metrics; +} + +Widgets::UIEditorScrollViewPalette BuildProjectGridScrollPalette() { + Widgets::UIEditorScrollViewPalette palette = ResolveUIEditorScrollViewPalette(); + palette.surfaceColor = kPaneColor; + return palette; +} + +int ResolveProjectGridColumnCount(float gridWidth) { + const float effectiveTileWidth = kGridTileWidth + kGridTileGapX; + int columnCount = effectiveTileWidth > 0.0f + ? static_cast((ClampNonNegative(gridWidth) + kGridTileGapX) / effectiveTileWidth) + : 1; + if (columnCount < 1) { + columnCount = 1; + } + return columnCount; +} + +float MeasureProjectGridContentHeight( + std::size_t itemCount, + int columnCount) { + if (itemCount == 0u || columnCount < 1) { + return 0.0f; + } + + const std::size_t resolvedColumnCount = static_cast(columnCount); + const std::size_t rowCount = + (itemCount + resolvedColumnCount - 1u) / resolvedColumnCount; + return static_cast(rowCount) * kGridTileHeight + + static_cast((std::max)(rowCount, std::size_t(1u)) - 1u) * + kGridTileGapY; +} + +Widgets::UIEditorScrollViewLayout BuildProjectGridScrollLayout( + const UIRect& bounds, + std::size_t itemCount, + float verticalOffset, + const Widgets::UIEditorScrollViewMetrics& metrics, + int* resolvedColumnCount = nullptr) { + int columnCount = ResolveProjectGridColumnCount(bounds.width); + float contentHeight = MeasureProjectGridContentHeight(itemCount, columnCount); + + Widgets::UIEditorScrollViewLayout scrollLayout = {}; + for (int iteration = 0; iteration < 3; ++iteration) { + scrollLayout = + Widgets::BuildUIEditorScrollViewLayout(bounds, contentHeight, verticalOffset, metrics); + verticalOffset = scrollLayout.verticalOffset; + + const int nextColumnCount = + ResolveProjectGridColumnCount(scrollLayout.contentRect.width); + const float nextContentHeight = + MeasureProjectGridContentHeight(itemCount, nextColumnCount); + if (nextColumnCount == columnCount && + std::abs(nextContentHeight - contentHeight) <= 0.01f) { + columnCount = nextColumnCount; + contentHeight = nextContentHeight; + break; + } + + columnCount = nextColumnCount; + contentHeight = nextContentHeight; + } + + scrollLayout = + Widgets::BuildUIEditorScrollViewLayout(bounds, contentHeight, verticalOffset, metrics); + if (resolvedColumnCount != nullptr) { + *resolvedColumnCount = columnCount; + } + return scrollLayout; +} + Widgets::UIEditorMenuPopupItem BuildContextMenuCommandItem( std::string itemId, std::string label, @@ -350,12 +431,14 @@ void ProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) { void ProjectPanel::ResetInteractionState() { m_assetDragState = {}; m_treeDragState = {}; + m_gridScrollInteractionState = {}; m_treeInteractionState = {}; m_treeFrame = {}; m_contextMenu = {}; ClearRenameState(); m_frameEvents.clear(); m_layout = {}; + m_gridVerticalOffset = 0.0f; m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); m_lastPrimaryClickTime = {}; @@ -387,6 +470,7 @@ bool ProjectPanel::WantsHostPointerRelease() const { bool ProjectPanel::HasActivePointerCapture() const { return m_splitterDragging || + m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb || GridDrag::HasActivePointerCapture(m_assetDragState) || TreeDrag::HasActivePointerCapture(m_treeDragState); } @@ -540,6 +624,11 @@ bool ProjectPanel::TryStartQueuedRenameSession() { initialText = folder->label; } + if (m_pendingRenameSurface == RenameSurface::Grid && + HasValidBounds(m_layout.bounds)) { + EnsureAssetVisible(m_pendingRenameItemId, m_layout.bounds); + } + const UIRect bounds = BuildRenameBounds(m_pendingRenameItemId, m_pendingRenameSurface); if (!HasValidBounds(bounds)) { @@ -709,6 +798,10 @@ bool ProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) SyncCurrentFolderSelection(); SyncAssetSelectionFromRuntime(); + ResetGridScrollPosition(); + if (HasValidBounds(m_layout.bounds)) { + RebuildLayout(m_layout.bounds); + } m_hoveredAssetItemId.clear(); m_lastPrimaryClickedAssetId.clear(); EmitEvent( @@ -729,7 +822,8 @@ bool ProjectPanel::OpenProjectItem(std::string_view itemId, EventSource source) if (navigated && HasValidBounds(m_layout.bounds)) { SyncCurrentFolderSelection(); SyncAssetSelectionFromRuntime(); - m_layout = BuildLayout(m_layout.bounds); + ResetGridScrollPosition(); + RebuildLayout(m_layout.bounds); m_hoveredAssetItemId.clear(); EmitEvent( EventKind::FolderNavigated, @@ -1199,6 +1293,9 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( m_lastPrimaryClickTime = {}; ResolveProjectRuntime()->SetSelection(createdItemId); SyncAssetSelectionFromRuntime(); + if (m_visible && HasValidBounds(m_layout.bounds)) { + EnsureAssetVisible(createdItemId, m_layout.bounds); + } const AssetEntry* createdAsset = FindAssetEntry(createdItemId); if (createdAsset == nullptr) { @@ -1230,7 +1327,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { - m_layout = BuildLayout(m_layout.bounds); + RebuildLayout(m_layout.bounds); } } @@ -1262,7 +1359,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand( if (target.containerFolder->itemId != GetBrowserModel().GetCurrentFolderId()) { NavigateToFolder(target.containerFolder->itemId, EventSource::GridSecondary); if (HasValidBounds(m_layout.bounds)) { - m_layout = BuildLayout(m_layout.bounds); + RebuildLayout(m_layout.bounds); } } @@ -1489,6 +1586,7 @@ void ProjectPanel::Update( FindMountedProjectPanel(contentHostFrame); if (panelState == nullptr) { if (m_splitterDragging || + m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb || m_assetDragState.dragging || m_treeDragState.dragging || m_renameState.active) { @@ -1497,6 +1595,7 @@ void ProjectPanel::Update( m_visible = false; m_assetDragState = {}; m_treeDragState = {}; + m_gridScrollInteractionState = {}; CloseContextMenu(); ClearRenameState(); ResetTransientFrames(); @@ -1504,7 +1603,11 @@ void ProjectPanel::Update( } if (!HasProjectRuntime()) { + if (m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb) { + m_requestPointerRelease = true; + } m_visible = false; + m_gridScrollInteractionState = {}; CloseContextMenu(); ClearRenameState(); ResetTransientFrames(); @@ -1540,7 +1643,7 @@ void ProjectPanel::Update( ClaimCommandFocus(filteredEvents, panelState->bounds, inputContext.allowInteraction); m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width); - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); if (m_contextMenu.open) { RebuildContextMenu(); } @@ -1584,7 +1687,7 @@ void ProjectPanel::Update( m_treeFrame.result.selectedItemId != GetBrowserModel().GetCurrentFolderId()) { CloseContextMenu(); NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree); - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); } if (m_treeFrame.result.renameRequested && !m_treeFrame.result.renameItemId.empty()) { @@ -1670,7 +1773,7 @@ void ProjectPanel::Update( EmitSelectionClearedEvent(EventSource::Tree); } SyncCurrentFolderSelection(); - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( m_layout.treeRect, GetBrowserModel().GetTreeItems(), @@ -1679,6 +1782,27 @@ void ProjectPanel::Update( m_treeInteractionState.verticalOffset); } + const Widgets::UIEditorScrollViewMetrics gridScrollMetrics = + BuildProjectGridScrollMetrics(); + const bool hadGridScrollCapture = + m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb; + UpdateUIEditorScrollViewInteraction( + m_gridScrollInteractionState, + m_gridVerticalOffset, + m_layout.gridRect, + m_layout.gridScrollLayout.contentHeight, + filteredEvents, + gridScrollMetrics); + if (!hadGridScrollCapture && + m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb) { + m_requestPointerCapture = true; + } else if ( + hadGridScrollCapture && + !m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb) { + m_requestPointerRelease = true; + } + RebuildLayout(panelState->bounds); + struct ProjectAssetDragCallbacks { ::XCEngine::UI::Widgets::UISelectionModel& assetSelection; ::XCEngine::UI::Widgets::UIExpansionModel& folderExpansion; @@ -1698,6 +1822,10 @@ void ProjectPanel::Update( } std::string ResolveDraggableItem(const UIPoint& point) const { + if (!ContainsPoint(layout.gridScrollLayout.contentRect, point)) { + return {}; + } + for (const AssetTileLayout& tile : layout.assetTiles) { if (tile.itemIndex >= assetEntries.size()) { continue; @@ -1793,7 +1921,7 @@ void ProjectPanel::Update( } } - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout( m_layout.treeRect, GetBrowserModel().GetTreeItems(), @@ -1845,7 +1973,7 @@ void ProjectPanel::Update( if (m_splitterDragging) { m_navigationWidth = ClampNavigationWidth(event.position.x - panelState->bounds.x, panelState->bounds.width); - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); } m_splitterHovered = @@ -1880,7 +2008,7 @@ void ProjectPanel::Update( m_pressedBreadcrumbIndex = HitTestBreadcrumbItem(event.position); - if (!ContainsPoint(m_layout.gridRect, event.position)) { + if (!ContainsPoint(m_layout.gridScrollLayout.contentRect, event.position)) { break; } @@ -1921,7 +2049,7 @@ void ProjectPanel::Update( } if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right && - ContainsPoint(m_layout.gridRect, event.position)) { + ContainsPoint(m_layout.gridScrollLayout.contentRect, event.position)) { const auto& assetEntries = GetBrowserModel().GetAssetEntries(); const std::size_t hitIndex = HitTestAssetTile(event.position); if (hitIndex >= assetEntries.size()) { @@ -1966,7 +2094,7 @@ void ProjectPanel::Update( m_layout.breadcrumbItems[releasedBreadcrumbIndex]; if (item.clickable) { NavigateToFolder(item.targetFolderId, EventSource::Breadcrumb); - m_layout = BuildLayout(panelState->bounds); + RebuildLayout(panelState->bounds); } } m_pressedBreadcrumbIndex = kInvalidLayoutIndex; @@ -2029,6 +2157,15 @@ ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { layout.browserBodyRect.y + kGridInsetY, ClampNonNegative(layout.browserBodyRect.width - kGridInsetX * 2.0f), ClampNonNegative(layout.browserBodyRect.height - kGridInsetY * 2.0f)); + const Widgets::UIEditorScrollViewMetrics gridScrollMetrics = + BuildProjectGridScrollMetrics(); + int columnCount = 1; + layout.gridScrollLayout = BuildProjectGridScrollLayout( + layout.gridRect, + assetEntries.size(), + m_gridVerticalOffset, + gridScrollMetrics, + &columnCount); const float breadcrumbRowHeight = kHeaderFontSize + kBreadcrumbItemPaddingY * 2.0f; const float breadcrumbY = @@ -2079,20 +2216,16 @@ ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { nextItemX += itemWidth + kBreadcrumbSpacing; } - const float effectiveTileWidth = kGridTileWidth + kGridTileGapX; - int columnCount = effectiveTileWidth > 0.0f - ? static_cast((layout.gridRect.width + kGridTileGapX) / effectiveTileWidth) - : 1; - if (columnCount < 1) { - columnCount = 1; - } - + const UIPoint gridContentOrigin = + Widgets::ResolveUIEditorScrollViewContentOrigin(layout.gridScrollLayout); layout.assetTiles.reserve(assetEntries.size()); for (std::size_t index = 0; index < assetEntries.size(); ++index) { const int column = static_cast(index % static_cast(columnCount)); const int row = static_cast(index / static_cast(columnCount)); - const float tileX = layout.gridRect.x + static_cast(column) * (kGridTileWidth + kGridTileGapX); - const float tileY = layout.gridRect.y + static_cast(row) * (kGridTileHeight + kGridTileGapY); + const float tileX = + gridContentOrigin.x + static_cast(column) * (kGridTileWidth + kGridTileGapX); + const float tileY = + gridContentOrigin.y + static_cast(row) * (kGridTileHeight + kGridTileGapY); AssetTileLayout tile = {}; tile.itemIndex = index; @@ -2113,6 +2246,59 @@ ProjectPanel::Layout ProjectPanel::BuildLayout(const UIRect& bounds) const { return layout; } +void ProjectPanel::RebuildLayout(const UIRect& bounds) { + m_layout = BuildLayout(bounds); + m_gridVerticalOffset = m_layout.gridScrollLayout.verticalOffset; +} + +void ProjectPanel::ResetGridScrollPosition() { + m_gridVerticalOffset = 0.0f; + m_gridScrollInteractionState.scrollViewState.draggingScrollbarThumb = false; +} + +void ProjectPanel::EnsureAssetVisible( + std::string_view itemId, + const UIRect& bounds) { + if (itemId.empty() || !HasValidBounds(bounds)) { + return; + } + + RebuildLayout(bounds); + const Widgets::UIEditorScrollViewMetrics gridScrollMetrics = + BuildProjectGridScrollMetrics(); + const auto& assetEntries = GetBrowserModel().GetAssetEntries(); + for (const AssetTileLayout& tile : m_layout.assetTiles) { + if (tile.itemIndex >= assetEntries.size()) { + continue; + } + + if (assetEntries[tile.itemIndex].itemId != itemId) { + continue; + } + + const UIRect& contentRect = m_layout.gridScrollLayout.contentRect; + float adjustedOffset = m_gridVerticalOffset; + const float tileBottom = tile.tileRect.y + tile.tileRect.height; + const float contentBottom = contentRect.y + contentRect.height; + if (tile.tileRect.y < contentRect.y) { + adjustedOffset -= contentRect.y - tile.tileRect.y; + } else if (tileBottom > contentBottom) { + adjustedOffset += tileBottom - contentBottom; + } + + adjustedOffset = Widgets::ClampUIEditorScrollViewOffset( + m_layout.gridScrollLayout.bounds, + m_layout.gridScrollLayout.contentHeight, + adjustedOffset, + gridScrollMetrics); + if (std::abs(adjustedOffset - m_gridVerticalOffset) > 0.01f) { + m_gridVerticalOffset = adjustedOffset; + RebuildLayout(bounds); + } + return; + } +} + std::size_t ProjectPanel::HitTestBreadcrumbItem(const UIPoint& point) const { for (std::size_t index = 0u; index < m_layout.breadcrumbItems.size(); ++index) { const BreadcrumbItemLayout& item = m_layout.breadcrumbItems[index]; @@ -2125,6 +2311,10 @@ std::size_t ProjectPanel::HitTestBreadcrumbItem(const UIPoint& point) const { } std::size_t ProjectPanel::HitTestAssetTile(const UIPoint& point) const { + if (!ContainsPoint(m_layout.gridScrollLayout.contentRect, point)) { + return kInvalidLayoutIndex; + } + for (const AssetTileLayout& tile : m_layout.assetTiles) { if (ContainsPoint(tile.tileRect, point)) { return tile.itemIndex; @@ -2195,6 +2385,16 @@ void ProjectPanel::Append(UIDrawList& drawList) const { m_layout.browserHeaderRect.width, kHeaderBottomBorderThickness), ResolveUIEditorDockHostPalette().splitterColor); + const Widgets::UIEditorScrollViewPalette gridScrollPalette = + BuildProjectGridScrollPalette(); + const Widgets::UIEditorScrollViewMetrics gridScrollMetrics = + BuildProjectGridScrollMetrics(); + AppendUIEditorScrollViewBackground( + drawList, + m_layout.gridScrollLayout, + m_gridScrollInteractionState.scrollViewState, + gridScrollPalette, + gridScrollMetrics); const Widgets::UIEditorTreeViewPalette treePalette = ResolveUIEditorTreeViewPalette(); const Widgets::UIEditorTreeViewMetrics treeMetrics = ResolveUIEditorTreeViewMetrics(); @@ -2274,6 +2474,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const { } drawList.PopClipRect(); + drawList.PushClipRect(m_layout.gridScrollLayout.contentRect); for (const AssetTileLayout& tile : m_layout.assetTiles) { if (tile.itemIndex >= assetEntries.size()) { continue; @@ -2339,6 +2540,7 @@ void ProjectPanel::Append(UIDrawList& drawList) const { break; } } + drawList.PopClipRect(); if (m_renameState.active) { const Widgets::UIEditorTextFieldPalette textFieldPalette = @@ -2351,19 +2553,30 @@ void ProjectPanel::Append(UIDrawList& drawList) const { BuildUIEditorPropertyGridTextFieldMetrics( ResolveUIEditorPropertyGridMetrics(), ResolveUIEditorTextFieldMetrics())); - AppendUIEditorInlineRenameSession( - drawList, - m_renameFrame, - m_renameState, - textFieldPalette, - textFieldMetrics); + if (m_activeRenameSurface == RenameSurface::Grid) { + drawList.PushClipRect(m_layout.gridScrollLayout.contentRect); + AppendUIEditorInlineRenameSession( + drawList, + m_renameFrame, + m_renameState, + textFieldPalette, + textFieldMetrics); + drawList.PopClipRect(); + } else { + AppendUIEditorInlineRenameSession( + drawList, + m_renameFrame, + m_renameState, + textFieldPalette, + textFieldMetrics); + } } if (assetEntries.empty()) { const UIRect messageRect( - m_layout.gridRect.x, - m_layout.gridRect.y, - m_layout.gridRect.width, + m_layout.gridScrollLayout.contentRect.x, + m_layout.gridScrollLayout.contentRect.y, + m_layout.gridScrollLayout.contentRect.width, 18.0f); drawList.AddText( UIPoint(messageRect.x, ResolveTextTop(messageRect.y, messageRect.height, kHeaderFontSize)), diff --git a/new_editor/app/Features/Project/ProjectPanel.h b/new_editor/app/Features/Project/ProjectPanel.h index 114bc2e8..add5d54a 100644 --- a/new_editor/app/Features/Project/ProjectPanel.h +++ b/new_editor/app/Features/Project/ProjectPanel.h @@ -7,6 +7,7 @@ #include "Commands/EditorEditCommandRoute.h" #include #include +#include #include #include #include @@ -161,6 +162,7 @@ private: ::XCEngine::UI::UIRect browserHeaderRect = {}; ::XCEngine::UI::UIRect browserBodyRect = {}; ::XCEngine::UI::UIRect gridRect = {}; + Widgets::UIEditorScrollViewLayout gridScrollLayout = {}; std::vector breadcrumbItems = {}; std::vector assetTiles = {}; }; @@ -181,6 +183,11 @@ private: const UIEditorPanelContentHostPanelState* FindMountedProjectPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const; Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const; + void RebuildLayout(const ::XCEngine::UI::UIRect& bounds); + void ResetGridScrollPosition(); + void EnsureAssetVisible( + std::string_view itemId, + const ::XCEngine::UI::UIRect& bounds); std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const; std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const; std::string ResolveAssetDropTargetItemId( @@ -251,6 +258,7 @@ private: ::XCEngine::UI::Widgets::UISelectionModel m_assetSelection = {}; Collections::GridDragDrop::State m_assetDragState = {}; Collections::TreeDragDrop::State m_treeDragState = {}; + UIEditorScrollViewInteractionState m_gridScrollInteractionState = {}; UIEditorTreeViewInteractionState m_treeInteractionState = {}; UIEditorTreeViewInteractionFrame m_treeFrame = {}; UIEditorInlineRenameSessionState m_renameState = {}; @@ -265,6 +273,7 @@ private: std::string m_hoveredAssetItemId = {}; std::string m_lastPrimaryClickedAssetId = {}; float m_navigationWidth = 248.0f; + float m_gridVerticalOffset = 0.0f; std::chrono::steady_clock::time_point m_lastPrimaryClickTime = {}; std::size_t m_hoveredBreadcrumbIndex = static_cast(-1); std::size_t m_pressedBreadcrumbIndex = static_cast(-1); diff --git a/new_editor/app/Platform/Win32/EditorWindow.cpp b/new_editor/app/Platform/Win32/EditorWindow.cpp index abc2cdcf..b2b297dd 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.cpp +++ b/new_editor/app/Platform/Win32/EditorWindow.cpp @@ -75,6 +75,13 @@ namespace XCEngine::UI::Editor::App { using namespace EditorWindowSupport; using ::XCEngine::UI::UIPoint; +namespace { + +constexpr UINT_PTR kFrameStatsRefreshTimerId = 0x58435549u; +constexpr UINT kFrameStatsRefreshIntervalMs = 250u; + +} + EditorWindow::EditorWindow( std::string windowId, std::wstring title, @@ -158,20 +165,29 @@ const UIEditorShellInteractionState& EditorWindow::GetShellInteractionState() co void EditorWindow::SetExternalDockHostDropPreview( const Widgets::UIEditorDockHostDropPreviewState& preview) { m_runtime->SetExternalDockHostDropPreview(preview); + RequestRender(); } void EditorWindow::ClearExternalDockHostDropPreview() { m_runtime->ClearExternalDockHostDropPreview(); + RequestRender(); } void EditorWindow::AttachHwnd(HWND hwnd) { m_state->window.hwnd = hwnd; m_state->window.closing = false; + m_state->window.renderRequested = true; } void EditorWindow::MarkDestroyed() { + if (m_state->window.hwnd != nullptr) { + KillTimer(m_state->window.hwnd, kFrameStatsRefreshTimerId); + } m_state->window.hwnd = nullptr; m_state->window.closing = false; + m_state->window.renderRequested = false; + m_state->window.renderingFrame = false; + m_state->window.frameStatsRefreshRequested = false; m_inputController->ResetWindowState(); } @@ -190,16 +206,16 @@ void EditorWindow::SetTrackingMouseLeave(bool trackingMouseLeave) { void EditorWindow::SetTitle(std::wstring title) { m_state->window.title = std::move(title); UpdateCachedTitleText(); + RequestRender(); } void EditorWindow::ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) { m_runtime->ReplaceWorkspaceController(std::move(workspaceController)); + RequestRender(); } -void EditorWindow::InvalidateHostWindow() const { - if (m_state->window.hwnd != nullptr && IsWindow(m_state->window.hwnd)) { - InvalidateRect(m_state->window.hwnd, nullptr, FALSE); - } +void EditorWindow::InvalidateHostWindow() { + RequestRender(); } bool EditorWindow::Initialize( @@ -222,16 +238,28 @@ bool EditorWindow::Initialize( << " scale=" << GetDpiScale(); LogRuntimeTrace("window", dpiTrace.str()); - return m_runtime->Initialize( + const bool initialized = m_runtime->Initialize( m_state->window.hwnd, repoRoot, editorContext, captureRoot, autoCaptureOnStartup); + if (initialized) { + SetTimer( + m_state->window.hwnd, + kFrameStatsRefreshTimerId, + kFrameStatsRefreshIntervalMs, + nullptr); + RequestRender(); + } + return initialized; } void EditorWindow::Shutdown() { ForceReleasePointerCapture(); + if (m_state->window.hwnd != nullptr && IsWindow(m_state->window.hwnd)) { + KillTimer(m_state->window.hwnd, kFrameStatsRefreshTimerId); + } m_runtime->Shutdown(); m_inputController->ClearPendingEvents(); @@ -250,6 +278,7 @@ void EditorWindow::ResetInteractionState() { m_chromeController->SetHoveredBorderlessResizeEdge( Host::BorderlessWindowResizeEdge::None); m_chromeController->ClearPredictedClientPixelSize(); + RequestRender(); } bool EditorWindow::ApplyWindowResize(UINT width, UINT height) { @@ -371,6 +400,7 @@ void EditorWindow::OnResize(UINT width, UINT height) { if (!matchesPredictedClientSize) { ApplyWindowResize(width, height); } + RequestRender(false); } void EditorWindow::OnEnterSizeMove() { @@ -385,6 +415,7 @@ void EditorWindow::OnExitSizeMove() { if (QueryCurrentClientPixelSize(width, height)) { ApplyWindowResize(width, height); } + RequestRender(false); } void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { @@ -413,6 +444,7 @@ void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { trace << "dpi changed to " << m_chromeController->GetWindowDpi() << " scale=" << GetDpiScale(); LogRuntimeTrace("window", trace.str()); + RequestRender(false); } bool EditorWindow::IsVerboseRuntimeTraceEnabled() { @@ -602,11 +634,16 @@ EditorWindowFrameTransferRequests EditorWindow::RenderFrame( return {}; } + const bool updateFrameTiming = !m_state->window.frameStatsRefreshRequested; + m_state->window.renderRequested = false; + m_state->window.frameStatsRefreshRequested = false; + UINT pixelWidth = 0u; UINT pixelHeight = 0u; if (!ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { return {}; } + m_state->window.renderingFrame = true; const float width = PixelsToDips(static_cast(pixelWidth)); const float height = PixelsToDips(static_cast(pixelHeight)); @@ -621,7 +658,12 @@ EditorWindowFrameTransferRequests EditorWindow::RenderFrame( EditorWindowFrameTransferRequests transferRequests = {}; if (editorContext.IsValid()) { transferRequests = - RenderRuntimeFrame(editorContext, globalTabDragActive, workspaceBounds, drawList); + RenderRuntimeFrame( + editorContext, + globalTabDragActive, + updateFrameTiming, + workspaceBounds, + drawList); } else { m_frameOrchestrator->AppendInvalidFrame(editorContext, drawList); } @@ -638,6 +680,10 @@ EditorWindowFrameTransferRequests EditorWindow::RenderFrame( pixelWidth, pixelHeight, presentResult.framePresented); + m_state->window.renderingFrame = false; + if (HasInteractiveCaptureState()) { + RequestRender(false); + } return transferRequests; } @@ -676,6 +722,7 @@ UIRect EditorWindow::ResolveWorkspaceBounds(float clientWidthDips, float clientH EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( EditorContext& editorContext, bool globalTabDragActive, + bool updateFrameTiming, const UIRect& workspaceBounds, UIDrawList& drawList) { SyncShellCapturedPointerButtonsFromSystemState(); @@ -685,6 +732,7 @@ EditorWindowFrameTransferRequests EditorWindow::RenderRuntimeFrame( m_frameOrchestrator->UpdateAndAppend( editorContext, *m_runtime, + updateFrameTiming, workspaceBounds, frameEvents, m_runtime->BuildCaptureStatusText(), @@ -845,6 +893,7 @@ void EditorWindow::QueuePointerEvent( ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)), wParam, doubleClick); + RequestRender(); } void EditorWindow::QueueSyntheticPointerStateSyncEvent( @@ -864,6 +913,7 @@ void EditorWindow::QueueSyntheticPointerStateSyncEvent( m_inputController->QueueSyntheticPointerStateSyncEvent( ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), modifiers); + RequestRender(); } void EditorWindow::QueuePointerLeaveEvent() { @@ -875,6 +925,7 @@ void EditorWindow::QueuePointerLeaveEvent() { position = ConvertClientPixelsToDips(clientPoint.x, clientPoint.y); } m_inputController->QueuePointerLeaveEvent(position); + RequestRender(); } void EditorWindow::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { @@ -892,18 +943,22 @@ void EditorWindow::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARA ConvertClientPixelsToDips(screenPoint.x, screenPoint.y), wheelDelta, wParam); + RequestRender(); } void EditorWindow::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { m_inputController->QueueKeyEvent(type, wParam, lParam); + RequestRender(); } void EditorWindow::QueueCharacterEvent(WPARAM wParam, LPARAM) { m_inputController->QueueCharacterEvent(wParam); + RequestRender(); } void EditorWindow::QueueWindowFocusEvent(UIInputEventType type) { m_inputController->QueueWindowFocusEvent(type); + RequestRender(); } void EditorWindow::SyncInputModifiersFromSystemState() { @@ -916,6 +971,48 @@ void EditorWindow::ResetInputModifiers() { void EditorWindow::RequestManualScreenshot() { m_runtime->RequestManualScreenshot("manual_f12"); + RequestRender(); +} + +bool EditorWindow::HandleFrameStatsTimer(UINT_PTR timerId) { + if (timerId != kFrameStatsRefreshTimerId) { + return false; + } + if (!m_runtime->IsReady() || + m_state->window.hwnd == nullptr || + !IsWindow(m_state->window.hwnd) || + IsIconic(m_state->window.hwnd)) { + return true; + } + if (HasInteractiveCaptureState() || HasPendingRenderRequest()) { + return true; + } + + RequestFrameStatsRefresh(); + return true; +} + +bool EditorWindow::HasPendingRenderRequest() const { + return m_state->window.renderRequested; +} + +void EditorWindow::RequestRender(bool invalidateHostWindow) { + m_state->window.renderRequested = true; + m_state->window.frameStatsRefreshRequested = false; + if (!invalidateHostWindow || + m_state->window.renderingFrame || + m_state->window.hwnd == nullptr || + !IsWindow(m_state->window.hwnd)) { + return; + } + + InvalidateRect(m_state->window.hwnd, nullptr, FALSE); +} + +void EditorWindow::RequestFrameStatsRefresh() { + m_state->window.frameStatsRefreshRequested = true; + RequestRender(false); + m_state->window.frameStatsRefreshRequested = true; } bool EditorWindow::IsPointerInsideClientArea() const { diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index fd58012e..077e96d9 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -112,7 +112,7 @@ private: bool TryResolveDockTabDropTarget( const POINT& screenPoint, UIEditorDockHostTabDropTarget& outTarget) const; - void InvalidateHostWindow() const; + void InvalidateHostWindow(); void SetExternalDockHostDropPreview( const Widgets::UIEditorDockHostDropPreviewState& preview); void ClearExternalDockHostDropPreview(); @@ -204,6 +204,10 @@ private: void SyncShellCapturedPointerButtonsFromSystemState(); void ResetInputModifiers(); void RequestManualScreenshot(); + bool HandleFrameStatsTimer(UINT_PTR timerId); + bool HasPendingRenderRequest() const; + void RequestRender(bool invalidateHostWindow = true); + void RequestFrameStatsRefresh(); bool ApplyWindowResize(UINT width, UINT height); bool QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const; @@ -214,6 +218,7 @@ private: EditorWindowFrameTransferRequests RenderRuntimeFrame( EditorContext& editorContext, bool globalTabDragActive, + bool updateFrameTiming, const ::XCEngine::UI::UIRect& workspaceBounds, ::XCEngine::UI::UIDrawList& drawList); bool IsPointerInsideClientArea() const; diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp index a5e27421..837341d0 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.cpp @@ -63,6 +63,7 @@ std::string DescribeInputEventType(const UIInputEvent& event) { EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend( EditorContext& editorContext, EditorWindowRuntimeController& runtimeController, + bool updateFrameTiming, const ::XCEngine::UI::UIRect& workspaceBounds, const std::vector& frameEvents, std::string_view captureStatusText, @@ -72,7 +73,8 @@ EditorWindowFrameTransferRequests EditorWindowFrameOrchestrator::UpdateAndAppend UIDrawList& drawList) const { LogInputTrace(editorContext, runtimeController, frameEvents); - const Host::D3D12WindowRenderLoopFrameContext frameContext = runtimeController.BeginFrame(); + const Host::D3D12WindowRenderLoopFrameContext frameContext = + runtimeController.BeginFrame(updateFrameTiming); if (!frameContext.warning.empty()) { LogRuntimeTrace("viewport", frameContext.warning); } diff --git a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h index 74d559f4..06c16c16 100644 --- a/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h +++ b/new_editor/app/Platform/Win32/EditorWindowFrameOrchestrator.h @@ -44,6 +44,7 @@ public: EditorWindowFrameTransferRequests UpdateAndAppend( EditorContext& editorContext, EditorWindowRuntimeController& runtimeController, + bool updateFrameTiming, const ::XCEngine::UI::UIRect& workspaceBounds, const std::vector<::XCEngine::UI::UIInputEvent>& frameEvents, std::string_view captureStatusText, diff --git a/new_editor/app/Platform/Win32/EditorWindowManager.h b/new_editor/app/Platform/Win32/EditorWindowManager.h index 3e0d6b7e..98ebfb1e 100644 --- a/new_editor/app/Platform/Win32/EditorWindowManager.h +++ b/new_editor/app/Platform/Win32/EditorWindowManager.h @@ -85,7 +85,7 @@ public: bool HasWindows() const; void DestroyClosedWindows(); - void RenderAllWindows(); + bool RenderScheduledWindows(); private: std::unique_ptr m_hostRuntime = {}; diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp index 36398302..482e9d30 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.cpp @@ -18,6 +18,8 @@ using namespace EditorWindowSupport; namespace { constexpr float kFrameTimeSmoothingFactor = 0.12f; +constexpr float kFrameTimingResetThresholdSeconds = 0.5f; +constexpr float kFrameStatsIdleLabelThresholdSeconds = 0.35f; } @@ -73,6 +75,7 @@ void EditorWindowRuntimeController::ClearExternalDockHostDropPreview() { void EditorWindowRuntimeController::SetDpiScale(float dpiScale) { m_renderer.SetDpiScale(dpiScale); + m_windowRenderLoop.SetDpiScale(dpiScale); } Host::NativeRenderer& EditorWindowRuntimeController::GetRenderer() { @@ -192,13 +195,16 @@ bool EditorWindowRuntimeController::ApplyResize(UINT width, UINT height) { return resizeResult.hasViewportSurfacePresentation; } -Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFrame() { - UpdateFrameTiming(); +Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFrame( + bool updateFrameTiming) { + if (updateFrameTiming) { + UpdateFrameTiming(); + } return m_windowRenderLoop.BeginFrame(); } Host::D3D12WindowRenderLoopPresentResult EditorWindowRuntimeController::Present( - const ::XCEngine::UI::UIDrawData& drawData) const { + const ::XCEngine::UI::UIDrawData& drawData) { return m_windowRenderLoop.Present(drawData); } @@ -240,19 +246,36 @@ std::string EditorWindowRuntimeController::BuildFrameRateText() const { return {}; } - char buffer[48] = {}; - std::snprintf( - buffer, - sizeof(buffer), - "FPS %.1f | %.2f ms", - m_displayFps, - m_displayFrameTimeMs); + const auto now = std::chrono::steady_clock::now(); + const float idleSeconds = + m_hasLastMeasuredFrameTime + ? std::chrono::duration(now - m_lastMeasuredFrameTime).count() + : 0.0f; + char buffer[72] = {}; + if (idleSeconds >= kFrameStatsIdleLabelThresholdSeconds) { + std::snprintf( + buffer, + sizeof(buffer), + "FPS %.1f | %.2f ms | %.1fs ago", + m_displayFps, + m_displayFrameTimeMs, + idleSeconds); + } else { + std::snprintf( + buffer, + sizeof(buffer), + "FPS %.1f | %.2f ms", + m_displayFps, + m_displayFrameTimeMs); + } return buffer; } void EditorWindowRuntimeController::ResetFrameTiming() { m_lastFrameTime = {}; + m_lastMeasuredFrameTime = {}; m_hasLastFrameTime = false; + m_hasLastMeasuredFrameTime = false; m_smoothedDeltaTimeSeconds = 0.0f; m_displayFps = 0.0f; m_displayFrameTimeMs = 0.0f; @@ -271,6 +294,9 @@ void EditorWindowRuntimeController::UpdateFrameTiming() { if (deltaTime <= 0.0f) { return; } + if (deltaTime > kFrameTimingResetThresholdSeconds) { + return; + } if (m_smoothedDeltaTimeSeconds <= 0.0f) { m_smoothedDeltaTimeSeconds = deltaTime; @@ -283,6 +309,8 @@ void EditorWindowRuntimeController::UpdateFrameTiming() { m_displayFps = m_smoothedDeltaTimeSeconds > 0.0f ? 1.0f / m_smoothedDeltaTimeSeconds : 0.0f; + m_lastMeasuredFrameTime = now; + m_hasLastMeasuredFrameTime = true; } } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h index 17cc11dc..fe843da0 100644 --- a/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h +++ b/new_editor/app/Platform/Win32/EditorWindowRuntimeController.h @@ -77,9 +77,9 @@ public: void ResetInteractionState(); bool ApplyResize(UINT width, UINT height); - Host::D3D12WindowRenderLoopFrameContext BeginFrame(); + Host::D3D12WindowRenderLoopFrameContext BeginFrame(bool updateFrameTiming = true); Host::D3D12WindowRenderLoopPresentResult Present( - const ::XCEngine::UI::UIDrawData& drawData) const; + const ::XCEngine::UI::UIDrawData& drawData); void CaptureIfRequested( const ::XCEngine::UI::UIDrawData& drawData, UINT pixelWidth, @@ -102,7 +102,9 @@ private: UIEditorWorkspaceController m_workspaceController = {}; EditorShellRuntime m_shellRuntime = {}; std::chrono::steady_clock::time_point m_lastFrameTime = {}; + std::chrono::steady_clock::time_point m_lastMeasuredFrameTime = {}; bool m_hasLastFrameTime = false; + bool m_hasLastMeasuredFrameTime = false; float m_smoothedDeltaTimeSeconds = 0.0f; float m_displayFps = 0.0f; float m_displayFrameTimeMs = 0.0f; diff --git a/new_editor/app/Platform/Win32/EditorWindowState.h b/new_editor/app/Platform/Win32/EditorWindowState.h index 25260936..875b75f6 100644 --- a/new_editor/app/Platform/Win32/EditorWindowState.h +++ b/new_editor/app/Platform/Win32/EditorWindowState.h @@ -17,6 +17,9 @@ struct EditorWindowWindowState { std::string titleText = {}; bool primary = false; bool closing = false; + bool renderRequested = true; + bool renderingFrame = false; + bool frameStatsRefreshRequested = false; }; struct EditorWindowState { diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp index 764f70a6..0048295f 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.cpp @@ -159,7 +159,7 @@ void EditorWindowHostRuntime::DestroyClosedWindows() { } } -void EditorWindowHostRuntime::RenderAllWindows( +bool EditorWindowHostRuntime::RenderScheduledWindows( bool globalTabDragActive, EditorWindowWorkspaceCoordinator& workspaceCoordinator) { struct WindowFrameTransferBatch { @@ -169,16 +169,22 @@ void EditorWindowHostRuntime::RenderAllWindows( std::vector transferBatches = {}; transferBatches.reserve(m_windows.size()); + bool renderedAnyWindow = false; for (const std::unique_ptr& window : m_windows) { if (window == nullptr || window->GetHwnd() == nullptr || - window->IsClosing()) { + window->IsClosing() || + !window->HasPendingRenderRequest()) { continue; } + renderedAnyWindow = true; EditorWindowFrameTransferRequests transferRequests = window->RenderFrame(m_editorContext, globalTabDragActive); + if (window->GetHwnd() != nullptr && IsWindow(window->GetHwnd())) { + ValidateRect(window->GetHwnd(), nullptr); + } workspaceCoordinator.CommitWindowProjection(*window); if (!transferRequests.HasPendingRequests()) { continue; @@ -201,6 +207,8 @@ void EditorWindowHostRuntime::RenderAllWindows( *batch.sourceWindow, std::move(batch.requests)); } + + return renderedAnyWindow; } void EditorWindowHostRuntime::HandleDestroyedWindow(HWND hwnd) { diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h index 12e9ae77..b90e2cc7 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowHostRuntime.h @@ -38,7 +38,7 @@ public: bool HasWindows() const; void DestroyClosedWindows(); - void RenderAllWindows( + bool RenderScheduledWindows( bool globalTabDragActive, EditorWindowWorkspaceCoordinator& workspaceCoordinator); void HandleDestroyedWindow(HWND hwnd); diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp index 950a6b7e..8de26e91 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowManager.cpp @@ -101,8 +101,8 @@ void EditorWindowManager::DestroyClosedWindows() { m_hostRuntime->DestroyClosedWindows(); } -void EditorWindowManager::RenderAllWindows() { - m_hostRuntime->RenderAllWindows( +bool EditorWindowManager::RenderScheduledWindows() { + return m_hostRuntime->RenderScheduledWindows( m_workspaceCoordinator->IsGlobalTabDragActive(), *m_workspaceCoordinator); } diff --git a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp index 21f7385e..5f3f5cf8 100644 --- a/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/EditorWindowMessageDispatcher.cpp @@ -397,6 +397,12 @@ bool EditorWindowMessageDispatcher::TryDispatchWindowLifecycleMessage( } outResult = 0; return true; + case WM_TIMER: + if (context.window.HandleFrameStatsTimer(static_cast(wParam))) { + outResult = 0; + return true; + } + return false; case WM_ERASEBKGND: outResult = 1; return true; diff --git a/new_editor/app/Ports/PortFwd.h b/new_editor/app/Ports/PortFwd.h index f659d2c4..e51b6bf1 100644 --- a/new_editor/app/Ports/PortFwd.h +++ b/new_editor/app/Ports/PortFwd.h @@ -5,6 +5,7 @@ namespace XCEngine::UI::Editor::Ports { class ShaderResourceDescriptorAllocatorPort; class SystemInteractionPort; class TexturePort; +class TextureDataPort; class ViewportRenderPort; } // namespace XCEngine::UI::Editor::Ports diff --git a/new_editor/app/Ports/TextureDataPort.h b/new_editor/app/Ports/TextureDataPort.h new file mode 100644 index 00000000..cd26462c --- /dev/null +++ b/new_editor/app/Ports/TextureDataPort.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace XCEngine::UI::Editor::Ports { + +struct TexturePixelDataView { + const std::uint8_t* pixels = nullptr; + std::uint32_t width = 0u; + std::uint32_t height = 0u; +}; + +class TextureDataPort { +public: + virtual ~TextureDataPort() = default; + + virtual bool ResolveTexturePixelData( + const ::XCEngine::UI::UITextureHandle& texture, + TexturePixelDataView& outView) const = 0; +}; + +} // namespace XCEngine::UI::Editor::Ports diff --git a/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp index a82ff795..8673db5a 100644 --- a/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12HostDevice.cpp @@ -1,4 +1,6 @@ #include "D3D12HostDevice.h" +#include "Support/EnvironmentFlags.h" + #include namespace XCEngine::UI::Editor::Host { @@ -15,6 +17,21 @@ using ::XCEngine::RHI::RHIDeviceDesc; using ::XCEngine::RHI::RHIFactory; using ::XCEngine::RHI::RHIType; +#ifdef _DEBUG +namespace { + +bool ShouldEnableD3D12DebugLayer() { + return App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_DEBUG") || + App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_VALIDATION"); +} + +bool ShouldEnableD3D12GpuValidation() { + return App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_VALIDATION"); +} + +} // namespace +#endif + bool D3D12HostDevice::Initialize() { Shutdown(); @@ -26,8 +43,8 @@ bool D3D12HostDevice::Initialize() { RHIDeviceDesc deviceDesc = {}; #ifdef _DEBUG - deviceDesc.enableDebugLayer = true; - deviceDesc.enableGPUValidation = true; + deviceDesc.enableDebugLayer = ShouldEnableD3D12DebugLayer(); + deviceDesc.enableGPUValidation = ShouldEnableD3D12GpuValidation(); #endif if (!m_device->Initialize(deviceDesc)) { m_lastError = "Failed to initialize the D3D12 RHI device."; diff --git a/new_editor/app/Rendering/D3D12/D3D12UIRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12UIRenderer.cpp new file mode 100644 index 00000000..a2a2bb31 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UIRenderer.cpp @@ -0,0 +1,1784 @@ +#include "D3D12UIRenderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +using ::XCEngine::UI::UIDrawCommand; +using ::XCEngine::UI::UIDrawCommandType; +using ::XCEngine::UI::UIDrawData; +using ::XCEngine::UI::UIDrawList; +using Microsoft::WRL::ComPtr; + +namespace { + +constexpr std::uint32_t kQuadPassConstantBinding = 0u; +constexpr float kAxisAlignedLineTolerance = 0.01f; +constexpr float kBaseDpi = 96.0f; +constexpr float kDefaultFontSize = 16.0f; + +struct QuadPassConstants { + float rectPx[4] = {}; + float color[4] = {}; + float viewportPx[2] = {}; + float uvMin[2] = {}; + float uvMax[2] = { 1.0f, 1.0f }; + float roundingPx = 0.0f; + float strokePx = 0.0f; +}; + +constexpr std::uint64_t AlignConstantBufferSize(std::uint64_t sizeInBytes) { + return (sizeInBytes + 255ull) & ~255ull; +} + +constexpr std::string_view kQuadVertexShaderSource = R"( +cbuffer QuadPassConstants : register(b0) { + float4 gRectPx; + float4 gColor; + float2 gViewportPx; + float2 gUvMin; + float2 gUvMax; + float gRoundingPx; + float gStrokePx; +}; + +struct VSOutput { + float4 position : SV_Position; + float4 color : COLOR0; + float2 localPx : TEXCOORD0; + float2 rectSizePx : TEXCOORD1; + float2 uv : TEXCOORD2; +}; + +VSOutput VSMain(uint vertexId : SV_VertexID) { + const float2 corners[6] = { + float2(0.0f, 0.0f), + float2(1.0f, 0.0f), + float2(0.0f, 1.0f), + float2(0.0f, 1.0f), + float2(1.0f, 0.0f), + float2(1.0f, 1.0f) + }; + + const float2 corner = corners[vertexId]; + const float2 pixelPosition = gRectPx.xy + corner * gRectPx.zw; + const float2 ndcPosition = float2( + (pixelPosition.x / gViewportPx.x) * 2.0f - 1.0f, + 1.0f - (pixelPosition.y / gViewportPx.y) * 2.0f); + + VSOutput output; + output.position = float4(ndcPosition, 0.0f, 1.0f); + output.color = gColor; + output.localPx = corner * gRectPx.zw; + output.rectSizePx = gRectPx.zw; + output.uv = lerp(gUvMin, gUvMax, corner); + return output; +} +)"; + +constexpr std::string_view kShapePixelShaderSource = R"( +cbuffer QuadPassConstants : register(b0) { + float4 gRectPx; + float4 gColor; + float2 gViewportPx; + float2 gUvMin; + float2 gUvMax; + float gRoundingPx; + float gStrokePx; +}; + +struct PSInput { + float4 position : SV_Position; + float4 color : COLOR0; + float2 localPx : TEXCOORD0; + float2 rectSizePx : TEXCOORD1; + float2 uv : TEXCOORD2; +}; + +float RoundedRectDistance(float2 localPx, float2 rectSizePx, float roundingPx) { + const float radius = min(gRoundingPx, min(rectSizePx.x, rectSizePx.y) * 0.5f); + const float2 halfSize = rectSizePx * 0.5f; + const float2 centerSpace = localPx - halfSize; + const float2 q = abs(centerSpace) - (halfSize - radius); + return length(max(q, 0.0f)) + min(max(q.x, q.y), 0.0f) - radius; +} + +float4 PSMain(PSInput input) : SV_Target0 { + const float distance = RoundedRectDistance(input.localPx, input.rectSizePx, gRoundingPx); + const float aa = max(fwidth(distance), 1.0f); + const float outerAlpha = 1.0f - smoothstep(0.0f, aa, distance); + float alpha = outerAlpha; + if (gStrokePx > 0.0f) { + const float innerAlpha = 1.0f - smoothstep(0.0f, aa, distance + gStrokePx); + alpha = saturate(outerAlpha - innerAlpha); + } + + return float4(input.color.rgb, input.color.a * alpha); +} +)"; + +constexpr std::string_view kImagePixelShaderSource = R"( +Texture2D gTexture : register(t0); +SamplerState gSampler : register(s0); + +cbuffer QuadPassConstants : register(b0) { + float4 gRectPx; + float4 gColor; + float2 gViewportPx; + float2 gUvMin; + float2 gUvMax; + float gRoundingPx; + float gStrokePx; +}; + +struct PSInput { + float4 position : SV_Position; + float4 color : COLOR0; + float2 localPx : TEXCOORD0; + float2 rectSizePx : TEXCOORD1; + float2 uv : TEXCOORD2; +}; + +float4 PSMain(PSInput input) : SV_Target0 { + const float4 sampleColor = gTexture.Sample(gSampler, input.uv); + const float4 premultipliedTint = float4(input.color.rgb * input.color.a, input.color.a); + return sampleColor * premultipliedTint; +} +)"; + +float ClampDpiScale(float dpiScale) { + return dpiScale > 0.0f ? dpiScale : 1.0f; +} + +float ResolveFontSize(float fontSize) { + return fontSize > 0.0f ? fontSize : kDefaultFontSize; +} + +std::vector ToShaderSource(std::string_view source) { + return std::vector(source.begin(), source.end()); +} + +std::wstring ToWideAscii(std::string_view value) { + return std::wstring(value.begin(), value.end()); +} + +std::wstring Utf8ToWide(std::string_view text) { + if (text.empty()) { + return {}; + } + + const int sizeNeeded = MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + nullptr, + 0); + if (sizeNeeded <= 0) { + return {}; + } + + std::wstring wideText(static_cast(sizeNeeded), L'\0'); + MultiByteToWideChar( + CP_UTF8, + 0, + text.data(), + static_cast(text.size()), + wideText.data(), + sizeNeeded); + return wideText; +} + +::XCEngine::RHI::ShaderCompileDesc BuildShaderDesc( + std::string_view source, + std::string_view entryPoint, + std::string_view profile) { + ::XCEngine::RHI::ShaderCompileDesc desc = {}; + desc.source = ToShaderSource(source); + desc.entryPoint = ToWideAscii(entryPoint); + desc.profile = ToWideAscii(profile); + return desc; +} + +::XCEngine::RHI::Rect IntersectRects( + const ::XCEngine::RHI::Rect& left, + const ::XCEngine::RHI::Rect& right) { + ::XCEngine::RHI::Rect result = {}; + result.left = (std::max)(left.left, right.left); + result.top = (std::max)(left.top, right.top); + result.right = (std::min)(left.right, right.right); + result.bottom = (std::min)(left.bottom, right.bottom); + if (result.right < result.left) { + result.right = result.left; + } + if (result.bottom < result.top) { + result.bottom = result.top; + } + return result; +} + +bool IsEmptyRect(const ::XCEngine::RHI::Rect& rect) { + return rect.right <= rect.left || rect.bottom <= rect.top; +} + +::XCEngine::RHI::Rect BuildFullScissorRect( + std::uint32_t width, + std::uint32_t height) { + ::XCEngine::RHI::Rect rect = {}; + rect.left = 0; + rect.top = 0; + rect.right = static_cast(width); + rect.bottom = static_cast(height); + return rect; +} + +::XCEngine::RHI::Rect ToPixelRect( + const ::XCEngine::UI::UIRect& rect, + float dpiScale, + std::uint32_t viewportWidth, + std::uint32_t viewportHeight) { + const float scaledLeft = rect.x * dpiScale; + const float scaledTop = rect.y * dpiScale; + const float scaledRight = (rect.x + rect.width) * dpiScale; + const float scaledBottom = (rect.y + rect.height) * dpiScale; + + ::XCEngine::RHI::Rect result = {}; + result.left = static_cast(std::floor(scaledLeft)); + result.top = static_cast(std::floor(scaledTop)); + result.right = static_cast(std::ceil(scaledRight)); + result.bottom = static_cast(std::ceil(scaledBottom)); + + return IntersectRects(result, BuildFullScissorRect(viewportWidth, viewportHeight)); +} + +QuadPassConstants BuildQuadPassConstants( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& color, + float dpiScale, + std::uint32_t viewportWidth, + std::uint32_t viewportHeight, + float rounding = 0.0f, + float strokeThickness = 0.0f, + const ::XCEngine::UI::UIPoint& uvMin = {}, + const ::XCEngine::UI::UIPoint& uvMax = ::XCEngine::UI::UIPoint(1.0f, 1.0f)) { + QuadPassConstants constants = {}; + constants.rectPx[0] = rect.x * dpiScale; + constants.rectPx[1] = rect.y * dpiScale; + constants.rectPx[2] = rect.width * dpiScale; + constants.rectPx[3] = rect.height * dpiScale; + constants.color[0] = color.r; + constants.color[1] = color.g; + constants.color[2] = color.b; + constants.color[3] = color.a; + constants.viewportPx[0] = static_cast(viewportWidth); + constants.viewportPx[1] = static_cast(viewportHeight); + constants.uvMin[0] = uvMin.x; + constants.uvMin[1] = uvMin.y; + constants.uvMax[0] = uvMax.x; + constants.uvMax[1] = uvMax.y; + constants.roundingPx = rounding * dpiScale; + constants.strokePx = strokeThickness * dpiScale; + return constants; +} + +bool TryBuildAxisAlignedLineRect( + const ::XCEngine::UI::UIDrawCommand& command, + ::XCEngine::UI::UIRect& outRect) { + outRect = {}; + const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f; + const float dx = command.uvMin.x - command.position.x; + const float dy = command.uvMin.y - command.position.y; + if (std::fabs(dx) <= kAxisAlignedLineTolerance) { + const float x = command.position.x - thickness * 0.5f; + const float top = (std::min)(command.position.y, command.uvMin.y); + const float height = std::fabs(dy); + outRect = ::XCEngine::UI::UIRect(x, top, thickness, height); + return true; + } + + if (std::fabs(dy) <= kAxisAlignedLineTolerance) { + const float y = command.position.y - thickness * 0.5f; + const float left = (std::min)(command.position.x, command.uvMin.x); + const float width = std::fabs(dx); + outRect = ::XCEngine::UI::UIRect(left, y, width, thickness); + return true; + } + + return false; +} + +std::vector ConvertPremultipliedBgraToPremultipliedRgba( + const std::uint8_t* pixels, + std::uint32_t width, + std::uint32_t height) { + const std::uint64_t pixelCount = + static_cast(width) * static_cast(height); + std::vector rgbaPixels(static_cast(pixelCount) * 4u); + for (std::uint64_t pixelIndex = 0u; pixelIndex < pixelCount; ++pixelIndex) { + const std::size_t offset = static_cast(pixelIndex) * 4u; + rgbaPixels[offset + 0u] = pixels[offset + 2u]; + rgbaPixels[offset + 1u] = pixels[offset + 1u]; + rgbaPixels[offset + 2u] = pixels[offset + 0u]; + rgbaPixels[offset + 3u] = pixels[offset + 3u]; + } + return rgbaPixels; +} + +std::string DescribeUnsupportedCommand( + const ::XCEngine::UI::UIDrawCommand& command, + std::size_t drawListIndex, + std::size_t commandIndex) { + const std::string location = + "drawList=" + std::to_string(drawListIndex) + + " command=" + std::to_string(commandIndex) + ": "; + switch (command.type) { + case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: + return location + "linear gradient fill is not implemented in the native D3D12 UI pass yet."; + case ::XCEngine::UI::UIDrawCommandType::Line: + return location + "non-axis-aligned line is not implemented in the native D3D12 UI pass yet."; + case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: + return location + "filled triangle is not implemented in the native D3D12 UI pass yet."; + case ::XCEngine::UI::UIDrawCommandType::FilledCircle: + return location + "filled circle is not implemented in the native D3D12 UI pass yet."; + case ::XCEngine::UI::UIDrawCommandType::CircleOutline: + return location + "circle outline is not implemented in the native D3D12 UI pass yet."; + default: + return location + "unsupported UI command."; + } +} + +} // namespace + +bool D3D12UIRenderer::AttachWindowRenderer(D3D12WindowRenderer& windowRenderer) { + m_windowRenderer = &windowRenderer; + m_lastError.clear(); + return true; +} + +void D3D12UIRenderer::DetachWindowRenderer() { + ReleaseResources(); + m_windowRenderer = nullptr; + m_textureDataSource = nullptr; + m_lastFrameAnalysis = {}; + m_lastError.clear(); +} + +void D3D12UIRenderer::SetTextureDataSource(const Ports::TextureDataPort* textureDataSource) { + m_textureDataSource = textureDataSource; +} + +bool D3D12UIRenderer::HasAttachedWindowRenderer() const { + return m_windowRenderer != nullptr; +} + +void D3D12UIRenderer::SetDpiScale(float dpiScale) { + m_dpiScale = ClampDpiScale(dpiScale); +} + +float D3D12UIRenderer::GetDpiScale() const { + return m_dpiScale; +} + +void D3D12UIRenderer::AnalyzeFrame(const UIDrawData& drawData) { + m_lastFrameAnalysis = BuildFrameAnalysis(drawData); +} + +bool D3D12UIRenderer::CanRender(const UIDrawData& drawData, std::string* outReason) { + AnalyzeFrame(drawData); + + if (m_windowRenderer == nullptr) { + m_lastError = "Native D3D12 UI renderer requires an attached window renderer."; + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + + if (m_lastFrameAnalysis.empty) { + m_lastError.clear(); + if (outReason != nullptr) { + outReason->clear(); + } + return true; + } + + const ::XCEngine::Rendering::RenderSurface* targetSurface = + m_windowRenderer->GetCurrentRenderSurface(); + if (targetSurface == nullptr) { + m_lastError = "Native D3D12 UI renderer could not resolve the current window render surface."; + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + + if (!EnsureShapePassResources(*targetSurface)) { + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + if ((m_lastFrameAnalysis.requiresImages || m_lastFrameAnalysis.requiresText) && + !EnsureImagePassResources(*targetSurface)) { + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + if (m_lastFrameAnalysis.requiresText && !EnsureTextRasterizer()) { + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + + std::string reason = {}; + if (!ValidateSupportedFrame(drawData, &reason)) { + m_lastError = std::move(reason); + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + + m_lastError.clear(); + if (outReason != nullptr) { + outReason->clear(); + } + return true; +} + +bool D3D12UIRenderer::Render(const UIDrawData& drawData) { + std::string reason = {}; + if (!CanRender(drawData, &reason)) { + m_lastError = std::move(reason); + return false; + } + + return RenderSupportedFrame(drawData); +} + +const D3D12UIRendererFrameAnalysis& D3D12UIRenderer::GetLastFrameAnalysis() const { + return m_lastFrameAnalysis; +} + +const std::string& D3D12UIRenderer::GetLastError() const { + return m_lastError; +} + +void D3D12UIRenderer::ReleaseResources() { + ReleasePipelineResources(); + ReleaseUploadedTextureCaches(); + ReleaseTextRasterizer(); +} + +void D3D12UIRenderer::ReleasePipelineResources() { + if (m_shapePipelineState != nullptr) { + m_shapePipelineState->Shutdown(); + delete m_shapePipelineState; + m_shapePipelineState = nullptr; + } + + if (m_shapePipelineLayout != nullptr) { + m_shapePipelineLayout->Shutdown(); + delete m_shapePipelineLayout; + m_shapePipelineLayout = nullptr; + } + + if (m_imagePipelineState != nullptr) { + m_imagePipelineState->Shutdown(); + delete m_imagePipelineState; + m_imagePipelineState = nullptr; + } + + if (m_imagePipelineLayout != nullptr) { + m_imagePipelineLayout->Shutdown(); + delete m_imagePipelineLayout; + m_imagePipelineLayout = nullptr; + } + + if (m_imageSamplerHeap != nullptr) { + m_imageSamplerHeap->Shutdown(); + delete m_imageSamplerHeap; + m_imageSamplerHeap = nullptr; + } + m_imageSamplerCpuHandle = {}; + m_imageSamplerGpuHandle = {}; + + ReleaseFrameConstantBuffers(); + m_cachedRenderTargetFormat = 0u; + m_cachedSampleCount = 0u; + m_cachedSampleQuality = 0u; +} + +void D3D12UIRenderer::ReleaseFrameConstantBuffers() { + for (auto*& buffer : m_frameConstantBuffers) { + if (buffer != nullptr) { + buffer->Shutdown(); + delete buffer; + buffer = nullptr; + } + } + + m_frameConstantBuffers.clear(); + m_frameConstantBufferCapacities.clear(); +} + +void D3D12UIRenderer::ReleaseUploadedTexture(UploadedTextureCacheEntry& texture) { + if (texture.texture != nullptr) { + texture.texture->Shutdown(); + delete texture.texture; + texture.texture = nullptr; + } + if (texture.cpuHandle.ptr != 0u || texture.gpuHandle.ptr != 0u) { + m_uploadedTextureAllocator.Free(texture.cpuHandle, texture.gpuHandle); + } + + texture = {}; +} + +void D3D12UIRenderer::ReleaseUploadedTextureCaches() { + for (auto& entry : m_uploadedTextureCache) { + ReleaseUploadedTexture(entry.second); + } + m_uploadedTextureCache.clear(); + + for (auto& entry : m_cachedTextTextures) { + ReleaseUploadedTexture(entry.second.uploaded); + } + m_cachedTextTextures.clear(); + + m_uploadedTextureAllocator.Shutdown(); +} + +void D3D12UIRenderer::ReleaseTextRasterizer() { + m_textFormats.clear(); + m_textWicFactory.Reset(); + m_textDWriteFactory.Reset(); + m_textD2DFactory.Reset(); + if (m_textComInitialized) { + CoUninitialize(); + m_textComInitialized = false; + } +} + +bool D3D12UIRenderer::EnsureShapePassResources( + const ::XCEngine::Rendering::RenderSurface& targetSurface) { + if (m_windowRenderer == nullptr) { + m_lastError = "Native D3D12 UI renderer requires an attached window renderer."; + return false; + } + + if (!::XCEngine::Rendering::HasSingleColorAttachment(targetSurface)) { + m_lastError = "Native D3D12 UI renderer requires a single-color render target."; + return false; + } + + const std::uint32_t renderTargetFormat = static_cast( + ::XCEngine::Rendering::ResolveSurfaceColorFormat(targetSurface, 0u)); + const std::uint32_t sampleCount = + ::XCEngine::Rendering::ResolveSurfaceSampleCount(targetSurface); + const std::uint32_t sampleQuality = + ::XCEngine::Rendering::ResolveSurfaceSampleQuality(targetSurface); + if (renderTargetFormat == + static_cast(::XCEngine::RHI::Format::Unknown)) { + m_lastError = "Native D3D12 UI renderer could not resolve the swap chain color format."; + return false; + } + + if (m_shapePipelineLayout != nullptr && + m_shapePipelineState != nullptr && + m_cachedRenderTargetFormat == renderTargetFormat && + m_cachedSampleCount == sampleCount && + m_cachedSampleQuality == sampleQuality) { + return true; + } + + ReleasePipelineResources(); + + ::XCEngine::RHI::RHIDevice* device = m_windowRenderer->GetRHIDevice(); + if (device == nullptr) { + m_lastError = "Native D3D12 UI renderer could not resolve the host RHI device."; + return false; + } + + ::XCEngine::RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.constantBufferCount = 1u; + m_shapePipelineLayout = device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_shapePipelineLayout == nullptr) { + m_lastError = "Native D3D12 UI renderer failed to create the shape-pass pipeline layout."; + ReleasePipelineResources(); + return false; + } + + ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = m_shapePipelineLayout; + pipelineDesc.topologyType = + static_cast(::XCEngine::RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.vertexShader = BuildShaderDesc(kQuadVertexShaderSource, "VSMain", "vs_5_0"); + pipelineDesc.fragmentShader = BuildShaderDesc(kShapePixelShaderSource, "PSMain", "ps_5_0"); + pipelineDesc.rasterizerState.cullMode = + static_cast(::XCEngine::RHI::CullMode::None); + pipelineDesc.rasterizerState.scissorTestEnable = true; + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = + static_cast(::XCEngine::RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + ::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + targetSurface, + pipelineDesc); + + m_shapePipelineState = device->CreatePipelineState(pipelineDesc); + if (m_shapePipelineState == nullptr || !m_shapePipelineState->IsValid()) { + m_lastError = "Native D3D12 UI renderer failed to create the shape-pass pipeline state."; + ReleasePipelineResources(); + return false; + } + + m_cachedRenderTargetFormat = renderTargetFormat; + m_cachedSampleCount = sampleCount; + m_cachedSampleQuality = sampleQuality; + m_lastError.clear(); + return true; +} + +bool D3D12UIRenderer::EnsureImagePassResources( + const ::XCEngine::Rendering::RenderSurface& targetSurface) { + if (!EnsureShapePassResources(targetSurface)) { + return false; + } + + if (m_imagePipelineLayout != nullptr && + m_imagePipelineState != nullptr && + m_imageSamplerHeap != nullptr && + m_uploadedTextureAllocator.IsInitialized()) { + return true; + } + + ::XCEngine::RHI::RHIDevice* device = m_windowRenderer->GetRHIDevice(); + ID3D12Device* nativeDevice = m_windowRenderer->GetDevice(); + if (device == nullptr || nativeDevice == nullptr) { + m_lastError = "Native D3D12 UI renderer could not resolve the host D3D12 device."; + return false; + } + + if (!m_uploadedTextureAllocator.IsInitialized() && + !m_uploadedTextureAllocator.Initialize(*device, 256u)) { + m_lastError = "Native D3D12 UI renderer failed to initialize the uploaded-texture descriptor allocator."; + return false; + } + + auto* samplerHeap = new ::XCEngine::RHI::D3D12DescriptorHeap(); + if (!samplerHeap->Initialize( + nativeDevice, + ::XCEngine::RHI::DescriptorHeapType::Sampler, + 1u, + true)) { + delete samplerHeap; + m_lastError = "Native D3D12 UI renderer failed to create the image sampler heap."; + return false; + } + + D3D12_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplerDesc.MinLOD = 0.0f; + samplerDesc.MaxLOD = D3D12_FLOAT32_MAX; + nativeDevice->CreateSampler( + &samplerDesc, + samplerHeap->GetCPUDescriptorHandleForHeapStart()); + m_imageSamplerHeap = samplerHeap; + m_imageSamplerCpuHandle = samplerHeap->GetCPUDescriptorHandleForHeapStart(); + m_imageSamplerGpuHandle = samplerHeap->GetGPUDescriptorHandleForHeapStart(); + + ::XCEngine::RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.constantBufferCount = 1u; + pipelineLayoutDesc.textureCount = 1u; + pipelineLayoutDesc.samplerCount = 1u; + m_imagePipelineLayout = device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_imagePipelineLayout == nullptr) { + m_lastError = "Native D3D12 UI renderer failed to create the image-pass pipeline layout."; + ReleasePipelineResources(); + return false; + } + + ::XCEngine::RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = m_imagePipelineLayout; + pipelineDesc.topologyType = + static_cast(::XCEngine::RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.vertexShader = BuildShaderDesc(kQuadVertexShaderSource, "VSMain", "vs_5_0"); + pipelineDesc.fragmentShader = BuildShaderDesc(kImagePixelShaderSource, "PSMain", "ps_5_0"); + pipelineDesc.rasterizerState.cullMode = + static_cast(::XCEngine::RHI::CullMode::None); + pipelineDesc.rasterizerState.scissorTestEnable = true; + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = + static_cast(::XCEngine::RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlend = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = + static_cast(::XCEngine::RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + ::XCEngine::Rendering::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc( + targetSurface, + pipelineDesc); + + m_imagePipelineState = device->CreatePipelineState(pipelineDesc); + if (m_imagePipelineState == nullptr || !m_imagePipelineState->IsValid()) { + m_lastError = "Native D3D12 UI renderer failed to create the image-pass pipeline state."; + ReleasePipelineResources(); + return false; + } + + m_lastError.clear(); + return true; +} + +bool D3D12UIRenderer::EnsureFrameConstantBufferCapacity( + std::uint32_t frameSlot, + std::size_t requiredDrawCount) { + if (m_windowRenderer == nullptr) { + m_lastError = "Native D3D12 UI renderer requires an attached window renderer."; + return false; + } + + if (frameSlot >= D3D12WindowRenderer::kFrameContextCount) { + m_lastError = "Native D3D12 UI renderer received an invalid frame slot."; + return false; + } + + if (m_frameConstantBuffers.size() < D3D12WindowRenderer::kFrameContextCount) { + m_frameConstantBuffers.resize(D3D12WindowRenderer::kFrameContextCount, nullptr); + m_frameConstantBufferCapacities.resize(D3D12WindowRenderer::kFrameContextCount, 0u); + } + + const std::uint64_t alignedConstantSize = + AlignConstantBufferSize(sizeof(QuadPassConstants)); + const std::uint64_t requiredCapacity = + alignedConstantSize * (std::max)(requiredDrawCount, 1u); + if (m_frameConstantBuffers[frameSlot] != nullptr && + m_frameConstantBufferCapacities[frameSlot] >= requiredCapacity) { + return true; + } + + ID3D12Device* nativeDevice = m_windowRenderer->GetDevice(); + if (nativeDevice == nullptr) { + m_lastError = "Native D3D12 UI renderer could not resolve the host D3D12 device."; + return false; + } + + auto* newBuffer = new ::XCEngine::RHI::D3D12Buffer(); + if (!newBuffer->Initialize( + nativeDevice, + requiredCapacity, + D3D12_RESOURCE_STATE_GENERIC_READ, + D3D12_HEAP_TYPE_UPLOAD)) { + delete newBuffer; + m_lastError = "Native D3D12 UI renderer failed to create the per-frame constant buffer."; + return false; + } + + newBuffer->SetBufferType(::XCEngine::RHI::BufferType::Constant); + newBuffer->SetStride(static_cast(alignedConstantSize)); + if (m_frameConstantBuffers[frameSlot] != nullptr) { + m_frameConstantBuffers[frameSlot]->Shutdown(); + delete m_frameConstantBuffers[frameSlot]; + } + + m_frameConstantBuffers[frameSlot] = newBuffer; + m_frameConstantBufferCapacities[frameSlot] = requiredCapacity; + return true; +} + +bool D3D12UIRenderer::EnsureTextRasterizer() { + if (m_textD2DFactory != nullptr && + m_textDWriteFactory != nullptr && + m_textWicFactory != nullptr) { + return true; + } + + const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) { + m_lastError = "Native D3D12 UI renderer failed to initialize COM for text rasterization."; + return false; + } + if (SUCCEEDED(initHr)) { + m_textComInitialized = true; + } + + D2D1_FACTORY_OPTIONS factoryOptions = {}; + HRESULT hr = D2D1CreateFactory( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + __uuidof(ID2D1Factory1), + &factoryOptions, + reinterpret_cast(m_textD2DFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr) || m_textD2DFactory == nullptr) { + m_lastError = "Native D3D12 UI renderer failed to create the offline D2D factory."; + return false; + } + + hr = DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast(m_textDWriteFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr) || m_textDWriteFactory == nullptr) { + m_lastError = "Native D3D12 UI renderer failed to create the offline DWrite factory."; + return false; + } + + hr = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(m_textWicFactory.ReleaseAndGetAddressOf())); + if (FAILED(hr) || m_textWicFactory == nullptr) { + m_lastError = "Native D3D12 UI renderer failed to create the offline WIC factory."; + return false; + } + + m_lastError.clear(); + return true; +} + +IDWriteTextFormat* D3D12UIRenderer::GetTextFormat(float scaledFontSize) { + if (m_textDWriteFactory == nullptr) { + return nullptr; + } + + const int key = static_cast(std::lround(scaledFontSize * 10.0f)); + const auto found = m_textFormats.find(key); + if (found != m_textFormats.end()) { + return found->second.Get(); + } + + ComPtr textFormat = {}; + const HRESULT hr = m_textDWriteFactory->CreateTextFormat( + L"Segoe UI", + nullptr, + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + scaledFontSize, + L"", + textFormat.ReleaseAndGetAddressOf()); + if (FAILED(hr) || textFormat == nullptr) { + return nullptr; + } + + textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); + textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); + textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + + IDWriteTextFormat* result = textFormat.Get(); + m_textFormats.emplace(key, std::move(textFormat)); + return result; +} + +bool D3D12UIRenderer::ValidateSupportedFrame( + const UIDrawData& drawData, + std::string* outReason) { + if (outReason != nullptr) { + outReason->clear(); + } + + for (std::size_t drawListIndex = 0u; + drawListIndex < drawData.GetDrawLists().size(); + ++drawListIndex) { + const UIDrawList& drawList = drawData.GetDrawLists()[drawListIndex]; + for (std::size_t commandIndex = 0u; + commandIndex < drawList.GetCommands().size(); + ++commandIndex) { + const UIDrawCommand& command = drawList.GetCommands()[commandIndex]; + switch (command.type) { + case UIDrawCommandType::FilledRect: + case UIDrawCommandType::RectOutline: + continue; + case UIDrawCommandType::Line: { + ::XCEngine::UI::UIRect lineRect = {}; + if (TryBuildAxisAlignedLineRect(command, lineRect)) { + continue; + } + break; + } + case UIDrawCommandType::Image: { + ID3D12DescriptorHeap* descriptorHeap = nullptr; + D3D12_GPU_DESCRIPTOR_HANDLE descriptor = {}; + std::string imageReason = {}; + if (ResolveUploadedTextureBinding( + command.texture, + &descriptorHeap, + &descriptor, + &imageReason)) { + continue; + } + if (outReason != nullptr) { + *outReason = imageReason; + } + return false; + } + case UIDrawCommandType::Text: { + ID3D12DescriptorHeap* descriptorHeap = nullptr; + D3D12_GPU_DESCRIPTOR_HANDLE descriptor = {}; + ::XCEngine::UI::UIRect textRect = {}; + std::string textReason = {}; + if (ResolveTextTextureBinding( + command, + &descriptorHeap, + &descriptor, + textRect, + &textReason)) { + continue; + } + if (outReason != nullptr) { + *outReason = textReason; + } + return false; + } + case UIDrawCommandType::PushClipRect: + case UIDrawCommandType::PopClipRect: + continue; + default: + break; + } + + if (outReason != nullptr) { + *outReason = DescribeUnsupportedCommand(command, drawListIndex, commandIndex); + } + return false; + } + } + + return true; +} + +bool D3D12UIRenderer::ResolveUploadedTextureBinding( + const ::XCEngine::UI::UITextureHandle& texture, + ID3D12DescriptorHeap** outDescriptorHeap, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle, + std::string* outReason) { + if (outDescriptorHeap != nullptr) { + *outDescriptorHeap = nullptr; + } + if (outGpuHandle != nullptr) { + *outGpuHandle = {}; + } + if (outReason != nullptr) { + outReason->clear(); + } + + if (!texture.IsValid()) { + if (outReason != nullptr) { + *outReason = "image command references an invalid texture handle."; + } + return false; + } + + if (texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) { + if (m_windowRenderer == nullptr || + m_windowRenderer->GetViewportTextureDescriptorHeap() == nullptr || + texture.nativeHandle == 0u) { + if (outReason != nullptr) { + *outReason = "image command references an unresolved shader-resource texture handle."; + } + return false; + } + + if (outDescriptorHeap != nullptr) { + *outDescriptorHeap = m_windowRenderer->GetViewportTextureDescriptorHeap(); + } + if (outGpuHandle != nullptr) { + outGpuHandle->ptr = static_cast(texture.nativeHandle); + } + return true; + } + + if (texture.kind != ::XCEngine::UI::UITextureHandleKind::DescriptorHandle || + m_textureDataSource == nullptr) { + if (outReason != nullptr) { + *outReason = "image command requires a CPU texture data source that is unavailable."; + } + return false; + } + + Ports::TexturePixelDataView pixelData = {}; + if (!m_textureDataSource->ResolveTexturePixelData(texture, pixelData) || + pixelData.pixels == nullptr || + pixelData.width == 0u || + pixelData.height == 0u) { + if (outReason != nullptr) { + *outReason = "image command references a released or unresolved CPU texture."; + } + return false; + } + + auto& cachedTexture = m_uploadedTextureCache[texture.nativeHandle]; + if (cachedTexture.texture == nullptr || + cachedTexture.sourcePixels != pixelData.pixels || + cachedTexture.width != pixelData.width || + cachedTexture.height != pixelData.height) { + ReleaseUploadedTexture(cachedTexture); + + if (!m_uploadedTextureAllocator.IsInitialized()) { + if (outReason != nullptr) { + *outReason = "native uploaded-texture descriptor allocator is not initialized."; + } + return false; + } + + ::XCEngine::RHI::RHIDevice* device = m_windowRenderer->GetRHIDevice(); + if (device == nullptr) { + if (outReason != nullptr) { + *outReason = "native D3D12 UI renderer could not resolve the host RHI device."; + } + return false; + } + + const std::vector rgbaPixels = + ConvertPremultipliedBgraToPremultipliedRgba( + pixelData.pixels, + pixelData.width, + pixelData.height); + ::XCEngine::RHI::TextureDesc textureDesc = {}; + textureDesc.width = pixelData.width; + textureDesc.height = pixelData.height; + textureDesc.depth = 1u; + textureDesc.mipLevels = 1u; + textureDesc.arraySize = 1u; + textureDesc.format = + static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); + textureDesc.textureType = + static_cast(::XCEngine::RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1u; + textureDesc.sampleQuality = 0u; + textureDesc.flags = 0u; + cachedTexture.texture = device->CreateTexture( + textureDesc, + rgbaPixels.data(), + rgbaPixels.size(), + pixelData.width * 4u); + if (cachedTexture.texture == nullptr) { + if (outReason != nullptr) { + *outReason = "native D3D12 UI renderer failed to upload a CPU texture to the GPU."; + } + ReleaseUploadedTexture(cachedTexture); + return false; + } + + if (!m_uploadedTextureAllocator.CreateTextureDescriptor( + cachedTexture.texture, + &cachedTexture.cpuHandle, + &cachedTexture.gpuHandle)) { + if (outReason != nullptr) { + *outReason = "native D3D12 UI renderer failed to allocate a GPU descriptor for a CPU texture."; + } + ReleaseUploadedTexture(cachedTexture); + return false; + } + + cachedTexture.sourcePixels = pixelData.pixels; + cachedTexture.width = pixelData.width; + cachedTexture.height = pixelData.height; + } + + if (outDescriptorHeap != nullptr) { + *outDescriptorHeap = m_uploadedTextureAllocator.GetDescriptorHeap(); + } + if (outGpuHandle != nullptr) { + *outGpuHandle = cachedTexture.gpuHandle; + } + return true; +} + +bool D3D12UIRenderer::ResolveTextTextureBinding( + const ::XCEngine::UI::UIDrawCommand& command, + ID3D12DescriptorHeap** outDescriptorHeap, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle, + ::XCEngine::UI::UIRect& outRect, + std::string* outReason) { + if (outDescriptorHeap != nullptr) { + *outDescriptorHeap = nullptr; + } + if (outGpuHandle != nullptr) { + *outGpuHandle = {}; + } + outRect = {}; + if (outReason != nullptr) { + outReason->clear(); + } + + if (!EnsureTextRasterizer()) { + if (outReason != nullptr) { + *outReason = m_lastError; + } + return false; + } + if (!m_uploadedTextureAllocator.IsInitialized()) { + if (outReason != nullptr) { + *outReason = "native uploaded-texture descriptor allocator is not initialized."; + } + return false; + } + if (command.text.empty()) { + if (outReason != nullptr) { + *outReason = "text command is empty."; + } + return false; + } + + const float dpiScale = ClampDpiScale(m_dpiScale); + const float scaledFontSize = ResolveFontSize(command.fontSize) * dpiScale; + const int fontKey = static_cast(std::lround(scaledFontSize * 10.0f)); + const std::string cacheKey = + std::to_string(fontKey) + "|" + command.text; + + auto cacheEntry = m_cachedTextTextures.find(cacheKey); + if (cacheEntry == m_cachedTextTextures.end()) { + IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize); + if (textFormat == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not create a DWrite text format."; + } + return false; + } + + const std::wstring wideText = Utf8ToWide(command.text); + if (wideText.empty()) { + if (outReason != nullptr) { + *outReason = "native text cache could not convert UTF-8 text to UTF-16."; + } + return false; + } + + ComPtr textLayout = {}; + const float lineHeight = std::ceil(scaledFontSize * 1.6f); + HRESULT hr = m_textDWriteFactory->CreateTextLayout( + wideText.c_str(), + static_cast(wideText.size()), + textFormat, + 4096.0f, + lineHeight, + textLayout.ReleaseAndGetAddressOf()); + if (FAILED(hr) || textLayout == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not create a text layout."; + } + return false; + } + + DWRITE_TEXT_METRICS textMetrics = {}; + hr = textLayout->GetMetrics(&textMetrics); + if (FAILED(hr)) { + if (outReason != nullptr) { + *outReason = "native text cache could not query text metrics."; + } + return false; + } + + const UINT widthPx = static_cast((std::max)( + 1.0f, + std::ceil(textMetrics.widthIncludingTrailingWhitespace) + 2.0f)); + const UINT heightPx = static_cast((std::max)( + 1.0f, + std::ceil(lineHeight) + 2.0f)); + + ComPtr bitmap = {}; + hr = m_textWicFactory->CreateBitmap( + widthPx, + heightPx, + GUID_WICPixelFormat32bppPBGRA, + WICBitmapCacheOnLoad, + bitmap.ReleaseAndGetAddressOf()); + if (FAILED(hr) || bitmap == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not create a WIC bitmap."; + } + return false; + } + + const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = + D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + kBaseDpi, + kBaseDpi); + + ComPtr renderTarget = {}; + hr = m_textD2DFactory->CreateWicBitmapRenderTarget( + bitmap.Get(), + renderTargetProperties, + renderTarget.ReleaseAndGetAddressOf()); + if (FAILED(hr) || renderTarget == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not create a WIC bitmap render target."; + } + return false; + } + + ComPtr brush = {}; + hr = renderTarget->CreateSolidColorBrush( + D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), + brush.ReleaseAndGetAddressOf()); + if (FAILED(hr) || brush == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not create a text brush."; + } + return false; + } + + renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + renderTarget->BeginDraw(); + renderTarget->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f)); + renderTarget->DrawTextW( + wideText.c_str(), + static_cast(wideText.size()), + textFormat, + D2D1::RectF(0.0f, 0.0f, static_cast(widthPx), static_cast(heightPx)), + brush.Get(), + D2D1_DRAW_TEXT_OPTIONS_CLIP, + DWRITE_MEASURING_MODE_GDI_NATURAL); + hr = renderTarget->EndDraw(); + if (FAILED(hr)) { + if (outReason != nullptr) { + *outReason = "native text cache could not rasterize a text bitmap."; + } + return false; + } + + std::vector bgraPixels( + static_cast(widthPx) * static_cast(heightPx) * 4u); + hr = bitmap->CopyPixels( + nullptr, + widthPx * 4u, + static_cast(bgraPixels.size()), + bgraPixels.data()); + if (FAILED(hr)) { + if (outReason != nullptr) { + *outReason = "native text cache could not read back text bitmap pixels."; + } + return false; + } + + const std::vector rgbaPixels = + ConvertPremultipliedBgraToPremultipliedRgba( + bgraPixels.data(), + widthPx, + heightPx); + + ::XCEngine::RHI::TextureDesc textureDesc = {}; + textureDesc.width = widthPx; + textureDesc.height = heightPx; + textureDesc.depth = 1u; + textureDesc.mipLevels = 1u; + textureDesc.arraySize = 1u; + textureDesc.format = + static_cast(::XCEngine::RHI::Format::R8G8B8A8_UNorm); + textureDesc.textureType = + static_cast(::XCEngine::RHI::TextureType::Texture2D); + textureDesc.sampleCount = 1u; + textureDesc.sampleQuality = 0u; + textureDesc.flags = 0u; + + CachedTextTextureEntry newEntry = {}; + newEntry.uploaded.texture = m_windowRenderer->GetRHIDevice()->CreateTexture( + textureDesc, + rgbaPixels.data(), + rgbaPixels.size(), + widthPx * 4u); + if (newEntry.uploaded.texture == nullptr) { + if (outReason != nullptr) { + *outReason = "native text cache could not upload a text bitmap to the GPU."; + } + return false; + } + + if (!m_uploadedTextureAllocator.CreateTextureDescriptor( + newEntry.uploaded.texture, + &newEntry.uploaded.cpuHandle, + &newEntry.uploaded.gpuHandle)) { + ReleaseUploadedTexture(newEntry.uploaded); + if (outReason != nullptr) { + *outReason = "native text cache could not allocate a GPU descriptor for a text bitmap."; + } + return false; + } + + newEntry.uploaded.width = widthPx; + newEntry.uploaded.height = heightPx; + newEntry.widthDips = static_cast(widthPx) / dpiScale; + newEntry.heightDips = static_cast(heightPx) / dpiScale; + + cacheEntry = m_cachedTextTextures.emplace(cacheKey, std::move(newEntry)).first; + } + + if (outDescriptorHeap != nullptr) { + *outDescriptorHeap = m_uploadedTextureAllocator.GetDescriptorHeap(); + } + if (outGpuHandle != nullptr) { + *outGpuHandle = cacheEntry->second.uploaded.gpuHandle; + } + outRect = ::XCEngine::UI::UIRect( + command.position.x, + command.position.y, + cacheEntry->second.widthDips, + cacheEntry->second.heightDips); + return true; +} + +bool D3D12UIRenderer::RenderSupportedFrame(const UIDrawData& drawData) { + if (m_windowRenderer == nullptr) { + m_lastError = "Native D3D12 UI renderer requires an attached window renderer."; + return false; + } + + const ::XCEngine::Rendering::RenderContext renderContext = + m_windowRenderer->GetRenderContext(); + const ::XCEngine::Rendering::RenderSurface* targetSurface = + m_windowRenderer->GetCurrentRenderSurface(); + auto* nativeCommandList = + renderContext.commandList != nullptr + ? static_cast<::XCEngine::RHI::D3D12CommandList*>(renderContext.commandList) + : nullptr; + if (!renderContext.IsValid() || + renderContext.commandList == nullptr || + nativeCommandList == nullptr || + targetSurface == nullptr || + !::XCEngine::Rendering::HasSingleColorAttachment(*targetSurface)) { + m_lastError = "Native D3D12 UI renderer could not resolve the current frame render target."; + return false; + } + + if (drawData.Empty()) { + if (!m_windowRenderer->SubmitFrame(false)) { + m_lastError = "Native D3D12 UI renderer failed to submit an empty frame: " + + m_windowRenderer->GetLastError(); + return false; + } + if (!m_windowRenderer->SignalFrameCompletion()) { + m_lastError = "Native D3D12 UI renderer failed to signal an empty frame."; + return false; + } + if (!m_windowRenderer->PresentFrame()) { + m_lastError = "Native D3D12 UI renderer failed to present an empty frame."; + return false; + } + + m_lastError.clear(); + return true; + } + + if (!EnsureShapePassResources(*targetSurface) || + ((m_lastFrameAnalysis.requiresImages || m_lastFrameAnalysis.requiresText) && + !EnsureImagePassResources(*targetSurface))) { + return false; + } + + std::size_t requiredDrawCount = 0u; + for (const UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + switch (command.type) { + case UIDrawCommandType::FilledRect: + case UIDrawCommandType::RectOutline: + case UIDrawCommandType::Image: + case UIDrawCommandType::Text: + ++requiredDrawCount; + break; + case UIDrawCommandType::Line: { + ::XCEngine::UI::UIRect lineRect = {}; + if (TryBuildAxisAlignedLineRect(command, lineRect)) { + ++requiredDrawCount; + } + break; + } + default: + break; + } + } + } + + const std::uint32_t frameSlot = m_windowRenderer->GetActiveFrameSlot(); + if (!EnsureFrameConstantBufferCapacity(frameSlot, requiredDrawCount)) { + return false; + } + + if (!m_windowRenderer->PreparePresentSurface()) { + m_lastError = "Native D3D12 UI renderer failed to prepare the D3D12 present surface: " + + m_windowRenderer->GetLastError(); + return false; + } + + std::vector<::XCEngine::RHI::RHIResourceView*> renderTargets = + targetSurface->GetColorAttachments(); + if (renderTargets.empty() || renderTargets[0] == nullptr) { + m_lastError = "Native D3D12 UI renderer could not resolve the swap chain render target view."; + return false; + } + + auto* commandList = renderContext.commandList; + commandList->SetRenderTargets( + static_cast(renderTargets.size()), + renderTargets.data(), + nullptr); + + ::XCEngine::RHI::Viewport viewport = {}; + viewport.topLeftX = 0.0f; + viewport.topLeftY = 0.0f; + viewport.width = static_cast(targetSurface->GetWidth()); + viewport.height = static_cast(targetSurface->GetHeight()); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + commandList->SetViewport(viewport); + commandList->SetPrimitiveTopology(::XCEngine::RHI::PrimitiveTopology::TriangleList); + + const auto* shapeLayout = + static_cast(m_shapePipelineLayout); + const auto* imageLayout = + static_cast(m_imagePipelineLayout); + const std::uint32_t shapeCbvRootIndex = + shapeLayout->GetConstantBufferRootParameterIndex(kQuadPassConstantBinding); + const std::uint32_t imageCbvRootIndex = + imageLayout != nullptr + ? imageLayout->GetConstantBufferRootParameterIndex(kQuadPassConstantBinding) + : 0u; + const std::uint32_t imageSrvRootIndex = + imageLayout != nullptr + ? imageLayout->GetShaderResourceTableRootParameterIndex() + : 0u; + const std::uint32_t imageSamplerRootIndex = + imageLayout != nullptr + ? imageLayout->GetSamplerTableRootParameterIndex() + : 0u; + const std::uint64_t alignedConstantSize = + AlignConstantBufferSize(sizeof(QuadPassConstants)); + auto* frameConstantBuffer = m_frameConstantBuffers[frameSlot]; + std::uint64_t emittedDrawCount = 0u; + const float dpiScale = ClampDpiScale(m_dpiScale); + + std::vector<::XCEngine::RHI::Rect> clipStack = {}; + const ::XCEngine::RHI::Rect fullScissor = BuildFullScissorRect( + targetSurface->GetWidth(), + targetSurface->GetHeight()); + commandList->SetScissorRect(fullScissor); + + enum class ActivePipeline : std::uint8_t { + None = 0, + Shape, + Image + }; + + ActivePipeline activePipeline = ActivePipeline::None; + ID3D12DescriptorHeap* activeTextureHeap = nullptr; + + const auto writeConstants = + [&](const QuadPassConstants& constants, std::uint32_t rootParameterIndex) { + const std::uint64_t constantOffset = emittedDrawCount * alignedConstantSize; + frameConstantBuffer->SetData( + &constants, + sizeof(constants), + static_cast(constantOffset)); + nativeCommandList->SetGraphicsRootConstantBufferView( + rootParameterIndex, + frameConstantBuffer->GetGPUVirtualAddress() + constantOffset); + ++emittedDrawCount; + }; + + const auto currentClipOrRect = + [&](const ::XCEngine::UI::UIRect& rect) { + const ::XCEngine::RHI::Rect rectPixels = ToPixelRect( + rect, + dpiScale, + targetSurface->GetWidth(), + targetSurface->GetHeight()); + return clipStack.empty() + ? rectPixels + : IntersectRects(rectPixels, clipStack.back()); + }; + + const auto emitShape = + [&](const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& color, + float rounding, + float strokeThickness) { + if (rect.width <= 0.0f || rect.height <= 0.0f || color.a <= 0.0f) { + return true; + } + + const ::XCEngine::RHI::Rect activeScissor = currentClipOrRect(rect); + if (IsEmptyRect(activeScissor)) { + return true; + } + + if (activePipeline != ActivePipeline::Shape) { + commandList->SetPipelineState(m_shapePipelineState); + activePipeline = ActivePipeline::Shape; + activeTextureHeap = nullptr; + } + + const QuadPassConstants constants = BuildQuadPassConstants( + rect, + color, + dpiScale, + targetSurface->GetWidth(), + targetSurface->GetHeight(), + rounding, + strokeThickness); + writeConstants(constants, shapeCbvRootIndex); + commandList->SetScissorRect(activeScissor); + commandList->Draw(6u); + return true; + }; + + const auto emitImage = + [&](const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIColor& tintColor, + const ::XCEngine::UI::UIPoint& uvMin, + const ::XCEngine::UI::UIPoint& uvMax, + ID3D12DescriptorHeap* textureHeap, + D3D12_GPU_DESCRIPTOR_HANDLE textureHandle, + const char* failureLabel) { + if (rect.width <= 0.0f || rect.height <= 0.0f) { + return true; + } + + const ::XCEngine::RHI::Rect activeScissor = currentClipOrRect(rect); + if (IsEmptyRect(activeScissor)) { + return true; + } + + if (textureHeap == nullptr || textureHandle.ptr == 0u || m_imageSamplerHeap == nullptr) { + m_lastError = failureLabel; + return false; + } + + if (activePipeline != ActivePipeline::Image) { + commandList->SetPipelineState(m_imagePipelineState); + activePipeline = ActivePipeline::Image; + activeTextureHeap = nullptr; + } + + if (activeTextureHeap != textureHeap) { + ID3D12DescriptorHeap* heaps[] = { + textureHeap, + m_imageSamplerHeap->GetDescriptorHeap() + }; + nativeCommandList->SetDescriptorHeaps(2u, heaps); + nativeCommandList->SetGraphicsRootDescriptorTable( + imageSamplerRootIndex, + m_imageSamplerGpuHandle); + activeTextureHeap = textureHeap; + } + + const QuadPassConstants constants = BuildQuadPassConstants( + rect, + tintColor, + dpiScale, + targetSurface->GetWidth(), + targetSurface->GetHeight(), + 0.0f, + 0.0f, + uvMin, + uvMax); + writeConstants(constants, imageCbvRootIndex); + nativeCommandList->SetGraphicsRootDescriptorTable( + imageSrvRootIndex, + textureHandle); + commandList->SetScissorRect(activeScissor); + commandList->Draw(6u); + return true; + }; + + for (const UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + switch (command.type) { + case UIDrawCommandType::FilledRect: + if (!emitShape(command.rect, command.color, command.rounding, 0.0f)) { + return false; + } + break; + case UIDrawCommandType::RectOutline: + if (!emitShape(command.rect, command.color, command.rounding, command.thickness)) { + return false; + } + break; + case UIDrawCommandType::Line: { + ::XCEngine::UI::UIRect lineRect = {}; + if (TryBuildAxisAlignedLineRect(command, lineRect) && + !emitShape(lineRect, command.color, 0.0f, 0.0f)) { + return false; + } + break; + } + case UIDrawCommandType::Image: { + ID3D12DescriptorHeap* descriptorHeap = nullptr; + D3D12_GPU_DESCRIPTOR_HANDLE descriptorHandle = {}; + std::string imageReason = {}; + if (!ResolveUploadedTextureBinding( + command.texture, + &descriptorHeap, + &descriptorHandle, + &imageReason)) { + m_lastError = imageReason; + return false; + } + if (!emitImage( + command.rect, + command.color, + command.uvMin, + command.uvMax, + descriptorHeap, + descriptorHandle, + "native D3D12 UI renderer failed to bind an image texture.")) { + return false; + } + break; + } + case UIDrawCommandType::Text: { + ID3D12DescriptorHeap* descriptorHeap = nullptr; + D3D12_GPU_DESCRIPTOR_HANDLE descriptorHandle = {}; + ::XCEngine::UI::UIRect textRect = {}; + std::string textReason = {}; + if (!ResolveTextTextureBinding( + command, + &descriptorHeap, + &descriptorHandle, + textRect, + &textReason)) { + m_lastError = textReason; + return false; + } + if (!emitImage( + textRect, + command.color, + {}, + ::XCEngine::UI::UIPoint(1.0f, 1.0f), + descriptorHeap, + descriptorHandle, + "native D3D12 UI renderer failed to bind a text texture.")) { + return false; + } + break; + } + case UIDrawCommandType::PushClipRect: { + ::XCEngine::RHI::Rect clipRect = ToPixelRect( + command.rect, + dpiScale, + targetSurface->GetWidth(), + targetSurface->GetHeight()); + if (command.intersectWithCurrentClip && !clipStack.empty()) { + clipRect = IntersectRects(clipRect, clipStack.back()); + } + clipStack.push_back(clipRect); + break; + } + case UIDrawCommandType::PopClipRect: + if (!clipStack.empty()) { + clipStack.pop_back(); + } + break; + default: + break; + } + } + } + + commandList->TransitionBarrier( + renderTargets[0], + ::XCEngine::RHI::ResourceStates::RenderTarget, + ::XCEngine::RHI::ResourceStates::Present); + + if (!m_windowRenderer->SubmitFrame(false)) { + m_lastError = "Native D3D12 UI renderer failed to submit the frame: " + + m_windowRenderer->GetLastError(); + return false; + } + if (!m_windowRenderer->SignalFrameCompletion()) { + m_lastError = "Native D3D12 UI renderer failed to signal frame completion."; + return false; + } + if (!m_windowRenderer->PresentFrame()) { + m_lastError = "Native D3D12 UI renderer failed to present the swap chain."; + return false; + } + + m_lastError.clear(); + return true; +} + +D3D12UIRendererFrameAnalysis D3D12UIRenderer::BuildFrameAnalysis(const UIDrawData& drawData) { + D3D12UIRendererFrameAnalysis analysis = {}; + analysis.drawListCount = drawData.GetDrawListCount(); + analysis.commandCount = drawData.GetTotalCommandCount(); + analysis.empty = analysis.commandCount == 0u; + + for (const UIDrawList& drawList : drawData.GetDrawLists()) { + for (const UIDrawCommand& command : drawList.GetCommands()) { + switch (command.type) { + case UIDrawCommandType::FilledRect: + ++analysis.filledRectCommandCount; + break; + case UIDrawCommandType::RectOutline: + ++analysis.outlineCommandCount; + break; + case UIDrawCommandType::FilledRectLinearGradient: + ++analysis.gradientCommandCount; + analysis.requiresGradients = true; + break; + case UIDrawCommandType::Line: + ++analysis.lineCommandCount; + break; + case UIDrawCommandType::FilledTriangle: + case UIDrawCommandType::FilledCircle: + case UIDrawCommandType::CircleOutline: + ++analysis.geometryCommandCount; + break; + case UIDrawCommandType::Text: + ++analysis.textCommandCount; + analysis.requiresText = true; + break; + case UIDrawCommandType::Image: + ++analysis.imageCommandCount; + analysis.requiresImages = true; + break; + case UIDrawCommandType::PushClipRect: + case UIDrawCommandType::PopClipRect: + ++analysis.clipCommandCount; + analysis.requiresClipStack = true; + break; + default: + break; + } + } + } + + return analysis; +} + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12UIRenderer.h b/new_editor/app/Rendering/D3D12/D3D12UIRenderer.h new file mode 100644 index 00000000..72beda97 --- /dev/null +++ b/new_editor/app/Rendering/D3D12/D3D12UIRenderer.h @@ -0,0 +1,143 @@ +#pragma once + +#include "D3D12ShaderResourceDescriptorAllocator.h" +#include "D3D12WindowRenderer.h" + +#include "Ports/TextureDataPort.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::Rendering { +class RenderSurface; +} + +namespace XCEngine::RHI { +class D3D12Buffer; +class D3D12DescriptorHeap; +class RHIPipelineLayout; +class RHIPipelineState; +class RHITexture; +} + +namespace XCEngine::UI::Editor::Host { + +struct D3D12UIRendererFrameAnalysis { + std::size_t drawListCount = 0u; + std::size_t commandCount = 0u; + std::uint32_t filledRectCommandCount = 0u; + std::uint32_t outlineCommandCount = 0u; + std::uint32_t gradientCommandCount = 0u; + std::uint32_t lineCommandCount = 0u; + std::uint32_t geometryCommandCount = 0u; + std::uint32_t textCommandCount = 0u; + std::uint32_t imageCommandCount = 0u; + std::uint32_t clipCommandCount = 0u; + bool requiresText = false; + bool requiresImages = false; + bool requiresGradients = false; + bool requiresClipStack = false; + bool empty = true; +}; + +class D3D12UIRenderer { +public: + bool AttachWindowRenderer(D3D12WindowRenderer& windowRenderer); + void DetachWindowRenderer(); + void SetTextureDataSource(const Ports::TextureDataPort* textureDataSource); + + bool HasAttachedWindowRenderer() const; + void SetDpiScale(float dpiScale); + float GetDpiScale() const; + void AnalyzeFrame(const ::XCEngine::UI::UIDrawData& drawData); + bool CanRender(const ::XCEngine::UI::UIDrawData& drawData, std::string* outReason = nullptr); + bool Render(const ::XCEngine::UI::UIDrawData& drawData); + + const D3D12UIRendererFrameAnalysis& GetLastFrameAnalysis() const; + const std::string& GetLastError() const; + +private: + struct UploadedTextureCacheEntry { + ::XCEngine::RHI::RHITexture* texture = nullptr; + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; + const std::uint8_t* sourcePixels = nullptr; + std::uint32_t width = 0u; + std::uint32_t height = 0u; + }; + + struct CachedTextTextureEntry { + UploadedTextureCacheEntry uploaded = {}; + float widthDips = 0.0f; + float heightDips = 0.0f; + }; + + static D3D12UIRendererFrameAnalysis BuildFrameAnalysis( + const ::XCEngine::UI::UIDrawData& drawData); + void ReleaseResources(); + void ReleasePipelineResources(); + void ReleaseFrameConstantBuffers(); + void ReleaseUploadedTextureCaches(); + void ReleaseUploadedTexture(UploadedTextureCacheEntry& texture); + void ReleaseTextRasterizer(); + bool EnsureShapePassResources(const ::XCEngine::Rendering::RenderSurface& targetSurface); + bool EnsureImagePassResources(const ::XCEngine::Rendering::RenderSurface& targetSurface); + bool EnsureFrameConstantBufferCapacity( + std::uint32_t frameSlot, + std::size_t requiredDrawCount); + bool EnsureTextRasterizer(); + IDWriteTextFormat* GetTextFormat(float scaledFontSize); + bool ValidateSupportedFrame( + const ::XCEngine::UI::UIDrawData& drawData, + std::string* outReason); + bool RenderSupportedFrame(const ::XCEngine::UI::UIDrawData& drawData); + bool ResolveUploadedTextureBinding( + const ::XCEngine::UI::UITextureHandle& texture, + ID3D12DescriptorHeap** outDescriptorHeap, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle, + std::string* outReason); + bool ResolveTextTextureBinding( + const ::XCEngine::UI::UIDrawCommand& command, + ID3D12DescriptorHeap** outDescriptorHeap, + D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle, + ::XCEngine::UI::UIRect& outRect, + std::string* outReason); + + D3D12WindowRenderer* m_windowRenderer = nullptr; + const Ports::TextureDataPort* m_textureDataSource = nullptr; + D3D12UIRendererFrameAnalysis m_lastFrameAnalysis = {}; + std::string m_lastError = {}; + ::XCEngine::RHI::RHIPipelineLayout* m_shapePipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_shapePipelineState = nullptr; + ::XCEngine::RHI::RHIPipelineLayout* m_imagePipelineLayout = nullptr; + ::XCEngine::RHI::RHIPipelineState* m_imagePipelineState = nullptr; + std::vector<::XCEngine::RHI::D3D12Buffer*> m_frameConstantBuffers = {}; + std::vector m_frameConstantBufferCapacities = {}; + D3D12ShaderResourceDescriptorAllocator m_uploadedTextureAllocator = {}; + ::XCEngine::RHI::D3D12DescriptorHeap* m_imageSamplerHeap = nullptr; + D3D12_CPU_DESCRIPTOR_HANDLE m_imageSamplerCpuHandle = {}; + D3D12_GPU_DESCRIPTOR_HANDLE m_imageSamplerGpuHandle = {}; + std::unordered_map m_uploadedTextureCache = {}; + std::unordered_map m_cachedTextTextures = {}; + Microsoft::WRL::ComPtr m_textD2DFactory = {}; + Microsoft::WRL::ComPtr m_textDWriteFactory = {}; + Microsoft::WRL::ComPtr m_textWicFactory = {}; + std::unordered_map> m_textFormats = {}; + bool m_textComInitialized = false; + std::uint32_t m_cachedRenderTargetFormat = 0u; + std::uint32_t m_cachedSampleCount = 0u; + std::uint32_t m_cachedSampleQuality = 0u; + float m_dpiScale = 1.0f; +}; + +} // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp index 9ba1fb86..8ac43367 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowInteropContext.cpp @@ -1,4 +1,5 @@ #include "D3D12WindowInteropHelpers.h" +#include "Support/EnvironmentFlags.h" #include #include @@ -52,6 +53,17 @@ namespace XCEngine::UI::Editor::Host { using namespace D3D12WindowInteropHelpers; +#ifdef _DEBUG +namespace { + +bool ShouldEnableD3D11InteropDebug() { + return App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_DEBUG") || + App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_VALIDATION"); +} + +} // namespace +#endif + bool D3D12WindowInteropContext::Attach( D3D12WindowRenderer& windowRenderer, ID2D1Factory1& d2dFactory) { @@ -139,7 +151,9 @@ bool D3D12WindowInteropContext::EnsureInterop() { UINT createFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; #ifdef _DEBUG - createFlags |= D3D11_CREATE_DEVICE_DEBUG; + if (ShouldEnableD3D11InteropDebug()) { + createFlags |= D3D11_CREATE_DEVICE_DEBUG; + } #endif D3D_FEATURE_LEVEL actualFeatureLevel = D3D_FEATURE_LEVEL_11_0; @@ -155,7 +169,8 @@ bool D3D12WindowInteropContext::EnsureInterop() { m_d3d11DeviceContext.ReleaseAndGetAddressOf(), &actualFeatureLevel); #ifdef _DEBUG - if (FAILED(hr)) { + if (FAILED(hr) && + (createFlags & D3D11_CREATE_DEVICE_DEBUG) != 0u) { createFlags &= ~D3D11_CREATE_DEVICE_DEBUG; hr = D3D11On12CreateDevice( d3d12Device, diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp index 04e09832..576fe03e 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.cpp @@ -7,6 +7,9 @@ D3D12WindowRenderLoopAttachResult D3D12WindowRenderLoop::Attach( D3D12WindowRenderer& windowRenderer) { m_uiRenderer = &uiRenderer; m_windowRenderer = &windowRenderer; + m_nativeUiRenderer.AttachWindowRenderer(windowRenderer); + m_nativeUiRenderer.SetTextureDataSource(&uiRenderer); + m_nativeUiRenderer.SetDpiScale(uiRenderer.GetDpiScale()); D3D12WindowRenderLoopAttachResult result = {}; result.hasViewportSurfacePresentation = m_uiRenderer->AttachWindowRenderer(*m_windowRenderer); @@ -25,10 +28,16 @@ void D3D12WindowRenderLoop::Detach() { m_uiRenderer->DetachWindowRenderer(); } + m_nativeUiRenderer.SetTextureDataSource(nullptr); + m_nativeUiRenderer.DetachWindowRenderer(); m_uiRenderer = nullptr; m_windowRenderer = nullptr; } +void D3D12WindowRenderLoop::SetDpiScale(float dpiScale) { + m_nativeUiRenderer.SetDpiScale(dpiScale); +} + D3D12WindowRenderLoopFrameContext D3D12WindowRenderLoop::BeginFrame() const { D3D12WindowRenderLoopFrameContext context = {}; if (!HasViewportSurfacePresentation()) { @@ -104,14 +113,28 @@ D3D12WindowRenderLoopResizeResult D3D12WindowRenderLoop::ApplyResize(UINT width, } D3D12WindowRenderLoopPresentResult D3D12WindowRenderLoop::Present( - const ::XCEngine::UI::UIDrawData& drawData) const { + const ::XCEngine::UI::UIDrawData& drawData) { D3D12WindowRenderLoopPresentResult result = {}; if (m_uiRenderer == nullptr) { result.warning = "window render loop has no ui renderer."; return result; } + m_nativeUiRenderer.AnalyzeFrame(drawData); + if (HasViewportSurfacePresentation()) { + std::string nativeUnsupportedReason = {}; + if (m_nativeUiRenderer.CanRender(drawData, &nativeUnsupportedReason)) { + result.framePresented = m_nativeUiRenderer.Render(drawData); + if (!result.framePresented) { + const std::string& nativeError = m_nativeUiRenderer.GetLastError(); + result.warning = nativeError.empty() + ? "native d3d12 ui rendering failed." + : "native d3d12 ui rendering failed: " + nativeError; + } + return result; + } + result.framePresented = m_uiRenderer->RenderToWindowRenderer(drawData); if (!result.framePresented) { const std::string& composeError = m_uiRenderer->GetLastRenderError(); diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h index f8dd4655..4984a55a 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderLoop.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -37,15 +38,17 @@ public: NativeRenderer& uiRenderer, D3D12WindowRenderer& windowRenderer); void Detach(); + void SetDpiScale(float dpiScale); D3D12WindowRenderLoopFrameContext BeginFrame() const; D3D12WindowRenderLoopResizeResult ApplyResize(UINT width, UINT height); D3D12WindowRenderLoopPresentResult Present( - const ::XCEngine::UI::UIDrawData& drawData) const; + const ::XCEngine::UI::UIDrawData& drawData); bool HasViewportSurfacePresentation() const; private: + D3D12UIRenderer m_nativeUiRenderer = {}; NativeRenderer* m_uiRenderer = nullptr; D3D12WindowRenderer* m_windowRenderer = nullptr; }; diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp index e785bbf0..b4376a1f 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.cpp @@ -219,6 +219,14 @@ std::uint32_t D3D12WindowRenderer::GetBackBufferCount() const { return m_presenter.GetBackBufferCount(); } +std::uint32_t D3D12WindowRenderer::GetActiveFrameSlot() const { + return m_activeFrameSlot; +} + +ID3D12DescriptorHeap* D3D12WindowRenderer::GetViewportTextureDescriptorHeap() const { + return m_viewportTextureAllocator.GetDescriptorHeap(); +} + ::XCEngine::Rendering::RenderContext D3D12WindowRenderer::GetRenderContext() const { return m_hostDevice.GetRenderContext(m_activeFrameSlot); } diff --git a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h index 6fc7997a..a2d3c63f 100644 --- a/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h +++ b/new_editor/app/Rendering/D3D12/D3D12WindowRenderer.h @@ -53,6 +53,8 @@ public: const ::XCEngine::RHI::D3D12Texture* GetCurrentBackBufferTexture() const; const ::XCEngine::RHI::D3D12Texture* GetBackBufferTexture(std::uint32_t index) const; std::uint32_t GetBackBufferCount() const; + std::uint32_t GetActiveFrameSlot() const; + ID3D12DescriptorHeap* GetViewportTextureDescriptorHeap() const; ::XCEngine::Rendering::RenderContext GetRenderContext() const; private: diff --git a/new_editor/app/Rendering/Native/NativeRenderer.cpp b/new_editor/app/Rendering/Native/NativeRenderer.cpp index 620295ae..46ed48f6 100644 --- a/new_editor/app/Rendering/Native/NativeRenderer.cpp +++ b/new_editor/app/Rendering/Native/NativeRenderer.cpp @@ -1,4 +1,6 @@ #include "NativeRendererHelpers.h" +#include "Support/EnvironmentFlags.h" + #include #include #include @@ -9,6 +11,17 @@ namespace XCEngine::UI::Editor::Host { using namespace NativeRendererHelpers; +#ifdef _DEBUG +namespace { + +bool ShouldEnableD2DDebugLayer() { + return App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_DEBUG") || + App::IsEnvironmentFlagEnabled("XCUIEDITOR_ENABLE_GPU_VALIDATION"); +} + +} // namespace +#endif + bool NativeRenderer::Initialize(HWND hwnd) { Shutdown(); @@ -20,7 +33,9 @@ bool NativeRenderer::Initialize(HWND hwnd) { m_hwnd = hwnd; D2D1_FACTORY_OPTIONS factoryOptions = {}; #ifdef _DEBUG - factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; + if (ShouldEnableD2DDebugLayer()) { + factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; + } #endif HRESULT hr = D2D1CreateFactory( D2D1_FACTORY_TYPE_SINGLE_THREADED, @@ -507,9 +522,16 @@ void NativeRenderer::RenderRectOutlineCommand( ID2D1RenderTarget& renderTarget, ID2D1SolidColorBrush& solidBrush, const ::XCEngine::UI::UIDrawCommand& command) { + if (command.thickness <= 0.0f || + command.color.a <= 0.0f || + command.rect.width <= 0.0f || + command.rect.height <= 0.0f) { + return; + } + const float dpiScale = ClampDpiScale(m_dpiScale); const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale); - const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale; + const float thickness = command.thickness * dpiScale; const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f; if (command.rounding > 0.0f) { renderTarget.DrawRoundedRectangle( @@ -1071,6 +1093,30 @@ void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) { texture = {}; } +bool NativeRenderer::ResolveTexturePixelData( + const ::XCEngine::UI::UITextureHandle& texture, + Ports::TexturePixelDataView& outView) const { + outView = {}; + if (!texture.IsValid() || + texture.kind != ::XCEngine::UI::UITextureHandleKind::DescriptorHandle) { + return false; + } + + auto* resource = reinterpret_cast(texture.nativeHandle); + if (resource == nullptr || + m_liveTextures.find(resource) == m_liveTextures.end() || + resource->pixels.empty() || + resource->width == 0u || + resource->height == 0u) { + return false; + } + + outView.pixels = resource->pixels.data(); + outView.width = resource->width; + outView.height = resource->height; + return true; +} + bool NativeRenderer::ResolveTextureBitmap( ID2D1RenderTarget& renderTarget, NativeTextureResource& texture, diff --git a/new_editor/app/Rendering/Native/NativeRenderer.h b/new_editor/app/Rendering/Native/NativeRenderer.h index 623f439f..5e913cd5 100644 --- a/new_editor/app/Rendering/Native/NativeRenderer.h +++ b/new_editor/app/Rendering/Native/NativeRenderer.h @@ -4,6 +4,7 @@ #define NOMINMAX #endif +#include "Ports/TextureDataPort.h" #include "Ports/TexturePort.h" #include @@ -33,6 +34,7 @@ namespace XCEngine::UI::Editor::Host { class NativeRenderer : public Ports::TexturePort + , public Ports::TextureDataPort , public ::XCEngine::UI::Editor::UIEditorTextMeasurer { public: bool Initialize(HWND hwnd); @@ -64,6 +66,9 @@ public: ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) override; void ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) override; + bool ResolveTexturePixelData( + const ::XCEngine::UI::UITextureHandle& texture, + Ports::TexturePixelDataView& outView) const override; float MeasureTextWidth( const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const override; bool CaptureToPng( diff --git a/new_editor/include/XCEditor/Fields/UIEditorAssetField.h b/new_editor/include/XCEditor/Fields/UIEditorAssetField.h index cf88b10b..82bde529 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorAssetField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorAssetField.h @@ -40,6 +40,7 @@ struct UIEditorAssetFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float valueBoxMinWidth = 116.0f; float controlInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorBoolField.h b/new_editor/include/XCEditor/Fields/UIEditorBoolField.h index 390173ab..9daa1cd1 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorBoolField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorBoolField.h @@ -31,6 +31,7 @@ struct UIEditorBoolFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float checkboxSize = 18.0f; float labelTextInsetY = 0.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorColorField.h b/new_editor/include/XCEditor/Fields/UIEditorColorField.h index cd7ec6f6..a0c6b653 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorColorField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorColorField.h @@ -44,6 +44,7 @@ struct UIEditorColorFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float swatchWidth = 54.0f; float swatchInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorEnumField.h b/new_editor/include/XCEditor/Fields/UIEditorEnumField.h index e731818f..739a1ac7 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorEnumField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorEnumField.h @@ -36,6 +36,7 @@ struct UIEditorEnumFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float valueBoxMinWidth = 96.0f; float controlInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorNumberField.h b/new_editor/include/XCEditor/Fields/UIEditorNumberField.h index 814da810..e2104fc0 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorNumberField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorNumberField.h @@ -41,6 +41,7 @@ struct UIEditorNumberFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float valueBoxMinWidth = 96.0f; float controlInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorObjectField.h b/new_editor/include/XCEditor/Fields/UIEditorObjectField.h index b84d32b8..f0f60e85 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorObjectField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorObjectField.h @@ -38,6 +38,7 @@ struct UIEditorObjectFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float valueBoxMinWidth = 96.0f; float controlInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h index f78e3158..b15b5914 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h +++ b/new_editor/include/XCEditor/Fields/UIEditorPropertyGrid.h @@ -198,6 +198,7 @@ struct UIEditorPropertyGridMetrics { float horizontalPadding = 12.0f; float sectionHeaderHorizontalPadding = 6.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float labelControlGap = 20.0f; float disclosureExtent = 12.0f; float disclosureLabelGap = 8.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorTextField.h b/new_editor/include/XCEditor/Fields/UIEditorTextField.h index c4c2d4e9..bf5c9b71 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorTextField.h +++ b/new_editor/include/XCEditor/Fields/UIEditorTextField.h @@ -36,6 +36,7 @@ struct UIEditorTextFieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float valueBoxMinWidth = 96.0f; float controlInsetY = 1.0f; diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h index 2435fc77..63bffcf7 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector2Field.h @@ -47,6 +47,7 @@ struct UIEditorVector2FieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float controlInsetY = 1.0f; float componentGap = 6.0f; @@ -90,10 +91,6 @@ struct UIEditorVector2FieldPalette { ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor componentFocusedBorderColor = ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); - ::XCEngine::UI::UIColor prefixColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); - ::XCEngine::UI::UIColor prefixBorderColor = - ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor labelColor = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); ::XCEngine::UI::UIColor valueColor = diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h index aff0bafe..53dce5bd 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector3Field.h @@ -47,6 +47,7 @@ struct UIEditorVector3FieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float controlInsetY = 1.0f; float componentGap = 6.0f; @@ -90,10 +91,6 @@ struct UIEditorVector3FieldPalette { ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor componentFocusedBorderColor = ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); - ::XCEngine::UI::UIColor prefixColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); - ::XCEngine::UI::UIColor prefixBorderColor = - ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor labelColor = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); ::XCEngine::UI::UIColor valueColor = diff --git a/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h b/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h index 8961c587..4122af9c 100644 --- a/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h +++ b/new_editor/include/XCEditor/Fields/UIEditorVector4Field.h @@ -58,6 +58,7 @@ struct UIEditorVector4FieldMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float controlInsetY = 1.0f; float componentGap = 6.0f; @@ -101,10 +102,6 @@ struct UIEditorVector4FieldPalette { ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor componentFocusedBorderColor = ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); - ::XCEngine::UI::UIColor prefixColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); - ::XCEngine::UI::UIColor prefixBorderColor = - ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor labelColor = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); ::XCEngine::UI::UIColor valueColor = diff --git a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h index 68fb8efc..1f101214 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h +++ b/new_editor/include/XCEditor/Shell/UIEditorShellCompose.h @@ -23,12 +23,12 @@ struct UIEditorShellToolbarLayout { }; struct UIEditorShellToolbarMetrics { - float barHeight = 24.0f; + float barHeight = 30.0f; float groupPaddingX = 6.0f; float groupPaddingY = 2.0f; - float buttonWidth = 18.0f; - float buttonHeight = 16.0f; - float buttonGap = 4.0f; + float buttonWidth = 20.0f; + float buttonHeight = 20.0f; + float buttonGap = 6.0f; float groupCornerRounding = 0.0f; float buttonCornerRounding = 0.0f; float borderThickness = 1.0f; diff --git a/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h b/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h index aa74cdf3..29032178 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h @@ -27,10 +27,6 @@ struct UIEditorInspectorFieldStyleTokens { ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor controlFocusedBorderColor = ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); - ::XCEngine::UI::UIColor prefixColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); - ::XCEngine::UI::UIColor prefixBorderColor = - ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); ::XCEngine::UI::UIColor axisXColor = ::XCEngine::UI::UIColor(0.78f, 0.42f, 0.42f, 1.0f); ::XCEngine::UI::UIColor axisYColor = @@ -98,6 +94,7 @@ struct UIEditorFieldRowLayoutMetrics { float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; + float sharedControlColumnMinWidth = 0.0f; float controlTrailingInset = 8.0f; float controlInsetY = 1.0f; }; diff --git a/new_editor/src/Fields/UIEditorAssetField.cpp b/new_editor/src/Fields/UIEditorAssetField.cpp index ae817718..2c72cafd 100644 --- a/new_editor/src/Fields/UIEditorAssetField.cpp +++ b/new_editor/src/Fields/UIEditorAssetField.cpp @@ -130,6 +130,7 @@ UIEditorAssetFieldLayout BuildUIEditorAssetFieldLayout( metrics.horizontalPadding, metrics.labelControlGap, metrics.controlColumnStart, + metrics.sharedControlColumnMinWidth, metrics.controlTrailingInset, metrics.controlInsetY, }); diff --git a/new_editor/src/Fields/UIEditorBoolField.cpp b/new_editor/src/Fields/UIEditorBoolField.cpp index 3bb14d0b..f9870a08 100644 --- a/new_editor/src/Fields/UIEditorBoolField.cpp +++ b/new_editor/src/Fields/UIEditorBoolField.cpp @@ -46,6 +46,7 @@ UIEditorBoolFieldLayout BuildUIEditorBoolFieldLayout( metrics.horizontalPadding, metrics.labelControlGap, metrics.controlColumnStart, + metrics.sharedControlColumnMinWidth, metrics.controlTrailingInset, 0.0f, }); diff --git a/new_editor/src/Fields/UIEditorColorField.cpp b/new_editor/src/Fields/UIEditorColorField.cpp index e53b26d9..5e3e65ce 100644 --- a/new_editor/src/Fields/UIEditorColorField.cpp +++ b/new_editor/src/Fields/UIEditorColorField.cpp @@ -120,6 +120,7 @@ UIEditorColorFieldLayout BuildUIEditorColorFieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.swatchInsetY, }); diff --git a/new_editor/src/Fields/UIEditorEnumField.cpp b/new_editor/src/Fields/UIEditorEnumField.cpp index f63317c8..546abd2a 100644 --- a/new_editor/src/Fields/UIEditorEnumField.cpp +++ b/new_editor/src/Fields/UIEditorEnumField.cpp @@ -115,6 +115,7 @@ UIEditorEnumFieldLayout BuildUIEditorEnumFieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); diff --git a/new_editor/src/Fields/UIEditorFieldStyle.cpp b/new_editor/src/Fields/UIEditorFieldStyle.cpp index 64fb8f95..a1daa105 100644 --- a/new_editor/src/Fields/UIEditorFieldStyle.cpp +++ b/new_editor/src/Fields/UIEditorFieldStyle.cpp @@ -17,17 +17,18 @@ const Widgets::UIEditorPropertyGridMetrics& GetUIEditorFixedPropertyGridMetrics( ResolveUIEditorTreeViewMetrics(); Widgets::UIEditorPropertyGridMetrics metrics = {}; metrics.contentInset = 0.0f; - metrics.sectionGap = 4.0f; + metrics.sectionGap = 0.0f; metrics.sectionHeaderHeight = 24.0f; metrics.fieldRowHeight = 22.0f; metrics.rowGap = 3.0f; metrics.horizontalPadding = 12.0f; metrics.sectionHeaderHorizontalPadding = 6.0f; metrics.controlColumnStart = 236.0f; + metrics.sharedControlColumnMinWidth = 160.0f; metrics.labelControlGap = 20.0f; metrics.disclosureExtent = treeMetrics.disclosureExtent; metrics.disclosureLabelGap = treeMetrics.disclosureLabelGap; - metrics.sectionTextInsetY = 6.0f; + metrics.sectionTextInsetY = 0.0f; metrics.sectionFontSize = 12.0f; metrics.labelTextInsetY = 0.0f; metrics.labelFontSize = 11.0f; @@ -82,6 +83,7 @@ Widgets::UIEditorBoolFieldMetrics BuildUIEditorPropertyGridBoolFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; hosted.labelFontSize = propertyGridMetrics.labelFontSize; @@ -119,6 +121,7 @@ Widgets::UIEditorNumberFieldMetrics BuildUIEditorPropertyGridNumberFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -162,6 +165,7 @@ Widgets::UIEditorTextFieldMetrics BuildUIEditorPropertyGridTextFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -205,6 +209,7 @@ Widgets::UIEditorVector2FieldMetrics BuildUIEditorPropertyGridVector2FieldMetric hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -234,8 +239,6 @@ Widgets::UIEditorVector2FieldPalette BuildUIEditorPropertyGridVector2FieldPalett hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; hosted.componentBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.componentFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; - hosted.prefixColor = propertyGridPalette.valueBoxHoverColor; - hosted.prefixBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.labelColor = propertyGridPalette.labelTextColor; hosted.valueColor = propertyGridPalette.valueTextColor; hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; @@ -250,6 +253,7 @@ Widgets::UIEditorVector3FieldMetrics BuildUIEditorPropertyGridVector3FieldMetric hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -279,8 +283,6 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorPropertyGridVector3FieldPalett hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; hosted.componentBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.componentFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; - hosted.prefixColor = propertyGridPalette.valueBoxHoverColor; - hosted.prefixBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.labelColor = propertyGridPalette.labelTextColor; hosted.valueColor = propertyGridPalette.valueTextColor; hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; @@ -295,6 +297,7 @@ Widgets::UIEditorVector4FieldMetrics BuildUIEditorPropertyGridVector4FieldMetric hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -324,8 +327,6 @@ Widgets::UIEditorVector4FieldPalette BuildUIEditorPropertyGridVector4FieldPalett hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; hosted.componentBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.componentFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; - hosted.prefixColor = propertyGridPalette.valueBoxHoverColor; - hosted.prefixBorderColor = propertyGridPalette.valueBoxBorderColor; hosted.labelColor = propertyGridPalette.labelTextColor; hosted.valueColor = propertyGridPalette.valueTextColor; hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; @@ -340,6 +341,7 @@ Widgets::UIEditorEnumFieldMetrics BuildUIEditorPropertyGridEnumFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -384,6 +386,7 @@ Widgets::UIEditorColorFieldMetrics BuildUIEditorPropertyGridColorFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.swatchWidth = boolMetrics.checkboxSize; hosted.swatchInsetY = @@ -427,6 +430,7 @@ Widgets::UIEditorObjectFieldMetrics BuildUIEditorPropertyGridObjectFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; @@ -479,6 +483,7 @@ Widgets::UIEditorAssetFieldMetrics BuildUIEditorPropertyGridAssetFieldMetrics( hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; hosted.labelControlGap = propertyGridMetrics.labelControlGap; hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.sharedControlColumnMinWidth = propertyGridMetrics.sharedControlColumnMinWidth; hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; diff --git a/new_editor/src/Fields/UIEditorNumberField.cpp b/new_editor/src/Fields/UIEditorNumberField.cpp index 7485c7cc..6f06bc79 100644 --- a/new_editor/src/Fields/UIEditorNumberField.cpp +++ b/new_editor/src/Fields/UIEditorNumberField.cpp @@ -233,6 +233,7 @@ UIEditorNumberFieldLayout BuildUIEditorNumberFieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); diff --git a/new_editor/src/Fields/UIEditorObjectField.cpp b/new_editor/src/Fields/UIEditorObjectField.cpp index 8fd509d7..1e368f8b 100644 --- a/new_editor/src/Fields/UIEditorObjectField.cpp +++ b/new_editor/src/Fields/UIEditorObjectField.cpp @@ -119,6 +119,7 @@ UIEditorObjectFieldLayout BuildUIEditorObjectFieldLayout( metrics.horizontalPadding, metrics.labelControlGap, metrics.controlColumnStart, + metrics.sharedControlColumnMinWidth, metrics.controlTrailingInset, metrics.controlInsetY, }); diff --git a/new_editor/src/Fields/UIEditorPropertyGrid.cpp b/new_editor/src/Fields/UIEditorPropertyGrid.cpp index ce49881c..8b80a179 100644 --- a/new_editor/src/Fields/UIEditorPropertyGrid.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGrid.cpp @@ -864,11 +864,18 @@ void AppendUIEditorPropertyGridForeground( layout.sectionDisclosureRects[sectionVisibleIndex], layout.sectionExpanded[sectionVisibleIndex], palette.disclosureColor); - drawList.PushClipRect(layout.sectionTitleRects[sectionVisibleIndex], true); + drawList.PushClipRect( + ResolveUIEditorTextClipRect( + layout.sectionTitleRects[sectionVisibleIndex], + metrics.sectionFontSize), + true); drawList.AddText( ::XCEngine::UI::UIPoint( layout.sectionTitleRects[sectionVisibleIndex].x, - layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY), + ResolveUIEditorTextTop( + layout.sectionTitleRects[sectionVisibleIndex], + metrics.sectionFontSize, + metrics.sectionTextInsetY)), section.title, palette.sectionTextColor, metrics.sectionFontSize); diff --git a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp index f5b5a468..79dee0b3 100644 --- a/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp +++ b/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp @@ -1201,6 +1201,7 @@ void PruneStateEntries( template bool HasMeaningfulResult( const typename Traits::InteractionResult& result, + const typename Traits::FieldStateType& previousFieldState, const typename Traits::InteractionState& interactionState, bool hadFocus, bool hadEditing) { @@ -1211,6 +1212,9 @@ bool HasMeaningfulResult( result.editCommitted || Traits::HasEditCommitRejected(result) || result.editCanceled || + Traits::HasHoverStateChanged( + previousFieldState, + Traits::FieldState(interactionState)) || result.hitTarget.kind != Traits::kNoneHitTargetKind || hadFocus || hadEditing || @@ -1297,6 +1301,8 @@ bool ProcessFieldEventImpl( BuildInteractionState(state, field.fieldId); const bool hadFocus = Traits::FieldState(interactionState).focused; const bool hadEditing = Traits::FieldState(interactionState).editing; + const typename Traits::FieldStateType previousFieldState = + Traits::FieldState(interactionState); typename Traits::Spec spec = Traits::BuildSpec(field); const typename Traits::InteractionFrame frame = Traits::Update( @@ -1308,6 +1314,7 @@ bool ProcessFieldEventImpl( if (!HasMeaningfulResult( frame.result, + previousFieldState, interactionState, hadFocus, hadEditing)) { @@ -1415,6 +1422,7 @@ struct NumberTraits { using Metrics = Widgets::UIEditorNumberFieldMetrics; using Spec = Widgets::UIEditorNumberFieldSpec; using HitTargetKind = Widgets::UIEditorNumberFieldHitTargetKind; + using FieldStateType = Widgets::UIEditorNumberFieldState; static constexpr UIEditorPropertyGridFieldKind kFieldKind = UIEditorPropertyGridFieldKind::Number; @@ -1470,6 +1478,12 @@ struct NumberTraits { static bool HasEditCommitRejected(const InteractionResult& result) { return result.editCommitRejected; } + + static bool HasHoverStateChanged( + const FieldStateType& before, + const FieldStateType& after) { + return before.hoveredTarget != after.hoveredTarget; + } }; struct TextTraits { @@ -1480,6 +1494,7 @@ struct TextTraits { using Metrics = Widgets::UIEditorTextFieldMetrics; using Spec = Widgets::UIEditorTextFieldSpec; using HitTargetKind = Widgets::UIEditorTextFieldHitTargetKind; + using FieldStateType = Widgets::UIEditorTextFieldState; static constexpr UIEditorPropertyGridFieldKind kFieldKind = UIEditorPropertyGridFieldKind::Text; @@ -1535,6 +1550,12 @@ struct TextTraits { static bool HasEditCommitRejected(const InteractionResult&) { return false; } + + static bool HasHoverStateChanged( + const FieldStateType& before, + const FieldStateType& after) { + return before.hoveredTarget != after.hoveredTarget; + } }; template <> @@ -2461,6 +2482,7 @@ void PruneStateEntries( template bool HasMeaningfulResult( const typename Traits::InteractionResult& result, + const typename Traits::FieldStateType& previousFieldState, const typename Traits::InteractionState& interactionState, bool hadFocus, bool hadEditing) { @@ -2473,6 +2495,9 @@ bool HasMeaningfulResult( result.editCommitted || result.editCommitRejected || result.editCanceled || + Traits::HasHoverStateChanged( + previousFieldState, + Traits::FieldState(interactionState)) || result.hitTarget.kind != Traits::kNoneHitTargetKind || hadFocus || hadEditing || @@ -2533,6 +2558,8 @@ bool ProcessVectorFieldEventImpl( BuildInteractionState(state, field.fieldId); const bool hadFocus = Traits::FieldState(vectorState).focused; const bool hadEditing = Traits::FieldState(vectorState).editing; + const typename Traits::FieldStateType previousFieldState = + Traits::FieldState(vectorState); typename Traits::Spec spec = Traits::BuildSpec(field); const typename Traits::InteractionFrame frame = Traits::Update( @@ -2544,6 +2571,7 @@ bool ProcessVectorFieldEventImpl( if (!HasMeaningfulResult( frame.result, + previousFieldState, vectorState, hadFocus, hadEditing)) { @@ -2651,6 +2679,7 @@ struct Vector2Traits { using InteractionFrame = UIEditorVector2FieldInteractionFrame; using Metrics = Widgets::UIEditorVector2FieldMetrics; using Spec = Widgets::UIEditorVector2FieldSpec; + using FieldStateType = Widgets::UIEditorVector2FieldState; static constexpr UIEditorPropertyGridFieldKind kFieldKind = UIEditorPropertyGridFieldKind::Vector2; @@ -2698,6 +2727,13 @@ struct Vector2Traits { inputEvents, metrics); } + + static bool HasHoverStateChanged( + const FieldStateType& before, + const FieldStateType& after) { + return before.hoveredTarget != after.hoveredTarget || + before.hoveredComponentIndex != after.hoveredComponentIndex; + } }; struct Vector3Traits { @@ -2707,6 +2743,7 @@ struct Vector3Traits { using InteractionFrame = UIEditorVector3FieldInteractionFrame; using Metrics = Widgets::UIEditorVector3FieldMetrics; using Spec = Widgets::UIEditorVector3FieldSpec; + using FieldStateType = Widgets::UIEditorVector3FieldState; static constexpr UIEditorPropertyGridFieldKind kFieldKind = UIEditorPropertyGridFieldKind::Vector3; @@ -2754,6 +2791,13 @@ struct Vector3Traits { inputEvents, metrics); } + + static bool HasHoverStateChanged( + const FieldStateType& before, + const FieldStateType& after) { + return before.hoveredTarget != after.hoveredTarget || + before.hoveredComponentIndex != after.hoveredComponentIndex; + } }; struct Vector4Traits { @@ -2763,6 +2807,7 @@ struct Vector4Traits { using InteractionFrame = UIEditorVector4FieldInteractionFrame; using Metrics = Widgets::UIEditorVector4FieldMetrics; using Spec = Widgets::UIEditorVector4FieldSpec; + using FieldStateType = Widgets::UIEditorVector4FieldState; static constexpr UIEditorPropertyGridFieldKind kFieldKind = UIEditorPropertyGridFieldKind::Vector4; @@ -2810,6 +2855,13 @@ struct Vector4Traits { inputEvents, metrics); } + + static bool HasHoverStateChanged( + const FieldStateType& before, + const FieldStateType& after) { + return before.hoveredTarget != after.hoveredTarget || + before.hoveredComponentIndex != after.hoveredComponentIndex; + } }; template <> diff --git a/new_editor/src/Fields/UIEditorTextField.cpp b/new_editor/src/Fields/UIEditorTextField.cpp index 253fde4c..8dbeac48 100644 --- a/new_editor/src/Fields/UIEditorTextField.cpp +++ b/new_editor/src/Fields/UIEditorTextField.cpp @@ -117,6 +117,7 @@ UIEditorTextFieldLayout BuildUIEditorTextFieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); diff --git a/new_editor/src/Fields/UIEditorVector2Field.cpp b/new_editor/src/Fields/UIEditorVector2Field.cpp index 68fa8204..1971f696 100644 --- a/new_editor/src/Fields/UIEditorVector2Field.cpp +++ b/new_editor/src/Fields/UIEditorVector2Field.cpp @@ -68,14 +68,6 @@ UIEditorVector2FieldPalette ResolvePalette(const UIEditorVector2FieldPalette& pa ::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f))) { resolved.componentFocusedBorderColor = tokens.controlFocusedBorderColor; } - if (AreUIEditorFieldColorsEqual(palette.prefixColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixColor = tokens.prefixColor; - } - if (AreUIEditorFieldColorsEqual( - palette.prefixBorderColor, - ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixBorderColor = tokens.prefixBorderColor; - } if (AreUIEditorFieldColorsEqual(palette.labelColor, ::XCEngine::UI::UIColor(0.80f, 0.80f, 0.80f, 1.0f))) { resolved.labelColor = tokens.labelColor; } @@ -200,6 +192,7 @@ UIEditorVector2FieldLayout BuildUIEditorVector2FieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); @@ -272,11 +265,11 @@ void AppendUIEditorVector2FieldBackground( for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { drawList.AddFilledRect( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), resolvedMetrics.componentRounding); drawList.AddRectOutline( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentBorderColor(state, resolvedPalette, componentIndex), resolvedMetrics.borderThickness, resolvedMetrics.componentRounding); diff --git a/new_editor/src/Fields/UIEditorVector3Field.cpp b/new_editor/src/Fields/UIEditorVector3Field.cpp index a804cd5c..7044ea77 100644 --- a/new_editor/src/Fields/UIEditorVector3Field.cpp +++ b/new_editor/src/Fields/UIEditorVector3Field.cpp @@ -68,14 +68,6 @@ UIEditorVector3FieldPalette ResolvePalette(const UIEditorVector3FieldPalette& pa ::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f))) { resolved.componentFocusedBorderColor = tokens.controlFocusedBorderColor; } - if (AreUIEditorFieldColorsEqual(palette.prefixColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixColor = tokens.prefixColor; - } - if (AreUIEditorFieldColorsEqual( - palette.prefixBorderColor, - ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixBorderColor = tokens.prefixBorderColor; - } if (AreUIEditorFieldColorsEqual(palette.labelColor, ::XCEngine::UI::UIColor(0.80f, 0.80f, 0.80f, 1.0f))) { resolved.labelColor = tokens.labelColor; } @@ -203,6 +195,7 @@ UIEditorVector3FieldLayout BuildUIEditorVector3FieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); @@ -275,11 +268,11 @@ void AppendUIEditorVector3FieldBackground( for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { drawList.AddFilledRect( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), resolvedMetrics.componentRounding); drawList.AddRectOutline( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentBorderColor(state, resolvedPalette, componentIndex), resolvedMetrics.borderThickness, resolvedMetrics.componentRounding); diff --git a/new_editor/src/Fields/UIEditorVector4Field.cpp b/new_editor/src/Fields/UIEditorVector4Field.cpp index 21e0731d..54bd290e 100644 --- a/new_editor/src/Fields/UIEditorVector4Field.cpp +++ b/new_editor/src/Fields/UIEditorVector4Field.cpp @@ -68,14 +68,6 @@ UIEditorVector4FieldPalette ResolvePalette(const UIEditorVector4FieldPalette& pa ::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f))) { resolved.componentFocusedBorderColor = tokens.controlFocusedBorderColor; } - if (AreUIEditorFieldColorsEqual(palette.prefixColor, ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixColor = tokens.prefixColor; - } - if (AreUIEditorFieldColorsEqual( - palette.prefixBorderColor, - ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f))) { - resolved.prefixBorderColor = tokens.prefixBorderColor; - } if (AreUIEditorFieldColorsEqual(palette.labelColor, ::XCEngine::UI::UIColor(0.80f, 0.80f, 0.80f, 1.0f))) { resolved.labelColor = tokens.labelColor; } @@ -206,6 +198,7 @@ UIEditorVector4FieldLayout BuildUIEditorVector4FieldLayout( resolvedMetrics.horizontalPadding, resolvedMetrics.labelControlGap, resolvedMetrics.controlColumnStart, + resolvedMetrics.sharedControlColumnMinWidth, resolvedMetrics.controlTrailingInset, resolvedMetrics.controlInsetY, }); @@ -278,11 +271,11 @@ void AppendUIEditorVector4FieldBackground( for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { drawList.AddFilledRect( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentFillColor(spec, state, resolvedPalette, componentIndex), resolvedMetrics.componentRounding); drawList.AddRectOutline( - layout.componentValueRects[componentIndex], + layout.componentRects[componentIndex], ResolveComponentBorderColor(state, resolvedPalette, componentIndex), resolvedMetrics.borderThickness, resolvedMetrics.componentRounding); diff --git a/new_editor/src/Shell/UIEditorShellCompose.cpp b/new_editor/src/Shell/UIEditorShellCompose.cpp index d75ed579..ccf48dcd 100644 --- a/new_editor/src/Shell/UIEditorShellCompose.cpp +++ b/new_editor/src/Shell/UIEditorShellCompose.cpp @@ -119,16 +119,6 @@ void AppendUIEditorShellToolbar( return; } - drawList.AddFilledRect( - layout.groupRect, - palette.groupColor, - metrics.groupCornerRounding); - drawList.AddRectOutline( - layout.groupRect, - palette.groupBorderColor, - metrics.borderThickness, - metrics.groupCornerRounding); - const std::size_t buttonCount = (std::min)(buttons.size(), layout.buttonRects.size()); for (std::size_t index = 0; index < buttonCount; ++index) { const UIRect& buttonRect = layout.buttonRects[index]; diff --git a/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp b/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp index 03fc8afc..f335a7a5 100644 --- a/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp +++ b/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp @@ -31,8 +31,6 @@ const UIEditorInspectorFieldStyleTokens& GetUIEditorInspectorFieldStyleTokens() tokens.controlReadOnlyColor = ::XCEngine::UI::UIColor(0.10f, 0.10f, 0.10f, 1.0f); tokens.controlBorderColor = ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); tokens.controlFocusedBorderColor = ::XCEngine::UI::UIColor(0.19f, 0.19f, 0.19f, 1.0f); - tokens.prefixColor = ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); - tokens.prefixBorderColor = ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); tokens.popupColor = ::XCEngine::UI::UIColor(0.10f, 0.10f, 0.10f, 1.0f); tokens.popupBorderColor = ::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); tokens.popupHeaderColor = ::XCEngine::UI::UIColor(0.11f, 0.11f, 0.11f, 1.0f); @@ -74,26 +72,37 @@ UIEditorFieldRowLayout BuildUIEditorFieldRowLayout( const float rowHeight = bounds.height > 0.0f ? bounds.height : metrics.rowHeight; const ::XCEngine::UI::UIRect rowBounds(bounds.x, bounds.y, bounds.width, rowHeight); - const float resolvedMinimumControlWidth = - ClampNonNegative((std::min)(minimumControlWidth, rowBounds.width)); - const float preferredControlX = rowBounds.x + metrics.controlColumnStart; - const float maximumControlX = - rowBounds.x + rowBounds.width - metrics.controlTrailingInset - resolvedMinimumControlWidth; + const float horizontalPadding = ClampNonNegative(metrics.horizontalPadding); + const float trailingInset = ClampNonNegative(metrics.controlTrailingInset); + const float defaultGap = ClampNonNegative(metrics.labelControlGap); + const float contentLeft = rowBounds.x + horizontalPadding; + const float contentRight = + rowBounds.x + ClampNonNegative(rowBounds.width) - trailingInset; + const float contentWidth = ClampNonNegative(contentRight - contentLeft); + const float requestedReservedControlWidth = ClampNonNegative( + metrics.sharedControlColumnMinWidth > 0.0f + ? metrics.sharedControlColumnMinWidth + : minimumControlWidth); + const float reservedControlWidth = + ClampNonNegative((std::min)(requestedReservedControlWidth, contentWidth)); + const float effectiveGap = + (std::min)(defaultGap, ClampNonNegative(contentWidth - reservedControlWidth)); + const float preferredControlX = + (std::clamp)(rowBounds.x + metrics.controlColumnStart, contentLeft, contentRight); + const float maximumControlX = contentRight - reservedControlWidth; const float controlX = - (std::clamp)( - (std::min)(preferredControlX, maximumControlX), - rowBounds.x, - rowBounds.x + rowBounds.width - metrics.controlTrailingInset); + (std::clamp)((std::min)(preferredControlX, maximumControlX), contentLeft, contentRight); + const float controlInsetY = (std::min)(metrics.controlInsetY, rowBounds.height * 0.25f); const float controlWidth = - ClampNonNegative(rowBounds.x + rowBounds.width - metrics.controlTrailingInset - controlX); + ClampNonNegative(contentRight - controlX); UIEditorFieldRowLayout layout = {}; layout.bounds = rowBounds; layout.labelRect = ::XCEngine::UI::UIRect( - rowBounds.x + metrics.horizontalPadding, + contentLeft, rowBounds.y, - ClampNonNegative(controlX - metrics.labelControlGap - rowBounds.x - metrics.horizontalPadding), + ClampNonNegative(controlX - effectiveGap - contentLeft), rowBounds.height); layout.controlRect = ::XCEngine::UI::UIRect( controlX,