#include "Actions/ActionRouting.h" #include "Actions/ProjectActionRouter.h" #include "Commands/ProjectCommands.h" #include "ProjectPanel.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" #include "Core/AssetItem.h" #include "UI/UI.h" #include #include #include 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; } 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() { 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(); 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; } } UI::TreeNodeOptions nodeOptions; nodeOptions.selected = folder->fullPath == currentFolderPath; nodeOptions.leaf = !hasChildFolders; nodeOptions.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath); const UI::TreeNodeResult node = UI::DrawTreeNode( (void*)folder.get(), folder->name.c_str(), nodeOptions); if (node.clicked) { manager.NavigateToFolder(folder); } if (node.open) { for (const auto& child : folder->children) { if (!child || !child->isFolder) { continue; } RenderFolderTreeNode(manager, child, currentFolderPath); } 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 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; } const float tileWidth = UI::AssetTileSize().x; const float spacing = UI::AssetGridSpacing().x; const float panelWidth = ImGui::GetContentRegionAvail().x; int columns = static_cast((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(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()) { UI::DrawEmptyState( search.empty() ? "No Assets" : "No Search Results", search.empty() ? "Current folder is empty" : "No assets match the current search"); } 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); 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); 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); }); if (tile.clicked) { interaction.clicked = true; } if (tile.contextRequested) { interaction.contextRequested = true; } interaction.droppedSourcePath = Actions::AcceptProjectAssetDropPayload(item); Actions::BeginProjectAssetDrag(item, iconKind); 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(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; } } }