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

621 lines
21 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 {
namespace {
template <typename Fn>
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
if (!pendingAction) {
pendingAction = std::forward<Fn>(fn);
}
}
void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) {
if (!context.drawList) {
return;
}
const float width = context.max.x - context.min.x;
const float height = context.max.y - context.min.y;
const float iconExtent = UI::NavigationTreeIconSize();
const float minX = context.min.x + (width - iconExtent) * 0.5f;
const float minY = context.min.y + (height - iconExtent) * 0.5f;
UI::DrawAssetIcon(
context.drawList,
ImVec2(minX, minY),
ImVec2(minX + iconExtent, minY + iconExtent),
UI::AssetIconKind::Folder);
}
2026-03-29 01:36:53 +08:00
UI::AssetIconKind ResolveProjectAssetIconKind(const AssetItemPtr& item) {
if (!item) {
return UI::AssetIconKind::File;
}
if (item->isFolder) {
return UI::AssetIconKind::Folder;
}
if (item->type == "Scene") {
return UI::AssetIconKind::Scene;
}
return UI::AssetIconKind::File;
}
std::string GetProjectAssetDisplayName(const AssetItemPtr& item) {
if (!item) {
return {};
}
if (item->isFolder) {
return item->name;
}
const size_t extensionPos = item->name.find_last_of('.');
if (extensionPos == std::string::npos || extensionPos == 0) {
return item->name;
}
return item->name.substr(0, extensionPos);
}
UI::AssetTileOptions MakeProjectAssetTileOptions() {
UI::AssetTileOptions options;
options.drawIdleFrame = false;
options.drawSelectionBorder = false;
options.iconOffset = UI::ProjectAssetTileIconOffset();
options.iconSize = UI::ProjectAssetTileIconSize();
return options;
}
} // namespace
ProjectPanel::ProjectPanel() : Panel("Project") {
}
void ProjectPanel::Initialize(const std::string& projectPath) {
m_context->GetProjectManager().Initialize(projectPath);
}
2026-03-29 01:36:53 +08:00
void ProjectPanel::BeginAssetDragDropFrame() {
m_assetDragDropState.Reset();
if (const char* draggedPath = Actions::GetDraggedProjectAssetPath()) {
m_assetDragDropState.dragging = true;
m_assetDragDropState.sourcePath = draggedPath;
}
}
void ProjectPanel::RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder) {
if (!m_assetDragDropState.dragging || !folder || !folder->isFolder || !ImGui::BeginDragDropTarget()) {
return;
}
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
if (!payload || !payload->IsDataType(Actions::ProjectAssetPayloadType())) {
ImGui::EndDragDropTarget();
return;
}
m_assetDragDropState.hoveredTarget = true;
const bool canDrop = Commands::CanMoveAssetToFolder(manager, m_assetDragDropState.sourcePath, folder);
if (canDrop) {
m_assetDragDropState.hoveredValidTarget = true;
if (const ImGuiPayload* accepted = ImGui::AcceptDragDropPayload(
Actions::ProjectAssetPayloadType(),
ImGuiDragDropFlags_AcceptNoDrawDefaultRect))
{
if (accepted->Delivery) {
m_assetDragDropState.deliveredSourcePath = m_assetDragDropState.sourcePath;
m_assetDragDropState.deliveredTarget = folder;
}
}
}
ImGui::EndDragDropTarget();
}
void ProjectPanel::FinalizeAssetDragDrop(IProjectManager& manager) {
if (m_assetDragDropState.dragging) {
ImGui::SetMouseCursor(
m_assetDragDropState.hoveredValidTarget ? ImGuiMouseCursor_Arrow : ImGuiMouseCursor_NotAllowed);
}
if (!m_assetDragDropState.deliveredSourcePath.empty() && m_assetDragDropState.deliveredTarget) {
Commands::MoveAssetToFolder(
manager,
m_assetDragDropState.deliveredSourcePath,
m_assetDragDropState.deliveredTarget);
}
}
void ProjectPanel::BeginRename(const AssetItemPtr& item) {
if (!item) {
CancelRename();
return;
}
m_renameState.Begin(item->fullPath, GetProjectAssetDisplayName(item).c_str());
}
bool ProjectPanel::CommitRename(IProjectManager& manager) {
if (!m_renameState.IsActive()) {
return false;
}
const std::string sourcePath = m_renameState.Item();
std::string currentDisplayName;
for (const auto& item : manager.GetCurrentItems()) {
if (item && item->fullPath == sourcePath) {
currentDisplayName = GetProjectAssetDisplayName(item);
break;
}
}
if (!currentDisplayName.empty() && currentDisplayName == m_renameState.Buffer()) {
CancelRename();
return true;
}
if (!Commands::RenameAsset(manager, sourcePath, m_renameState.Buffer())) {
return false;
}
CancelRename();
return true;
}
void ProjectPanel::CancelRename() {
m_renameState.Cancel();
}
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();
2026-03-29 01:36:53 +08:00
BeginAssetDragDropFrame();
m_deferredContextAction = {};
RenderToolbar();
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
UI::PanelContentScope content("ProjectContent", ImVec2(0.0f, 0.0f));
if (!content.IsOpen()) {
ImGui::PopStyleColor();
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);
2026-03-29 01:36:53 +08:00
FinalizeAssetDragDrop(manager);
ImGui::PopStyleColor();
if (m_deferredContextAction) {
auto deferredAction = std::move(m_deferredContextAction);
m_deferredContextAction = {};
deferredAction();
}
}
void ProjectPanel::RenderToolbar() {
UI::PanelToolbarScope toolbar(
"ProjectToolbar",
UI::ProjectPanelToolbarHeight(),
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
true,
UI::ToolbarPadding(),
UI::ToolbarItemSpacing(),
UI::ProjectPanelToolbarBackgroundColor());
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) {
auto* managerPtr = &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();
UI::ResetTreeLayout();
if (rootFolder) {
RenderFolderTreeNode(manager, rootFolder, currentFolderPath);
} else {
UI::DrawEmptyState("No Assets Folder");
}
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
UI::EndContextMenu();
}
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;
}
}
UI::TreeNodeDefinition nodeDefinition;
nodeDefinition.options.selected = folder->fullPath == currentFolderPath;
nodeDefinition.options.leaf = !hasChildFolders;
nodeDefinition.options.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath);
nodeDefinition.persistenceKey = folder->fullPath;
nodeDefinition.style = UI::ProjectFolderTreeStyle();
nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth();
nodeDefinition.prefix.draw = DrawProjectFolderTreePrefix;
nodeDefinition.callbacks.onInteraction = [this, &manager, folder](const UI::TreeNodeResult& node) {
if (node.clicked) {
manager.NavigateToFolder(folder);
}
};
2026-03-27 22:05:05 +08:00
const UI::TreeNodeResult node = UI::DrawTreeNode(
&m_folderTreeState,
2026-03-27 22:05:05 +08:00
(void*)folder.get(),
folder->name.c_str(),
nodeDefinition);
2026-03-29 01:36:53 +08:00
RegisterFolderDropTarget(manager, 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) {
auto* managerPtr = &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;
2026-03-29 01:36:53 +08:00
if (m_renameState.IsActive() && manager.FindCurrentItemIndex(m_renameState.Item()) < 0) {
CancelRename();
}
visibleItems.reserve(items.size());
for (const auto& item : items) {
2026-03-29 01:36:53 +08:00
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, search)) {
visibleItems.push_back(item);
}
}
RenderBrowserHeader(manager);
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
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);
ImGui::PopStyleColor();
if (!bodyOpen) {
ImGui::EndChild();
ImGui::EndChild();
return;
}
2026-03-26 21:18:33 +08:00
const float tileWidth = UI::AssetTileSize().x;
const float tileHeight = UI::AssetTileSize().y;
2026-03-26 21:18:33 +08:00
const float spacing = UI::AssetGridSpacing().x;
const float rowSpacing = UI::AssetGridSpacing().y;
2026-03-26 21:18:33 +08:00
const float panelWidth = ImGui::GetContentRegionAvail().x;
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
if (columns < 1) {
columns = 1;
}
AssetItemPtr pendingSelection;
AssetItemPtr pendingOpenTarget;
const std::string selectedItemPath = manager.GetSelectedItemPath();
const ImVec2 gridOrigin = ImGui::GetCursorPos();
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
const int column = visibleIndex % columns;
const int row = visibleIndex / columns;
ImGui::SetCursorPos(ImVec2(
gridOrigin.x + column * (tileWidth + spacing),
gridOrigin.y + row * (tileHeight + rowSpacing)));
const AssetItemPtr& item = visibleItems[visibleIndex];
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
if (interaction.clicked) {
pendingSelection = item;
}
if (interaction.openRequested) {
pendingOpenTarget = item;
break;
}
if (m_deferredContextAction) {
break;
}
}
if (!visibleItems.empty()) {
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
}
if (visibleItems.empty() && !search.empty()) {
2026-03-26 21:18:33 +08:00
UI::DrawEmptyState(
"No Search Results",
"No assets match the current search");
2026-03-26 21:18:33 +08:00
}
if (!m_deferredContextAction) {
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
if (pendingSelection) {
manager.SetSelectedItem(pendingSelection);
}
if (pendingOpenTarget) {
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
}
}
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
if (manager.CanNavigateBack()) {
Actions::DrawMenuSeparator();
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
managerPtr->NavigateBack();
});
});
2026-03-29 01:36:53 +08:00
}
UI::EndContextMenu();
}
2026-03-26 23:52:05 +08:00
ImGui::EndChild();
ImGui::EndChild();
}
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
auto* managerPtr = &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;
}
const float rowHeight = UI::BreadcrumbItemHeight();
const float startY = ImGui::GetCursorPosY();
const float availableHeight = ImGui::GetContentRegionAvail().y;
if (availableHeight > rowHeight) {
2026-03-29 01:36:53 +08:00
ImGui::SetCursorPosY(startY + (availableHeight - rowHeight) * 0.5f - 1.0f);
}
UI::DrawToolbarBreadcrumbs(
"Assets",
manager.GetPathDepth(),
[&](size_t index) { return manager.GetPathName(index); },
[&](size_t index) {
QueueDeferredAction(m_deferredContextAction, [managerPtr, index]() {
managerPtr->NavigateToIndex(index);
});
});
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 windowMin = ImGui::GetWindowPos();
const ImVec2 windowMax(windowMin.x + ImGui::GetWindowSize().x, windowMin.y + ImGui::GetWindowSize().y);
UI::DrawHorizontalDivider(drawList, windowMin.x, windowMax.x, windowMax.y - 0.5f);
ImGui::EndChild();
}
ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItemPtr& item, bool isSelected) {
AssetItemInteraction interaction;
ImGui::PushID(item ? item->fullPath.c_str() : "ProjectItem");
2026-03-29 01:36:53 +08:00
const bool isRenaming = item && m_renameState.IsEditing(item->fullPath);
const bool isDraggingThisItem = !isRenaming && Actions::IsProjectAssetBeingDragged(item);
const UI::AssetIconKind iconKind = ResolveProjectAssetIconKind(item);
const std::string displayName = GetProjectAssetDisplayName(item);
UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions();
tileOptions.drawLabel = !isRenaming;
2026-03-26 21:18:33 +08:00
const UI::AssetTileResult tile = UI::DrawAssetTile(
2026-03-29 01:36:53 +08:00
displayName.c_str(),
2026-03-26 21:18:33 +08:00
isSelected,
isDraggingThisItem,
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
2026-03-30 01:46:49 +08:00
if (item && item->canUseImagePreview &&
2026-03-29 01:36:53 +08:00
UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, item->fullPath)) {
return;
}
2026-03-26 21:18:33 +08:00
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
},
tileOptions);
const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right);
2026-03-26 21:18:33 +08:00
2026-03-29 01:36:53 +08:00
if (isRenaming) {
const float renameWidth = tile.labelMax.x - tile.labelMin.x;
const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f);
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt(
2026-03-29 01:36:53 +08:00
"##Rename",
ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY),
2026-03-29 01:36:53 +08:00
m_renameState.Buffer(),
m_renameState.BufferSize(),
renameWidth,
m_renameState.ConsumeFocusRequest());
2026-03-29 01:36:53 +08:00
if (renameField.cancelRequested) {
2026-03-29 01:36:53 +08:00
CancelRename();
} else if (renameField.submitted || renameField.deactivated) {
2026-03-29 01:36:53 +08:00
CommitRename(m_context->GetProjectManager());
}
} else {
if (tile.clicked) {
interaction.clicked = true;
}
if (secondaryClicked && item) {
m_context->GetProjectManager().SetSelectedItem(item);
2026-03-29 01:36:53 +08:00
}
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
Actions::BeginProjectAssetDrag(item, iconKind);
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Actions::OpenProjectAsset(*m_context, item);
});
});
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
BeginRename(item);
});
});
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Commands::DeleteAsset(m_context->GetProjectManager(), item);
});
});
UI::EndContextMenu();
}
2026-03-29 01:36:53 +08:00
if (tile.openRequested) {
interaction.openRequested = true;
}
}
2026-03-29 01:36:53 +08:00
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;
}
}
}