5.6 KiB
5.6 KiB
TreeView
命名空间: XCEngine::Editor::UI
类型: header-helper + class + struct
源文件: editor/src/UI/TreeView.h
描述: 提供编辑器树形视图的统一节点状态、前缀绘制槽、交互回调和展开状态持久化机制。
概述
TreeView.h 是当前 Editor UI 重构里最重要的公共基础设施之一。它把原本很容易散落在 HierarchyPanel、ProjectPanel 中的树节点绘制逻辑抽成一层统一接口,让不同面板能够共享:
- 相同的树节点视觉风格。
- 相同的展开 / 收起行为。
- 相同的前缀图标插槽模型。
- 相同的点击、双击、右键、拖拽扩展接入点。
如果从使用体验上理解,它很接近商业编辑器中“导航树控件”的内建基础层。HierarchyPanel 和 ProjectPanel 的左侧目录树并没有各写一套独立的 tree widget,而是都基于这层构建。
公开类型与函数
| 成员 | 说明 |
|---|---|
class TreeViewState |
保存节点展开状态的内存态容器。 |
TreeNodeOptions |
控制选中、叶子节点、默认展开、点击展开策略等基础选项。 |
TreeNodeResult |
返回节点本帧是否打开、是否点击、是否双击、是否右键等交互结果。 |
TreeNodePrefixContext |
给前缀绘制回调提供绘制区域、选中状态和 hover 状态。 |
TreeNodePrefixSlot |
定义一个可选的前缀绘制槽。 |
TreeNodeCallbacks |
定义交互回调和额外渲染回调。 |
TreeNodeDefinition |
组合节点选项、持久化 key、样式、前缀槽与回调。 |
ResetTreeLayout() |
清空当前树布局的缩进栈,开始一棵新的树。 |
DrawTreeNode(...) |
绘制节点并返回本帧结果。 |
EndTreeNode() |
与打开状态的节点配对,弹出一层树缩进。 |
TreeViewState 的角色
TreeViewState 当前只做一件事:根据 std::string_view key 保存节点展开状态。
它不是场景数据,也不是项目数据,只是某个 UI 实例自己的展开状态缓存。因此:
HierarchyPanel用它记住实体树哪些节点已经展开。ProjectPanel用它记住目录树哪些文件夹已经展开。
当前实现采用 std::unordered_map<std::string, bool>,所以最好的 persistenceKey 是稳定且能跨帧复现的标识,例如:
- 场景对象的 UUID
- 资源目录的完整路径
当前实现行为
按 editor/src/UI/TreeView.h 的实现,当前版本有几个关键事实:
- 树缩进不是保存在某个对象里,而是保存在一个静态
TreeIndentStack()中。 - 因此每次绘制一棵新的根树之前都应该先调用
ResetTreeLayout()。 DrawTreeNode(...)若返回open = true,调用方在绘制完所有子节点后必须调用EndTreeNode()与之配对。- 节点背景当前直接使用
ImGuiCol_Header/HeaderHovered/HeaderActive这一组颜色绘制选中与 hover 态。 - 展开箭头由
Core.h里的DrawDisclosureArrow(...)手工绘制。 - prefix slot 允许调用方在标签前插入小图标、状态灯或其他自定义绘制内容。
callbacks.onInteraction适合接选择、右键菜单、双击重命名等语义。callbacks.onRenderExtras适合接拖拽源 / 拖拽目标之类必须紧跟节点绘制的额外逻辑。
这说明它不是对 ImGui::TreeNodeEx(...) 的轻封装,而是自己接管了树节点的大部分视觉和交互行为。
设计说明
为什么要单独造这一层,而不是每个面板直接用 ImGui 原生 tree API?
Hierarchy和Project都需要统一的左侧导航树风格,这种复用非常适合抽成共享 helper。- 商业编辑器中的树视图往往不仅是“文本 + 箭头”,还需要图标前缀、整行高亮、拖放热点、重命名切换等语义。
- 一旦把节点定义、展开状态和回调插槽标准化,上层面板代码就能更聚焦于业务对象,而不是 UI 细节。
如果和 Unity 对照,可以把它理解成“Hierarchy / Project 导航树的共用控件层”,而不是某个特定面板的局部技巧。
典型用法
UI::ResetTreeLayout();
UI::TreeNodeDefinition node;
node.options.selected = isSelected;
node.options.leaf = !hasChildren;
node.persistenceKey = stablePath;
node.style = UI::ProjectFolderTreeStyle();
node.prefix.width = UI::NavigationTreePrefixWidth();
node.prefix.draw = DrawFolderPrefix;
node.callbacks.onInteraction = [&](const UI::TreeNodeResult& result) {
if (result.clicked) {
NavigateToFolder();
}
};
const UI::TreeNodeResult result =
UI::DrawTreeNode(&m_treeState, folder.get(), folder->name.c_str(), node);
if (result.open) {
DrawChildren();
UI::EndTreeNode();
}
生命周期与线程语义
TreeViewState由具体面板持有,通常作为成员变量存在于面板实例生命周期内。- 整套 API 都依赖当前 ImGui 帧状态,只能在 UI 线程使用。
- 它不负责把展开状态写入磁盘或布局文件;当前只是内存级持久化。
当前限制
- 静态
TreeIndentStack()让它更像“约定式 helper”,而不是完全可重入的小部件对象。 - 没有键盘导航、滚动到可见、虚拟化大树等更高级的树控件能力。
- 展开状态目前只保存在面板实例中,重启编辑器后不会自动恢复。
- 当前回调模型偏即时模式,适合编辑器内部使用,但不是声明式 UI 框架。