// // AssetBrowserPanel.cpp:项目资产浏览器实现。 // // 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一 // 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区 // 铺排 / 选中态 / 双击分派(打开或实例化)。 // #include "AssetBrowserPanel.h" #include "AssetItemView.h" // 列表行 / 网格瓦片 / 目录条目 #include "AssetKind.h" // EditorAsset::AssetKind / KindOfPath #include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器 #include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器 #include #include #include #include #include #include #include #include namespace { // 固定根:下标与 Editor 无耦合,仅面板内部使用 constexpr const char* kRootNames[] = {"Assets", "Scenes"}; constexpr const char* kRootPaths[] = {"assets", "scenes"}; // 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序 constexpr const char* kTypeNames[] = {"All", "Models", "Textures", "Scenes", "Materials", "Controllers", "Prefabs"}; bool PassFilter(int typeFilter, EditorAsset::AssetKind kind) { switch (typeFilter) { case 1: return kind == EditorAsset::AssetKind::Model; case 2: return kind == EditorAsset::AssetKind::Texture; case 3: return kind == EditorAsset::AssetKind::Scene; case 4: return kind == EditorAsset::AssetKind::Material; case 5: return kind == EditorAsset::AssetKind::AnimClip; case 6: return kind == EditorAsset::AssetKind::Prefab; default: return true; } } // 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格) std::string ToAssetPath(const std::filesystem::path& p) { std::string s = p.generic_string(); std::replace(s.begin(), s.end(), '\\', '/'); return s; } // 双击分派:材质/控制器 → 打开编辑器;模型/环境 → 缓存预热; // 场景 → 提示走 File 菜单;预制体 → 快捷实例化(授权位姿落点) void DispatchOpen(const EditorContext& ctx, const std::filesystem::path& path, EditorAsset::AssetKind kind) { const std::string assetPath = path.generic_string(); if (kind == EditorAsset::AssetKind::Material && ctx.materialEditor) { ctx.materialEditor->OpenAsset(assetPath); } else if (kind == EditorAsset::AssetKind::AnimClip && ctx.controllerEditor) { ctx.controllerEditor->Open(assetPath); } else if (kind == EditorAsset::AssetKind::Model && ctx.assets) { const auto ref = ctx.assets->LoadModel(assetPath); ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})", assetPath, ref.Valid()); } else if (kind == EditorAsset::AssetKind::Texture && ctx.assets) { const auto ref = ctx.assets->LoadEnvironment(assetPath); ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})", assetPath, ref.Valid()); } else if (kind == EditorAsset::AssetKind::Scene) { ENGINE_INFO("AssetBrowser: load scenes via File menu ({})", assetPath); } else if (kind == EditorAsset::AssetKind::Prefab && ctx.scene && ctx.assets) { // 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner 手动改); // 拖拽实例化见 Outliner 落点 const auto root = Engine::Serialization::SceneSerializer:: InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath); ENGINE_INFO("AssetBrowser: prefab '{}' instantiated (valid={})", assetPath, root != entt::null); } } } // namespace void AssetBrowserPanel::Draw(const EditorContext& ctx) { if (!ImGui::Begin("Content Browser")) { ImGui::End(); return; } // ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ---- if (ImGui::BeginTabBar("##roots")) { for (int i = 0; i < 2; ++i) { if (ImGui::BeginTabItem(kRootNames[i])) { m_Root = i; ImGui::EndTabItem(); } } ImGui::EndTabBar(); } ImGui::SameLine(); ImGui::SetNextItemWidth(110.0f); ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7); ImGui::SameLine(); ImGui::SetNextItemWidth(140.0f); ImGui::InputTextWithHint("##name", "filter", m_NameFilter, sizeof(m_NameFilter)); ImGui::Separator(); const std::filesystem::path root(kRootPaths[m_Root]); // 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with // 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验 // 防 "assets_x" 之类误匹配 const std::string rootStr = root.generic_string(); const std::string curStr = m_CurrentDir.generic_string(); const bool under = curStr == rootStr || (curStr.size() > rootStr.size() && curStr.compare(0, rootStr.size(), rootStr) == 0 && curStr[rootStr.size()] == '/'); if (m_CurrentDir.empty() || !under) { m_CurrentDir = root; } // ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ---- if (ImGui::Button("Root")) { m_CurrentDir = root; } ImGui::SameLine(); std::string rel = std::filesystem::relative(m_CurrentDir, root) .generic_string(); ImGui::TextDisabled("%s/%s", kRootNames[m_Root], rel.empty() ? "" : rel.c_str()); // 视图切换(列表 / 网格)+ 网格瓦片尺寸 ImGui::SameLine(); const bool listMode = m_ViewMode == ViewMode::List; if (ImGui::RadioButton("List", listMode)) { m_ViewMode = ViewMode::List; } ImGui::SameLine(); if (ImGui::RadioButton("Grid", !listMode)) { m_ViewMode = ViewMode::Grid; } if (m_ViewMode == ViewMode::Grid) { ImGui::SameLine(); ImGui::SetNextItemWidth(120.0f); ImGui::SliderFloat("##tile", &m_TileSize, 48.0f, 160.0f, "%.0f px"); } ImGui::Separator(); // ---- 左:目录树 | 右:当前目录内容(列表 / 网格)---- const float treeWidth = 180.0f; ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f), ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders); if (std::filesystem::exists(root)) { // 根节点也画进树(可点击选中) ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNodeEx(kRootNames[m_Root], ImGuiTreeNodeFlags_OpenOnArrow)) { if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { m_CurrentDir = root; } DrawDirTree(ctx, root); ImGui::TreePop(); } } else { ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]); } ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders); if (std::filesystem::exists(m_CurrentDir)) { DrawContent(ctx, m_CurrentDir); } ImGui::EndChild(); ImGui::End(); } void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx, const std::filesystem::path& dir) { for (const auto& entry : std::filesystem::directory_iterator(dir)) { if (!entry.is_directory()) { continue; } const std::string name = entry.path().filename().string(); // OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠 const bool open = ImGui::TreeNodeEx( name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { m_CurrentDir = entry.path(); } if (open) { DrawDirTree(ctx, entry.path()); ImGui::TreePop(); } } } void AssetBrowserPanel::DrawContent(const EditorContext& ctx, const std::filesystem::path& dir) { namespace fs = std::filesystem; // 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序 std::vector dirs; std::vector> files; const std::string filter = m_NameFilter; std::error_code ec; for (const auto& entry : fs::directory_iterator(dir, ec)) { if (entry.is_directory()) { dirs.push_back(entry.path()); continue; } const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(entry.path()); if (!PassFilter(m_TypeFilter, kind)) { continue; } if (!filter.empty()) { const std::string name = entry.path().filename().string(); if (name.find(filter) == std::string::npos) { continue; } } files.emplace_back(entry.path(), kind); } std::sort(dirs.begin(), dirs.end(), [](const fs::path& a, const fs::path& b) { return a.filename().string() < b.filename().string(); }); std::sort(files.begin(), files.end(), [](const auto& a, const auto& b) { return a.first.filename().string() < b.first.filename().string(); }); // 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态 //(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱) bool pendingOpen = false; std::filesystem::path pendingOpenPath; EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other; if (m_ViewMode == ViewMode::List) { for (const auto& d : dirs) { if (EditorAssetView::DrawFolderRow(d)) { m_CurrentDir = d; m_SelectedPath.clear(); } } for (const auto& [path, kind] : files) { const std::string assetPath = ToAssetPath(path); const auto res = EditorAssetView::DrawRow( ctx, path, assetPath == m_SelectedPath); if (res.clicked) { m_SelectedPath = assetPath; } if (res.doubleClicked) { pendingOpen = true; pendingOpenPath = path; pendingOpenKind = kind; } } } else { // ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)---- const float tile = m_TileSize; const float cellW = tile + ImGui::GetStyle().CellPadding.x * 2.0f; int cols = static_cast(ImGui::GetContentRegionAvail().x / cellW); if (cols < 1) { cols = 1; } if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit)) { for (int i = 0; i < cols; ++i) { ImGui::TableSetupColumn("##c", ImGuiTableColumnFlags_WidthFixed, cellW); } for (const auto& d : dirs) { ImGui::TableNextColumn(); const std::string assetPath = ToAssetPath(d); if (EditorAssetView::DrawFolderTile( d, tile, assetPath == m_SelectedPath)) { m_CurrentDir = d; m_SelectedPath.clear(); } } for (const auto& [path, kind] : files) { ImGui::TableNextColumn(); const std::string assetPath = ToAssetPath(path); const auto res = EditorAssetView::DrawTile( ctx, path, tile, assetPath == m_SelectedPath); if (res.clicked) { m_SelectedPath = assetPath; } if (res.doubleClicked) { pendingOpen = true; pendingOpenPath = path; pendingOpenKind = kind; } } ImGui::EndTable(); } } if (pendingOpen) { DispatchOpen(ctx, pendingOpenPath, pendingOpenKind); } }