docs: sync project browser docs

This commit is contained in:
2026-04-04 01:02:57 +08:00
parent 42e2e1b8f2
commit 2abe0bbbd4
30 changed files with 1725 additions and 116 deletions

View File

@@ -6,121 +6,263 @@
**源文件**: `editor/src/panels/ProjectPanel.h`
**描述**: 编辑器项目资源浏览面板,负责目录树、面包屑、搜索框、资源卡片和拖放交互的前端呈现
**描述**: 项目资源浏览面板,负责`IProjectManager` 当前目录模型渲染成“目录树 + breadcrumb + 搜索 + 资源网格 + 上下文菜单 + 拖放”的 Project Browser
## 概述
`ProjectPanel` 当前 Editor 中最接近“资源浏览器”的面板。它本身不维护项目资源数据库,而是把 `IProjectManager` 暴露的数据模型转成一个典型的双栏浏览界面:
`ProjectPanel` 当前 Editor 里项目资源浏览的前端外壳,但它不是资产数据库本身。
- 左侧是目录树导航。
- 右侧是当前目录的资源浏览区。
- 顶部是搜索框与面包屑。
它的职责边界比较明确:
如果和 Unity 对照,可以把它理解成一个更轻量、当前聚焦于基础浏览和拖放的 Project Browser。
- 项目目录模型与选择状态来自 [IProjectManager](../../Core/IProjectManager/IProjectManager.md)
- 资源打开、创建、删除、重命名、移动等语义落到 [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- 少量通用交互协议由 [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md) 提供
- 面板自身主要负责布局、即时模式 UI 状态和延迟动作收口
## 当前实现行为
## 生命周期与公开入口
`editor/src/panels/ProjectPanel.cpp` 的实现:
- [Constructor](Constructor.md)
创建标题固定为 `"Project"` 的面板。
- [Initialize](Initialize.md)
把项目路径初始化工作委托给当前 `IProjectManager`
- [Render](Render.md)
执行完整的项目浏览器绘制与交互驱动。
- `Initialize(projectPath)` 直接委托给 `m_context->GetProjectManager().Initialize(projectPath)`
- `Render()` 会:
- 建立标准 `PanelWindowScope`
- 把焦点动作路由标记为 `EditorActionRoute::Project`
- 先渲染 toolbar。
- 再创建左右分栏内容区。
- 使用 `UI::DrawSplitter(...)` 支持调整左侧导航栏宽度。
- 收尾时绘制“新建文件夹”对话框。
## 面板内部状态
当前导航栏宽度会被显式夹紧在一个合理范围内
`ProjectPanel` 当前长期持有的主要是 UI 状态,而不是项目数据本体
- 最小值来自 `ProjectNavigationMinWidth()`
- 右侧浏览区最小宽度来自 `ProjectBrowserMinWidth()`
| 状态 | 作用 |
|------|------|
| `m_searchBuffer` | 当前目录搜索关键字 |
| `m_navigationWidth` | 左侧目录树宽度 |
| `m_folderTreeState` | 目录树展开状态 |
| `m_renameState` | 行内重命名状态 |
| `m_assetDragDropState` | 本帧拖放源 / 目标状态 |
| `m_deferredContextAction` | 延迟到本帧末尾执行的上下文菜单动作 |
这说明当前实现已经从一开始就按“可调整工作区”的编辑器思路组织,而不是固定死尺寸。
## 初始化与每帧主流程
### [Initialize(projectPath)](Initialize.md)
当前实现只有一行:
```cpp
m_context->GetProjectManager().Initialize(projectPath);
```
这说明 `ProjectPanel` 不自己扫描目录,也不自己构建项目树。
### [Render()](Render.md)
当前每帧主要顺序是:
1. 打开 `PanelWindowScope`
2. 调用 `ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project)`
3. `BeginAssetDragDropFrame()`
4. 渲染顶部 toolbar
5. 渲染左侧目录树与右侧浏览区
6. `FinalizeAssetDragDrop(manager)`
7. 在帧末执行 `m_deferredContextAction`
这里最关键的两个点是:
- Project 面板会显式声明当前焦点 route
- 拖放提交和上下文菜单命令都在本帧尾部统一收口,避免打断当前 UI 遍历
## 顶部工具栏
`RenderToolbar()` 当前负责两件事:
- 左侧显示当前 `ResourceManager` 的 project asset import 状态
- 右侧提供当前目录搜索框
导入状态文本和 tooltip 来自:
- `ResourceManager::GetProjectAssetImportStatus()`
- `AssetImportService::ImportStatusSnapshot`
搜索行为则比较克制:
- 只过滤 `manager.GetCurrentItems()` 返回的当前目录条目
- 匹配逻辑使用 `UI::SearchQuery::Matches(item->name)`
- 不做全项目搜索、类型过滤或标签过滤
## 左侧目录树
左侧目录树是当前 `ProjectPanel` 最典型的共享 UI 基建使用者之一:
### 数据来源
- 根节点来自 `IProjectManager::GetRootFolder()`
- 当前目录来自 `IProjectManager::GetCurrentFolder()`
- 每个目录节点都通过 `UI::DrawTreeNode(...)` 绘制。
- 节点样式使用 `UI::ProjectFolderTreeStyle()`
- 节点前缀使用 `UI::DrawAssetIcon(..., AssetIconKind::Folder)`
- `defaultOpen` 会根据 `IsCurrentTreeBranch(...)` 判断当前目录是否位于该目录分支下。
- 展开状态保存在 `m_folderTreeState` 中。
目录树主要消费:
用户单击树节点时,会立即调用 `manager.NavigateToFolder(folder)` 完成导航;右键则通过 `Actions::HandleProjectItemContextRequest(...)` 打开对应上下文菜单。
- `GetRootFolder()`
- `GetCurrentFolder()`
### 节点绘制与交互
每个目录节点当前通过 `UI::DrawTreeNode(...)` 绘制,并带有:
- 当前目录高亮
- 基于 `IsCurrentTreeBranch(...)` 的默认展开
- 自定义 folder icon prefix
当前支持的主要交互包括:
- 左键点击目录 -> `NavigateToFolder(folder)`
- 目录作为资源拖放目标
- 空白区右键弹出项目上下文菜单
若根目录不可用,则显示 `No Assets Folder` 空状态。
## 右侧浏览区
右侧浏览区分成两
右侧浏览区分成两部分
- Header显示面包屑导航并在底部画一条 divider。
- Body以资源网格形式显示当前目录下通过搜索过滤后的条目。
- `RenderBrowserHeader(manager)`
- `RenderBrowserPane(manager)` 的资源网格主体
资源卡片当前通过 `UI::DrawAssetTile(...)` 绘制,并根据 `item->isFolder` 区分:
### Breadcrumb
- `Folder` 图标
- `File` 图标
Header 当前通过 `UI::DrawToolbarBreadcrumbs(...)` 渲染路径,并消费:
交互结果统一收集到 `AssetItemInteraction` 中,再在循环结束后统一处理。这种写法有一个很实用的好处:可以避免在绘制过程中直接修改当前迭代容器或导航状态,让即时模式 UI 代码更稳定。
- `manager.GetPathDepth()`
- `manager.GetPathName(index)`
- `manager.NavigateToIndex(index)`
## 搜索与导航
因此目录导航当前主要依赖目录树和 breadcrumb而不是单独的“后退按钮”。
当前搜索实现比较明确,也需要在文档里写清楚:
### 资源网格
- 搜索框位于 toolbar 右侧。
- 搜索只对 `manager.GetCurrentItems()` 返回的当前目录条目生效。
- 匹配逻辑是大小写不敏感的子串匹配。
- 当前没有全文索引、标签过滤或类型过滤。
主体区域当前会:
面包屑则通过 `UI::DrawToolbarBreadcrumbs(...)` 实现,点击非当前段会调用 `manager.NavigateToIndex(index)`
1. 先基于搜索关键字计算 `visibleItems`
2. 动态估算卡片高度与列数
3. 逐项调用 `RenderAssetItem(...)`
4. 在帧末统一处理选中、打开与空白区清选
## 拖放与上下文菜单
`RenderAssetItem(...)` 当前支持:
当前 `ProjectPanel` 并不自己实现拖放协议和命令执行,而是按职责分层调用其他模块:
- 单击选中
- 双击打开
- 右键选中并弹出上下文菜单
- 纹理预览与图标 fallback
- 行内重命名
- 拖放源 / 目录拖放目标
- `Actions::BeginProjectAssetDrag(item, iconKind)` 负责发起拖拽源。
- `Actions::AcceptProjectAssetDropPayload(item)` 负责检测是否有资源被拖到当前目标上。
- 若确实发生移动,则调用 `Commands::MoveAssetToFolder(...)`
- 右键菜单与空白区域菜单由 `Actions::*ContextPopup(...)` 系列 helper 负责绘制。
若搜索结果为空,会显示:
这种组织方式和 `HierarchyPanel` 一样,遵循的是“面板负责展示与采样,动作层 / 命令层负责实际编辑行为”的编辑器架构。
- `No Search Results`
- `No assets match the current search`
## 生命周期与线程语义
## 上下文菜单与重命名
- 面板本身持有的是 UI 状态:搜索文本、导航栏宽度、目录树展开状态、弹窗状态。
- 真实项目数据由 `IProjectManager` 持有。
- 整套逻辑应视为编辑器 UI 主线程代码。
### 行内重命名
## 设计说明
当前重命名由 `m_renameState` 驱动:
当前 `ProjectPanel` 的设计方向是合理的,因为它先把“资源浏览器”拆成几个稳定的基础体验单元:
1. `BeginRename(item)` 激活编辑状态
2. `DrawInlineRenameFieldAt(...)` 覆盖卡片 label 区
3. 提交时调用 `CommitRename(...)`
4. 真正的重命名落到 `Commands::RenameAsset(...)`
- 分栏布局
- 目录树
- 面包屑
- 资源网格
- 拖放与上下文菜单
若用户没有实际修改显示名称,`CommitRename(...)` 会直接结束重命名,而不会重复提交文件操作。
这样做的好处是,后续无论是加缩略图、资源导入器状态、筛选器还是收藏夹,都可以在现有骨架上演进,而不是推倒重来。
### 右键菜单
`DrawProjectContextMenu(...)` 当前支持:
- `Create -> Folder`
- `Create -> Material`
- `Show in Explore`
- `Open`
- `Delete`
- `Rename`
- `Copy Path`
其中创建资源采用 deferred action
1. 必要时先导航到目标目录
2. 调用 `Commands::CreateFolder(...)``Commands::CreateMaterial(...)`
3. 新建成功后自动进入重命名状态
## 拖放与移动资源
当前拖放逻辑被收口成一套帧内状态机,而不是散落在每个 tile 里。
### 帧开始
`BeginAssetDragDropFrame()` 会:
- 清空 `m_assetDragDropState`
- 通过 `Actions::GetDraggedProjectAssetPath()` 识别当前拖拽源
### 目录作为 drop target
`RegisterFolderDropTarget(...)` 会:
- 只接受目录目标
- 校验 payload 类型为 `ProjectAssetPayloadType()`
-`Commands::CanMoveAssetToFolder(...)` 判断是否合法
- 在真正投递时记录源路径与目标目录
### 帧结束统一提交
`FinalizeAssetDragDrop(...)` 会:
- 根据当前悬停是否合法切换鼠标样式
- 若本帧成功投递,则统一调用 `Commands::MoveAssetToFolder(...)`
这样可以避免在 UI 遍历过程中直接修改底层目录树。
## 与项目工作流命令的关系
`ProjectPanel` 当前直接消费的主要是资源级命令:
- `CreateFolder`
- `CreateMaterial`
- `DeleteAsset`
- `RenameAsset`
- `MoveAssetToFolder`
- `OpenAsset`
它**不**负责发起以下项目级入口:
- `Save Project`
- `Open Project...`
- `New Project...`
- `Rebuild Script Assemblies`
这些工作流分别由主菜单和命令层收口。
当外部命令触发 `RefreshCurrentFolder()` 时,面板会在下一帧反映新的目录树状态。
## 测试锚点
`tests/editor/test_action_routing.cpp` 当前与这条资源链路直接相关的测试包括:
- `ProjectCommandsCreateFolderMoveAssetAndOpenFolderHelper`
- `ProjectCommandsCreateFolderUsesUniqueDefaultName`
- `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension`
- `ProjectCommandsRejectInvalidMoveTargets`
- `ProjectSelectionSurvivesRefreshWhenItemOrderChanges`
- `ProjectCommandsRejectMovingFolderIntoItsDescendant`
## 当前限制
- 搜索只在当前目录里做前端子串过滤,不是全项目索引搜索
- 当前没有资源缩略图生成或异步预览系统,图标仍以 `BuiltInIcons` 和简单卡片为主
- 文件树展开状态只保存在内存中,不会自动跨重启恢复
- 拖放移动是直接命令式处理,没有更复杂的批处理或事务预览。
- 搜索当前只覆盖当前目录的名称匹配
- 目录树展开状态当前只保存在内存里
- 拖拽预览 tooltip 被关闭,拖放反馈比较轻量
- 资源上下文菜单仍偏基础,没有批量操作或复杂预览。
- 面板本身不是项目保存或脚本重建入口。
## 相关文档
- [Constructor](Constructor.md)
- [Initialize](Initialize.md)
- [Render](Render.md)
- [panels](../panels.md)
- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [IProjectManager](../../Core/IProjectManager/IProjectManager.md)
- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md)
- [AssetItem](../../Core/AssetItem/AssetItem.md)
- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [TreeView](../../UI/TreeView/TreeView.md)
- [BuiltInIcons](../../UI/BuiltInIcons/BuiltInIcons.md)
- [SplitterChrome](../../UI/SplitterChrome/SplitterChrome.md)
- [Widgets](../../UI/Widgets/Widgets.md)