Files
XCEngine/editor/src/panels/ProjectPanel.cpp

347 lines
12 KiB
C++
Raw Normal View History

2026-03-26 22:10:43 +08:00
#include "Actions/ActionRouting.h"
2026-03-26 23:52:05 +08:00
#include "Actions/ProjectActionRouter.h"
2026-03-26 21:18:33 +08:00
#include "Commands/ProjectCommands.h"
#include "ProjectPanel.h"
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/AssetItem.h"
2026-03-26 21:18:33 +08:00
#include "UI/UI.h"
#include <algorithm>
#include <cctype>
#include <imgui.h>
namespace XCEngine {
namespace Editor {
ProjectPanel::ProjectPanel() : Panel("Project") {
}
void ProjectPanel::Initialize(const std::string& projectPath) {
m_context->GetProjectManager().Initialize(projectPath);
}
void ProjectPanel::Render() {
UI::PanelWindowScope panel(m_name.c_str());
if (!panel.IsOpen()) {
return;
}
2026-03-26 22:10:43 +08:00
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project);
auto& manager = m_context->GetProjectManager();
RenderToolbar();
UI::PanelContentScope content("ProjectContent", ImVec2(0.0f, 0.0f));
if (!content.IsOpen()) {
return;
}
const float totalHeight = ImGui::GetContentRegionAvail().y;
const float splitterWidth = UI::PanelSplitterHitThickness();
const float availableWidth = ImGui::GetContentRegionAvail().x;
const float clampedNavigationWidth = std::clamp(
m_navigationWidth,
UI::ProjectNavigationMinWidth(),
std::max(UI::ProjectNavigationMinWidth(), availableWidth - UI::ProjectBrowserMinWidth() - splitterWidth));
m_navigationWidth = clampedNavigationWidth;
RenderFolderTreePane(manager);
ImGui::SameLine(0.0f, 0.0f);
const UI::SplitterResult splitter = UI::DrawSplitter("##ProjectPaneSplitter", UI::SplitterAxis::Vertical, totalHeight);
if (splitter.active) {
m_navigationWidth += splitter.delta;
}
ImGui::SameLine(0.0f, 0.0f);
RenderBrowserPane(manager);
Actions::DrawProjectCreateFolderDialog(*m_context, m_createFolderDialog);
}
void ProjectPanel::RenderToolbar() {
2026-03-26 21:18:33 +08:00
UI::PanelToolbarScope toolbar("ProjectToolbar", UI::ProjectPanelToolbarHeight());
if (!toolbar.IsOpen()) {
return;
}
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("##Spacer", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
2026-03-26 21:18:33 +08:00
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
ImGui::EndTable();
}
}
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
if (!open) {
ImGui::EndChild();
return;
}
const AssetItemPtr rootFolder = manager.GetRootFolder();
const AssetItemPtr currentFolder = manager.GetCurrentFolder();
const std::string currentFolderPath = currentFolder ? currentFolder->fullPath : std::string();
if (rootFolder) {
RenderFolderTreeNode(manager, rootFolder, currentFolderPath);
} else {
UI::DrawEmptyState("No Assets Folder");
}
ImGui::EndChild();
}
void ProjectPanel::RenderFolderTreeNode(
IProjectManager& manager,
const AssetItemPtr& folder,
const std::string& currentFolderPath) {
if (!folder || !folder->isFolder) {
return;
}
bool hasChildFolders = false;
for (const auto& child : folder->children) {
if (child && child->isFolder) {
hasChildFolders = true;
break;
}
}
2026-03-27 22:05:05 +08:00
UI::TreeNodeOptions nodeOptions;
nodeOptions.selected = folder->fullPath == currentFolderPath;
nodeOptions.leaf = !hasChildFolders;
nodeOptions.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath);
2026-03-27 22:05:05 +08:00
const UI::TreeNodeResult node = UI::DrawTreeNode(
(void*)folder.get(),
folder->name.c_str(),
nodeOptions);
2026-03-27 22:05:05 +08:00
if (node.clicked) {
manager.NavigateToFolder(folder);
}
2026-03-27 22:05:05 +08:00
if (node.open) {
for (const auto& child : folder->children) {
if (!child || !child->isFolder) {
continue;
}
RenderFolderTreeNode(manager, child, currentFolderPath);
}
2026-03-27 22:05:05 +08:00
UI::EndTreeNode();
}
}
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
if (!open) {
ImGui::EndChild();
return;
}
std::vector<AssetItemPtr> visibleItems;
const auto& items = manager.GetCurrentItems();
const std::string search = m_searchBuffer;
visibleItems.reserve(items.size());
for (const auto& item : items) {
if (MatchesSearch(item, search)) {
visibleItems.push_back(item);
}
}
RenderBrowserHeader(manager);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectBrowserPanePadding());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::AssetGridSpacing());
const bool bodyOpen = ImGui::BeginChild("ProjectBrowserBody", ImVec2(0.0f, 0.0f), false);
ImGui::PopStyleVar(2);
if (!bodyOpen) {
ImGui::EndChild();
ImGui::EndChild();
return;
}
2026-03-26 21:18:33 +08:00
const float tileWidth = UI::AssetTileSize().x;
const float spacing = UI::AssetGridSpacing().x;
const float panelWidth = ImGui::GetContentRegionAvail().x;
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
if (columns < 1) {
columns = 1;
}
AssetItemPtr pendingSelection;
AssetItemPtr pendingContextTarget;
AssetItemPtr pendingOpenTarget;
AssetItemPtr pendingMoveTarget;
std::string pendingMoveSourcePath;
const std::string selectedItemPath = manager.GetSelectedItemPath();
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
if (visibleIndex > 0 && visibleIndex % columns != 0) {
ImGui::SameLine();
}
const AssetItemPtr& item = visibleItems[visibleIndex];
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
if (interaction.clicked) {
pendingSelection = item;
}
if (interaction.contextRequested) {
pendingContextTarget = item;
}
if (!interaction.droppedSourcePath.empty()) {
pendingMoveSourcePath = interaction.droppedSourcePath;
pendingMoveTarget = item;
break;
}
if (interaction.openRequested) {
pendingOpenTarget = item;
break;
}
}
if (visibleItems.empty()) {
2026-03-26 21:18:33 +08:00
UI::DrawEmptyState(
search.empty() ? "No Assets" : "No Search Results",
search.empty() ? "Current folder is empty" : "No assets match the current search");
2026-03-26 21:18:33 +08:00
}
Actions::HandleProjectBackgroundPrimaryClick(manager);
if (pendingSelection) {
manager.SetSelectedItem(pendingSelection);
}
if (pendingContextTarget) {
Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu);
}
if (!pendingMoveSourcePath.empty() && pendingMoveTarget) {
Commands::MoveAssetToFolder(manager, pendingMoveSourcePath, pendingMoveTarget);
}
if (pendingOpenTarget) {
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
}
Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu);
Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu);
Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, m_createFolderDialog);
2026-03-26 23:52:05 +08:00
ImGui::EndChild();
ImGui::EndChild();
}
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
const bool open = ImGui::BeginChild(
"ProjectBrowserHeader",
ImVec2(0.0f, UI::ProjectBrowserHeaderHeight()),
false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
if (!open) {
ImGui::EndChild();
return;
}
if (ImGui::BeginTable("##ProjectBrowserHeaderLayout", 1, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
UI::DrawToolbarBreadcrumbs(
"Assets",
manager.GetPathDepth(),
[&](size_t index) { return manager.GetPathName(index); },
[&](size_t index) { manager.NavigateToIndex(index); });
ImGui::EndTable();
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 min = ImGui::GetWindowPos();
const ImVec2 max(min.x + ImGui::GetWindowSize().x, min.y + ImGui::GetWindowSize().y);
UI::DrawHorizontalDivider(drawList, min.x, max.x, max.y - 0.5f);
ImGui::EndChild();
}
ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItemPtr& item, bool isSelected) {
AssetItemInteraction interaction;
ImGui::PushID(item ? item->fullPath.c_str() : "ProjectItem");
const bool isDraggingThisItem = Actions::IsProjectAssetBeingDragged(item);
2026-03-26 21:18:33 +08:00
const UI::AssetIconKind iconKind = item->isFolder ? UI::AssetIconKind::Folder : UI::AssetIconKind::File;
const UI::AssetTileResult tile = UI::DrawAssetTile(
item->name.c_str(),
isSelected,
isDraggingThisItem,
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
});
2026-03-26 21:18:33 +08:00
if (tile.clicked) {
interaction.clicked = true;
}
2026-03-26 21:18:33 +08:00
if (tile.contextRequested) {
interaction.contextRequested = true;
}
interaction.droppedSourcePath = Actions::AcceptProjectAssetDropPayload(item);
Actions::BeginProjectAssetDrag(item, iconKind);
2026-03-26 21:18:33 +08:00
if (tile.openRequested) {
interaction.openRequested = true;
}
ImGui::PopID();
return interaction;
}
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const std::string& search) {
if (!item) {
return false;
}
if (search.empty()) {
return true;
}
auto toLower = [](unsigned char c) {
return static_cast<char>(std::tolower(c));
};
std::string itemName = item->name;
std::string searchText = search;
std::transform(itemName.begin(), itemName.end(), itemName.begin(), toLower);
std::transform(searchText.begin(), searchText.end(), searchText.begin(), toLower);
return itemName.find(searchText) != std::string::npos;
}
bool ProjectPanel::IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath) {
if (currentFolderPath.empty() || folderPath.empty()) {
return false;
}
if (currentFolderPath == folderPath) {
return true;
}
const std::string withForwardSlash = folderPath + "/";
const std::string withBackwardSlash = folderPath + "\\";
return currentFolderPath.rfind(withForwardSlash, 0) == 0 || currentFolderPath.rfind(withBackwardSlash, 0) == 0;
}
}
}