Files
HelloVulkan/App/editor/AssetBrowserPanel.cpp
T
atdunbg fd2d8f9b34 fix(editor): asset browser directory navigation
The file list was hardwired to the root directory, so it was always
empty (all assets live in subdirectories). Directories are now
selectable from both the tree (click row) and the list (click [D]
row), with a breadcrumb + Root button. Prefix check done manually
(path::starts_with is missing in this libstdc++).
2026-10-04 00:55:19 +08:00

294 lines
9.2 KiB
C++

//
// AssetBrowserPanel.cpp:项目资产浏览器实现。
//
#include "AssetBrowserPanel.h"
#include <Engine/Core/Log.h>
#include <imgui.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <vector>
namespace {
// 固定根:下标与 Editor 无耦合,仅面板内部使用
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
constexpr const char* kRootPaths[] = {"assets", "scenes"};
// 类型过滤(下标即身份):0 = All
constexpr const char* kTypeNames[] = {"All", "Models", "Textures",
"Scenes"};
// 资产类别:按扩展名分派(过滤用)
enum class Kind { Model, Texture, Scene, Other };
Kind KindOf(const std::filesystem::path& p)
{
std::string ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
ext == ".obj")
{
return Kind::Model;
}
if (ext == ".hdr" || ext == ".png" || ext == ".jpg" ||
ext == ".jpeg")
{
return Kind::Texture;
}
if (ext == ".scene")
{
return Kind::Scene;
}
return Kind::Other;
}
bool PassFilter(int typeFilter, Kind kind)
{
switch (typeFilter)
{
case 1: return kind == Kind::Model;
case 2: return kind == Kind::Texture;
case 3: return kind == Kind::Scene;
default: return true;
}
}
// 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签)
const char* KindTag(Kind kind)
{
switch (kind)
{
case Kind::Model: return "[M]";
case Kind::Texture: return "[T]";
case Kind::Scene: return "[S]";
default: return "[ ]";
}
}
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
std::string ToAssetPath(const std::filesystem::path& p)
{
std::string s = p.generic_string();
std::replace(s.begin(), s.end(), '\\', '/');
return s;
}
} // namespace
void AssetBrowserPanel::Draw(const EditorContext& ctx)
{
if (!ImGui::Begin("Asset 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, 4);
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::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))
{
DrawFileList(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::DrawFileList(const EditorContext& ctx,
const std::filesystem::path& dir)
{
// 收集 + 过滤 + 排序(名字序;目录迭代序不稳定)
struct Item
{
std::filesystem::path path;
Kind kind;
};
std::vector<Item> items;
const std::string filter = m_NameFilter;
for (const auto& entry : std::filesystem::directory_iterator(dir))
{
if (entry.is_directory())
{
// 子目录也进列表:单行点击进入(树/列表双向导航)
const std::string dname = entry.path().filename().string();
ImGui::PushID(dname.c_str());
ImGui::TextUnformatted("[D]");
ImGui::SameLine();
if (ImGui::Selectable(dname.c_str()))
{
m_CurrentDir = entry.path();
}
ImGui::PopID();
continue;
}
const Kind kind = KindOf(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;
}
}
items.push_back({entry.path(), kind});
}
std::sort(items.begin(), items.end(),
[](const Item& a, const Item& b)
{
return a.path.filename().string() <
b.path.filename().string();
});
for (const auto& item : items)
{
const std::string name = item.path.filename().string();
const std::string assetPath = ToAssetPath(item.path);
ImGui::PushID(name.c_str());
ImGui::TextUnformatted(KindTag(item.kind));
ImGui::SameLine();
ImGui::Selectable(name.c_str());
// 双击预热:model/HDR 进缓存(路径去重,重复双击零成本);
// 场景加载含环境恢复/选中清理流程,提示走 File 菜单
if (ImGui::IsItemHovered() &&
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
if (item.kind == Kind::Model && ctx.assets)
{
const auto ref = ctx.assets->LoadModel(assetPath);
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (item.kind == Kind::Texture && ctx.assets)
{
const auto ref = ctx.assets->LoadEnvironment(assetPath);
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (item.kind == Kind::Scene)
{
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
assetPath);
}
}
// tooltip:完整相对路径(面板窄时文件名截断仍可辨识)
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", assetPath.c_str());
}
ImGui::PopID();
}
}