feat: implement double-column Project panel with breadcrumbs, IconSize slider, folders-first sorting, list/grid auto-switching, and wrap-layout

This commit is contained in:
ayuan9957 2026-06-08 10:27:03 +08:00
parent d7590c53b6
commit 5b4095f049

View File

@ -82,6 +82,7 @@ static std::array<char, 256> AssetFilterBuffer_{};
static std::filesystem::path PendingRenamePath_{};
static std::array<char, 256> RenameBuffer_{};
static std::array<char, 256> NewFolderBuffer_{};
static float IconSize_ = 64.0f;
static HANDLE PlayerProcessHandle_ = NULL;
static HANDLE PlayerStdoutRead_ = NULL;
@ -4573,132 +4574,241 @@ public:
ImGui::NextColumn();
const std::filesystem::path selectedDirectory = editorContext.GetSelectedProjectDirectory();
ImGui::TextDisabled("目录: %s", selectedDirectory.generic_string().c_str());
ImGui::Separator();
for (const auto& d : assetDatabaseService->GetDirectoriesUnder(selectedDirectory)) {
const bool isSelected = selectedDirectory == d;
if (isSelected) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
// 1. 绘制面包屑导航 (Breadcrumbs)
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2));
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("路径:");
ImGui::SameLine();
std::vector<std::filesystem::path> pathSegments;
{
std::filesystem::path p = selectedDirectory;
while (p != "Assets" && p != "Scenes" && p != "Ui" && p != "Runtime" && p.has_relative_path() && !p.empty()) {
pathSegments.push_back(p);
p = p.parent_path();
}
const bool clicked = MatchesAssetFilter(d.filename().string()) && ImGui::Selectable(("[Dir] " + d.filename().string()).c_str(), isSelected);
if (isSelected) {
ImGui::PopStyleColor(3);
pathSegments.push_back(p);
}
for (int i = static_cast<int>(pathSegments.size()) - 1; i >= 0; --i) {
if (i < static_cast<int>(pathSegments.size()) - 1) {
ImGui::SameLine();
ImGui::TextDisabled(">");
ImGui::SameLine();
}
if (clicked) {
editorContext.SetSelectedProjectDirectory(d);
std::string segName = pathSegments[i].filename().string();
if (segName.empty()) {
segName = pathSegments[i].string();
}
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / d);
if (pathSegments[i] == selectedDirectory) {
ImGui::Text("%s", segName.c_str());
} else {
if (ImGui::SmallButton(segName.c_str())) {
editorContext.SetSelectedProjectDirectory(pathSegments[i]);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = d;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", d.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(d);
}
ImGui::EndPopup();
}
}
const MetaCoreAssetGuid selectedAssetGuid = editorContext.GetSelectedAsset().Guid;
// 2. 绘制图标尺寸滑块
ImGui::SameLine(ImGui::GetContentRegionMax().x - 120.0f);
ImGui::PushItemWidth(100.0f);
ImGui::SliderFloat("##IconSize", &IconSize_, 16.0f, 128.0f, "");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("图标大小: %.0f px", IconSize_);
}
ImGui::PopItemWidth();
ImGui::PopStyleVar();
ImGui::Separator();
ImGui::Spacing();
// 3. 收集并过滤当前目录下的子项 (文件夹在前,文件在后)
struct ProjectItem {
bool IsDirectory = false;
std::filesystem::path Path{};
MetaCoreAssetGuid Guid{};
std::string Type{};
MetaCoreAssetStorageKind StorageKind = MetaCoreAssetStorageKind::SourcePackage;
};
std::vector<ProjectItem> items;
for (const auto& d : assetDatabaseService->GetDirectoriesUnder(selectedDirectory)) {
if (MatchesAssetFilter(d.filename().string())) {
ProjectItem item{};
item.IsDirectory = true;
item.Path = d;
items.push_back(item);
}
}
for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) {
if (!MatchesAssetFilter(a.RelativePath.filename().string()) && !MatchesAssetFilter(a.Type)) continue;
const std::string label = a.RelativePath.filename().string() + " [" + a.Type + "]";
const bool selected = selectedAssetGuid == a.Guid;
if (selected) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
}
const bool clicked = ImGui::Selectable(label.c_str(), selected);
if (selected) {
ImGui::PopStyleColor(3);
}
if (clicked) {
editorContext.SelectAsset(MetaCoreSelectedAssetState{
a.Guid,
a.RelativePath,
a.Type,
a.StorageKind
});
editorContext.ClearSelectedAssetSubId();
editorContext.ClearSelection();
if (ImGui::IsMouseDoubleClicked(0)) {
if (a.Type == "scene" && scenePersistenceService) (void)scenePersistenceService->LoadScene(editorContext, a.RelativePath);
else if (a.Type == "model") (void)MetaCoreInstantiateModelAsset(editorContext, a.Guid, std::nullopt);
else if (a.Type == "prefab") (void)MetaCoreInstantiatePrefab(editorContext, a.Guid, std::nullopt);
ProjectItem item{};
item.IsDirectory = false;
item.Path = a.RelativePath;
item.Guid = a.Guid;
item.Type = a.Type;
item.StorageKind = a.StorageKind;
items.push_back(item);
}
// 4. 渲染视图项
if (IconSize_ <= 32.0f) {
// 列表视图模式 (List View)
for (const auto& item : items) {
std::string label;
ImVec4 iconColor(1.0f, 1.0f, 1.0f, 1.0f);
const char* iconStr = "📄";
if (item.IsDirectory) {
iconStr = "📁";
iconColor = ImVec4(0.95f, 0.75f, 0.25f, 1.0f);
label = std::string(iconStr) + " " + item.Path.filename().string();
} else {
if (item.Type == "material") {
iconStr = "🔮";
iconColor = ImVec4(0.65f, 0.45f, 0.95f, 1.0f);
} else if (item.Type == "model") {
iconStr = "📦";
iconColor = ImVec4(0.25f, 0.85f, 0.45f, 1.0f);
} else if (item.Type == "scene") {
iconStr = "🎬";
iconColor = ImVec4(0.95f, 0.35f, 0.35f, 1.0f);
} else if (item.Type == "ui_document") {
iconStr = "📄";
iconColor = ImVec4(0.25f, 0.75f, 0.95f, 1.0f);
}
label = std::string(iconStr) + " " + item.Path.filename().string() + " [" + item.Type + "]";
}
}
MetaCoreBeginProjectAssetDragDropSource(a);
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Open")) {
if (a.Type == "scene" && scenePersistenceService) {
(void)scenePersistenceService->LoadScene(editorContext, a.RelativePath);
} else if (a.Type == "model") {
(void)MetaCoreInstantiateModelAsset(editorContext, a.Guid, std::nullopt);
} else if (a.Type == "prefab") {
(void)MetaCoreInstantiatePrefab(editorContext, a.Guid, std::nullopt);
bool isSelected = false;
if (item.IsDirectory) {
isSelected = (selectedDirectory == item.Path);
} else {
isSelected = (editorContext.GetSelectedAsset().Guid == item.Guid);
}
if (isSelected) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
}
ImGui::PushID(item.IsDirectory ? item.Path.string().c_str() : item.Guid.ToString().c_str());
const bool clicked = ImGui::Selectable(label.c_str(), isSelected);
if (isSelected) {
ImGui::PopStyleColor(3);
}
if (clicked) {
if (item.IsDirectory) {
editorContext.SetSelectedProjectDirectory(item.Path);
} else {
editorContext.SelectAsset(MetaCoreSelectedAssetState{
a.Guid,
a.RelativePath,
a.Type,
a.StorageKind
item.Guid,
item.Path,
item.Type,
item.StorageKind
});
editorContext.ClearSelectedAssetSubId();
editorContext.ClearSelection();
if (ImGui::IsMouseDoubleClicked(0)) {
if (item.Type == "scene" && scenePersistenceService) (void)scenePersistenceService->LoadScene(editorContext, item.Path);
else if (item.Type == "model") (void)MetaCoreInstantiateModelAsset(editorContext, item.Guid, std::nullopt);
else if (item.Type == "prefab") (void)MetaCoreInstantiatePrefab(editorContext, item.Guid, std::nullopt);
}
}
}
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / a.RelativePath);
}
if (ImGui::MenuItem("Copy GUID")) {
const std::string guidText = a.Guid.ToString();
ImGui::SetClipboardText(guidText.c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("Reimport", nullptr, false, !a.SourcePath.empty())) {
MetaCoreReimportProjectAsset(editorContext, a.Guid);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = a.RelativePath;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", a.RelativePath.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(a.RelativePath);
}
ImGui::EndPopup();
}
if (selected && a.Type == "model") {
const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a);
if (modelDocument.has_value()) {
if (!item.IsDirectory) {
MetaCoreAssetRecord a{};
a.Guid = item.Guid;
a.RelativePath = item.Path;
a.Type = item.Type;
a.StorageKind = item.StorageKind;
MetaCoreBeginProjectAssetDragDropSource(a);
}
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (item.IsDirectory) {
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = item.Path;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", item.Path.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(item.Path);
}
} else {
if (ImGui::MenuItem("Open")) {
if (item.Type == "scene" && scenePersistenceService) {
(void)scenePersistenceService->LoadScene(editorContext, item.Path);
} else if (item.Type == "model") {
(void)MetaCoreInstantiateModelAsset(editorContext, item.Guid, std::nullopt);
} else if (item.Type == "prefab") {
(void)MetaCoreInstantiatePrefab(editorContext, item.Guid, std::nullopt);
} else {
editorContext.SelectAsset(MetaCoreSelectedAssetState{
item.Guid,
item.Path,
item.Type,
item.StorageKind
});
editorContext.ClearSelectedAssetSubId();
editorContext.ClearSelection();
}
}
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path);
}
if (ImGui::MenuItem("Copy GUID")) {
const std::string guidText = item.Guid.ToString();
ImGui::SetClipboardText(guidText.c_str());
}
ImGui::Separator();
const auto fullRecord = assetDatabaseService->FindAssetByGuid(item.Guid);
const bool canReimport = fullRecord.has_value() && !fullRecord->SourcePath.empty();
if (ImGui::MenuItem("Reimport", nullptr, false, canReimport)) {
MetaCoreReimportProjectAsset(editorContext, item.Guid);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = item.Path;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", item.Path.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(item.Path);
}
}
ImGui::EndPopup();
}
if (!item.IsDirectory && isSelected && item.Type == "model") {
MetaCoreAssetRecord a{};
a.Guid = item.Guid;
a.RelativePath = item.Path;
a.Type = item.Type;
a.StorageKind = item.StorageKind;
const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a);
if (modelDocument.has_value()) {
ImGui::Indent(18.0F);
for (const auto& generatedEntry : BuildGeneratedResourceListEntries(*modelDocument)) {
if (!MatchesGeneratedResourceKindFilter(generatedEntry.Kind)) {
continue;
}
if (!MatchesGeneratedResourceKindFilter(generatedEntry.Kind)) continue;
const std::string filterLabel = generatedEntry.Name + " " + generatedEntry.SecondaryText;
if (!MatchesGeneratedResourceFilter(filterLabel)) {
continue;
}
if (!MatchesGeneratedResourceFilter(filterLabel)) continue;
const std::string subId = MetaCoreBuildGeneratedResourceSubId(
generatedEntry.Kind,
generatedEntry.AssetGuid
);
const bool generatedSelected =
editorContext.GetSelectedAsset().Guid == a.Guid &&
editorContext.GetSelectedAssetSubId() == subId;
const std::string generatedLabel =
"" + generatedEntry.Name + " [" + generatedEntry.SecondaryText + "]";
const std::string subId = MetaCoreBuildGeneratedResourceSubId(generatedEntry.Kind, generatedEntry.AssetGuid);
const bool generatedSelected = editorContext.GetSelectedAsset().Guid == a.Guid && editorContext.GetSelectedAssetSubId() == subId;
const std::string generatedLabel = "" + generatedEntry.Name + " [" + generatedEntry.SecondaryText + "]";
if (generatedSelected) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.173F, 0.365F, 0.529F, 1.0F));
@ -4720,8 +4830,201 @@ public:
}
}
ImGui::Unindent(18.0F);
}
}
ImGui::PopID();
}
} else {
// 网格卡片视图模式 (Grid View)
const float windowVisibleX2 = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
const ImGuiStyle& style = ImGui::GetStyle();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 8.0f));
for (const auto& item : items) {
std::string filename = item.Path.filename().string();
bool isSelected = false;
if (item.IsDirectory) {
isSelected = (selectedDirectory == item.Path);
} else {
isSelected = (editorContext.GetSelectedAsset().Guid == item.Guid);
}
ImGui::PushID(item.IsDirectory ? item.Path.string().c_str() : item.Guid.ToString().c_str());
const ImVec2 cardSize(IconSize_ + 16.0f, IconSize_ + 36.0f);
ImGui::BeginGroup();
const ImVec2 startPos = ImGui::GetCursorPos();
if (isSelected) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.173F, 0.365F, 0.529F, 0.40F));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.173F, 0.365F, 0.529F, 0.60F));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.173F, 0.365F, 0.529F, 0.80F));
} else {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.08f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1.0f, 1.0f, 1.0f, 0.12f));
}
const bool clicked = ImGui::Selectable("##card", isSelected, 0, cardSize);
ImGui::PopStyleColor(3);
const ImVec2 currentPos = ImGui::GetCursorPos();
ImGui::SetCursorPos(startPos);
ImGui::SetCursorPosY(startPos.y + 6.0f);
ImVec4 iconColor(1.0f, 1.0f, 1.0f, 1.0f);
const char* iconStr = "📄";
if (item.IsDirectory) {
iconStr = "📁";
iconColor = ImVec4(0.95f, 0.75f, 0.25f, 1.0f);
} else if (item.Type == "material") {
iconStr = "🔮";
iconColor = ImVec4(0.65f, 0.45f, 0.95f, 1.0f);
} else if (item.Type == "model") {
iconStr = "📦";
iconColor = ImVec4(0.25f, 0.85f, 0.45f, 1.0f);
} else if (item.Type == "scene") {
iconStr = "🎬";
iconColor = ImVec4(0.95f, 0.35f, 0.35f, 1.0f);
} else if (item.Type == "ui_document") {
iconStr = "📄";
iconColor = ImVec4(0.25f, 0.75f, 0.95f, 1.0f);
}
float scaleFactor = IconSize_ / 32.0f;
if (scaleFactor < 1.0f) scaleFactor = 1.0f;
if (scaleFactor > 3.0f) scaleFactor = 3.0f;
ImGui::SetWindowFontScale(scaleFactor);
const float textWidth = ImGui::CalcTextSize(iconStr).x;
ImGui::SetCursorPosX(startPos.x + (cardSize.x - textWidth) * 0.5f);
ImGui::TextColored(iconColor, "%s", iconStr);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPosY(startPos.y + IconSize_ + 8.0f);
std::string displayName = filename;
if (ImGui::CalcTextSize(displayName.c_str()).x > cardSize.x - 4.0f) {
while (!displayName.empty() && ImGui::CalcTextSize((displayName + "...").c_str()).x > cardSize.x - 4.0f) {
displayName.pop_back();
}
displayName += "...";
}
const float nameWidth = ImGui::CalcTextSize(displayName.c_str()).x;
ImGui::SetCursorPosX(startPos.x + (cardSize.x - nameWidth) * 0.5f);
ImGui::TextUnformatted(displayName.c_str());
ImGui::SetCursorPos(currentPos);
ImGui::EndGroup();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s\n类型: %s", item.Path.string().c_str(), item.IsDirectory ? "Folder" : item.Type.c_str());
}
if (clicked) {
if (item.IsDirectory) {
if (ImGui::IsMouseDoubleClicked(0)) {
editorContext.SetSelectedProjectDirectory(item.Path);
}
} else {
editorContext.SelectAsset(MetaCoreSelectedAssetState{
item.Guid,
item.Path,
item.Type,
item.StorageKind
});
editorContext.ClearSelectedAssetSubId();
editorContext.ClearSelection();
if (ImGui::IsMouseDoubleClicked(0)) {
if (item.Type == "scene" && scenePersistenceService) (void)scenePersistenceService->LoadScene(editorContext, item.Path);
else if (item.Type == "model") (void)MetaCoreInstantiateModelAsset(editorContext, item.Guid, std::nullopt);
else if (item.Type == "prefab") (void)MetaCoreInstantiatePrefab(editorContext, item.Guid, std::nullopt);
}
}
}
if (!item.IsDirectory) {
MetaCoreAssetRecord a{};
a.Guid = item.Guid;
a.RelativePath = item.Path;
a.Type = item.Type;
a.StorageKind = item.StorageKind;
MetaCoreBeginProjectAssetDragDropSource(a);
}
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (item.IsDirectory) {
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = item.Path;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", item.Path.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(item.Path);
}
} else {
if (ImGui::MenuItem("Open")) {
if (item.Type == "scene" && scenePersistenceService) {
(void)scenePersistenceService->LoadScene(editorContext, item.Path);
} else if (item.Type == "model") {
(void)MetaCoreInstantiateModelAsset(editorContext, item.Guid, std::nullopt);
} else if (item.Type == "prefab") {
(void)MetaCoreInstantiatePrefab(editorContext, item.Guid, std::nullopt);
} else {
editorContext.SelectAsset(MetaCoreSelectedAssetState{
item.Guid,
item.Path,
item.Type,
item.StorageKind
});
editorContext.ClearSelectedAssetSubId();
editorContext.ClearSelection();
}
}
if (ImGui::MenuItem("Reveal in Explorer")) {
MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path);
}
if (ImGui::MenuItem("Copy GUID")) {
const std::string guidText = item.Guid.ToString();
ImGui::SetClipboardText(guidText.c_str());
}
ImGui::Separator();
const auto fullRecord = assetDatabaseService->FindAssetByGuid(item.Guid);
const bool canReimport = fullRecord.has_value() && !fullRecord->SourcePath.empty();
if (ImGui::MenuItem("Reimport", nullptr, false, canReimport)) {
MetaCoreReimportProjectAsset(editorContext, item.Guid);
}
ImGui::Separator();
if (ImGui::MenuItem("Rename...")) {
PendingRenamePath_ = item.Path;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", item.Path.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("Delete")) {
(void)assetDatabaseService->DeletePath(item.Path);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
const float lastButtonX2 = ImGui::GetItemRectMax().x;
const float nextButtonX2 = lastButtonX2 + cardSize.x + style.ItemSpacing.x;
if (nextButtonX2 < windowVisibleX2) {
ImGui::SameLine();
}
}
ImGui::PopStyleVar();
}
ImGui::Columns(1);