2026-03-20 17:08:06 +08:00
|
|
|
#include "ProjectPanel.h"
|
2026-03-25 16:25:55 +08:00
|
|
|
#include "Core/IEditorContext.h"
|
|
|
|
|
#include "Core/IProjectManager.h"
|
2026-03-26 01:26:26 +08:00
|
|
|
#include "Core/ISceneManager.h"
|
2026-03-26 01:59:14 +08:00
|
|
|
#include "Core/IUndoManager.h"
|
|
|
|
|
#include "Core/ISelectionManager.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include "Core/AssetItem.h"
|
2026-03-26 16:43:06 +08:00
|
|
|
#include "UI/Core.h"
|
|
|
|
|
#include "UI/PanelChrome.h"
|
2026-03-26 01:26:26 +08:00
|
|
|
#include "Utils/SceneEditorUtils.h"
|
2026-03-20 17:08:06 +08:00
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <imgui_internal.h>
|
|
|
|
|
|
2026-03-24 20:02:38 +08:00
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Editor {
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
const char* DRAG_DROP_TYPE = "ASSET_ITEM";
|
|
|
|
|
|
2026-03-26 16:43:06 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
constexpr float kProjectToolbarHeight = 60.0f;
|
|
|
|
|
|
|
|
|
|
void DrawFolderIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 fillColor, ImU32 lineColor) {
|
|
|
|
|
const float width = max.x - min.x;
|
|
|
|
|
const float height = max.y - min.y;
|
|
|
|
|
const ImVec2 tabMax(min.x + width * 0.45f, min.y + height * 0.35f);
|
|
|
|
|
drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.14f), tabMax, fillColor, 2.0f);
|
|
|
|
|
drawList->AddRectFilled(ImVec2(min.x, min.y + height * 0.28f), max, fillColor, 2.0f);
|
|
|
|
|
drawList->AddRect(ImVec2(min.x, min.y + height * 0.14f), tabMax, lineColor, 2.0f);
|
|
|
|
|
drawList->AddRect(ImVec2(min.x, min.y + height * 0.28f), max, lineColor, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrawFileIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, ImU32 fillColor, ImU32 lineColor) {
|
|
|
|
|
const ImVec2 foldA(max.x - 8.0f, min.y);
|
|
|
|
|
const ImVec2 foldB(max.x, min.y + 8.0f);
|
|
|
|
|
drawList->AddRectFilled(min, max, fillColor, 2.0f);
|
|
|
|
|
drawList->AddRect(min, max, lineColor, 2.0f);
|
|
|
|
|
drawList->AddTriangleFilled(foldA, ImVec2(max.x, min.y), foldB, IM_COL32(235, 235, 235, 40));
|
|
|
|
|
drawList->AddLine(foldA, foldB, lineColor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
ProjectPanel::ProjectPanel() : Panel("Project") {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ProjectPanel::Initialize(const std::string& projectPath) {
|
2026-03-25 16:25:55 +08:00
|
|
|
m_context->GetProjectManager().Initialize(projectPath);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ProjectPanel::Render() {
|
|
|
|
|
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
|
|
|
|
|
if (payload && payload->IsDataType(DRAG_DROP_TYPE)) {
|
|
|
|
|
m_draggingPath = (const char*)payload->Data;
|
|
|
|
|
} else if (!ImGui::IsMouseDown(0)) {
|
|
|
|
|
m_draggingPath.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 16:43:06 +08:00
|
|
|
UI::PanelWindowScope panel(m_name.c_str());
|
|
|
|
|
if (!panel.IsOpen()) {
|
|
|
|
|
return;
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 16:43:06 +08:00
|
|
|
auto& manager = m_context->GetProjectManager();
|
|
|
|
|
|
|
|
|
|
UI::PanelToolbarScope toolbar("ProjectToolbar", kProjectToolbarHeight);
|
|
|
|
|
if (toolbar.IsOpen()) {
|
|
|
|
|
|
|
|
|
|
bool canGoBack = manager.CanNavigateBack();
|
|
|
|
|
ImGui::BeginDisabled(!canGoBack);
|
|
|
|
|
if (UI::ToolbarButton("<", false, ImVec2(28.0f, 0.0f))) {
|
|
|
|
|
if (canGoBack) {
|
|
|
|
|
manager.NavigateBack();
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
}
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
size_t pathDepth = manager.GetPathDepth();
|
|
|
|
|
if (pathDepth == 0) {
|
|
|
|
|
ImGui::TextUnformatted("Assets");
|
2026-03-20 17:08:06 +08:00
|
|
|
} else {
|
2026-03-26 16:43:06 +08:00
|
|
|
for (size_t i = 0; i < pathDepth; i++) {
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextDisabled("/");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
}
|
|
|
|
|
std::string name = manager.GetPathName(i);
|
|
|
|
|
if (i < pathDepth - 1) {
|
|
|
|
|
if (ImGui::Button(name.c_str())) {
|
|
|
|
|
manager.NavigateToIndex(i);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::Text("%s", name.c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
ImGui::Dummy(ImVec2(0.0f, 2.0f));
|
|
|
|
|
ImGui::SetNextItemWidth(-1.0f);
|
|
|
|
|
ImGui::InputTextWithHint("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
UI::PanelContentScope content(
|
|
|
|
|
"ProjectContent",
|
|
|
|
|
UI::AssetPanelContentPadding(),
|
|
|
|
|
ImGuiWindowFlags_None,
|
|
|
|
|
true,
|
|
|
|
|
ImVec2(8.0f, 10.0f));
|
|
|
|
|
if (!content.IsOpen()) {
|
|
|
|
|
return;
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
float buttonWidth = 104.0f;
|
|
|
|
|
float padding = 8.0f;
|
2026-03-20 17:08:06 +08:00
|
|
|
float panelWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
|
int columns = (int)(panelWidth / (buttonWidth + padding));
|
|
|
|
|
if (columns < 1) columns = 1;
|
|
|
|
|
|
|
|
|
|
auto& items = manager.GetCurrentItems();
|
|
|
|
|
std::string searchStr = m_searchBuffer;
|
2026-03-26 01:26:26 +08:00
|
|
|
int displayedCount = 0;
|
2026-03-20 17:08:06 +08:00
|
|
|
|
|
|
|
|
for (int i = 0; i < (int)items.size(); i++) {
|
|
|
|
|
if (!searchStr.empty()) {
|
|
|
|
|
if (items[i]->name.find(searchStr) == std::string::npos) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 01:26:26 +08:00
|
|
|
if (displayedCount > 0 && displayedCount % columns != 0) {
|
2026-03-20 17:08:06 +08:00
|
|
|
ImGui::SameLine();
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
RenderAssetItem(items[i], i);
|
|
|
|
|
displayedCount++;
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) {
|
|
|
|
|
manager.SetSelectedIndex(-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginPopup("ItemContextMenu")) {
|
|
|
|
|
if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) {
|
|
|
|
|
auto& item = items[m_contextMenuIndex];
|
2026-03-26 01:26:26 +08:00
|
|
|
if (item->isFolder || item->type == "Scene") {
|
2026-03-20 17:08:06 +08:00
|
|
|
if (ImGui::MenuItem("Open")) {
|
2026-03-26 01:26:26 +08:00
|
|
|
if (item->isFolder) {
|
|
|
|
|
manager.NavigateToFolder(item);
|
|
|
|
|
} else if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) {
|
2026-03-26 01:59:14 +08:00
|
|
|
if (m_context->GetSceneManager().LoadScene(item->fullPath)) {
|
|
|
|
|
m_context->GetSelectionManager().ClearSelection();
|
|
|
|
|
m_context->GetUndoManager().ClearHistory();
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::MenuItem("Delete")) {
|
|
|
|
|
manager.DeleteItem(m_contextMenuIndex);
|
|
|
|
|
m_contextMenuIndex = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) {
|
|
|
|
|
ImGui::OpenPopup("EmptyContextMenu");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginPopup("EmptyContextMenu")) {
|
|
|
|
|
if (ImGui::MenuItem("Create Folder")) {
|
|
|
|
|
m_showCreateFolderPopup = true;
|
|
|
|
|
strcpy_s(m_newFolderName, "NewFolder");
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_showCreateFolderPopup) {
|
|
|
|
|
ImGui::OpenPopup("Create Folder");
|
|
|
|
|
m_showCreateFolderPopup = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginPopupModal("Create Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
ImGui::InputText("Name", m_newFolderName, sizeof(m_newFolderName));
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::Button("Create", ImVec2(80, 0))) {
|
|
|
|
|
CreateNewFolder(m_newFolderName);
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
|
2026-03-25 16:25:55 +08:00
|
|
|
auto& manager = m_context->GetProjectManager();
|
2026-03-20 17:08:06 +08:00
|
|
|
bool isSelected = (manager.GetSelectedIndex() == index);
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(index);
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
ImVec2 buttonSize(104.0f, 82.0f);
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("##AssetBtn", buttonSize);
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
2026-03-20 17:08:06 +08:00
|
|
|
manager.SetSelectedIndex(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool doubleClicked = false;
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
|
|
|
|
doubleClicked = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool openContextMenu = false;
|
|
|
|
|
if (ImGui::IsItemClicked(1)) {
|
|
|
|
|
manager.SetSelectedIndex(index);
|
|
|
|
|
m_contextMenuIndex = index;
|
|
|
|
|
openContextMenu = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImVec2 min = ImGui::GetItemRectMin();
|
|
|
|
|
ImVec2 max = ImVec2(min.x + buttonSize.x, min.y + buttonSize.y);
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
|
|
|
|
if (hovered || isSelected) {
|
|
|
|
|
drawList->AddRectFilled(min, max, isSelected ? IM_COL32(156, 156, 156, 30) : IM_COL32(255, 255, 255, 10), 2.0f);
|
|
|
|
|
}
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
drawList->AddRect(min, max, IM_COL32(188, 188, 188, 110), 2.0f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:08:06 +08:00
|
|
|
if (!m_draggingPath.empty() && item->fullPath == m_draggingPath) {
|
|
|
|
|
drawList->AddRectFilled(min, max, IM_COL32(0, 0, 0, 60), 0.0f);
|
|
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
ImU32 iconFillColor = item->isFolder ? IM_COL32(118, 118, 118, 255) : IM_COL32(104, 104, 104, 255);
|
|
|
|
|
ImU32 iconLineColor = item->isFolder ? IM_COL32(184, 184, 184, 220) : IM_COL32(166, 166, 166, 220);
|
|
|
|
|
|
|
|
|
|
const ImVec2 iconMin(min.x + 14.0f, min.y + 12.0f);
|
|
|
|
|
const ImVec2 iconMax(iconMin.x + 28.0f, iconMin.y + 22.0f);
|
2026-03-20 17:08:06 +08:00
|
|
|
if (item->isFolder) {
|
2026-03-26 16:43:06 +08:00
|
|
|
DrawFolderIcon(drawList, iconMin, iconMax, iconFillColor, iconLineColor);
|
2026-03-20 17:08:06 +08:00
|
|
|
} else {
|
2026-03-26 16:43:06 +08:00
|
|
|
DrawFileIcon(drawList, iconMin, iconMax, iconFillColor, iconLineColor);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
2026-03-26 16:43:06 +08:00
|
|
|
|
|
|
|
|
ImVec4 textColor = isSelected ? ImVec4(0.93f, 0.93f, 0.93f, 1.0f) : ImVec4(0.76f, 0.76f, 0.76f, 1.0f);
|
2026-03-20 17:08:06 +08:00
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(item->name.c_str());
|
2026-03-26 16:43:06 +08:00
|
|
|
float textY = max.y - textSize.y - 10.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::PushClipRect(ImVec2(min.x + 6.0f, min.y), ImVec2(max.x - 6.0f, max.y), true);
|
|
|
|
|
drawList->AddText(ImVec2(min.x + 6.0f, textY), ImGui::GetColorU32(textColor), item->name.c_str());
|
2026-03-20 17:08:06 +08:00
|
|
|
ImGui::PopClipRect();
|
|
|
|
|
|
|
|
|
|
if (item->isFolder) {
|
|
|
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
|
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(DRAG_DROP_TYPE)) {
|
|
|
|
|
const char* draggedPath = (const char*)payload->Data;
|
|
|
|
|
std::string sourcePath(draggedPath);
|
|
|
|
|
manager.MoveItem(sourcePath, item->fullPath);
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndDragDropTarget();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!item->fullPath.empty()) {
|
|
|
|
|
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
|
|
|
|
ImGui::SetDragDropPayload(DRAG_DROP_TYPE, item->fullPath.c_str(), item->fullPath.length() + 1);
|
|
|
|
|
|
2026-03-26 16:43:06 +08:00
|
|
|
ImVec2 previewMin = ImGui::GetMousePos();
|
|
|
|
|
ImVec2 previewMax = ImVec2(previewMin.x + 24.0f, previewMin.y + 20.0f);
|
2026-03-20 17:08:06 +08:00
|
|
|
if (item->isFolder) {
|
2026-03-26 16:43:06 +08:00
|
|
|
DrawFolderIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconFillColor, iconLineColor);
|
2026-03-20 17:08:06 +08:00
|
|
|
} else {
|
2026-03-26 16:43:06 +08:00
|
|
|
DrawFileIcon(ImGui::GetForegroundDrawList(), previewMin, previewMax, iconFillColor, iconLineColor);
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndDragDropSource();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 01:26:26 +08:00
|
|
|
if (doubleClicked) {
|
|
|
|
|
if (item->isFolder) {
|
|
|
|
|
manager.NavigateToFolder(item);
|
|
|
|
|
} else if (item->type == "Scene") {
|
|
|
|
|
if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) {
|
2026-03-26 01:59:14 +08:00
|
|
|
if (m_context->GetSceneManager().LoadScene(item->fullPath)) {
|
|
|
|
|
m_context->GetSelectionManager().ClearSelection();
|
|
|
|
|
m_context->GetUndoManager().ClearHistory();
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 17:08:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
|
|
|
|
if (openContextMenu) {
|
|
|
|
|
ImGui::OpenPopup("ItemContextMenu");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ProjectPanel::CreateNewFolder(const std::string& name) {
|
2026-03-25 16:25:55 +08:00
|
|
|
auto& manager = m_context->GetProjectManager();
|
2026-03-20 17:08:06 +08:00
|
|
|
manager.CreateFolder(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ProjectPanel::HandleDrop(const AssetItemPtr& targetFolder) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 20:02:38 +08:00
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
}
|