#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 { namespace { 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); } 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); } 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; } Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project); auto& manager = m_context->GetProjectManager(); BeginAssetDragDropFrame(); 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); FinalizeAssetDragDrop(manager); ImGui::PopStyleColor(); } 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(); 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(); UI::ResetTreeLayout(); 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::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); } if (node.secondaryClicked) { Actions::HandleProjectItemContextRequest(manager, folder, m_itemContextMenu); } }; const UI::TreeNodeResult node = UI::DrawTreeNode( &m_folderTreeState, (void*)folder.get(), folder->name.c_str(), nodeDefinition); RegisterFolderDropTarget(manager, 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; if (m_renameState.IsActive() && manager.FindCurrentItemIndex(m_renameState.Item()) < 0) { CancelRename(); } visibleItems.reserve(items.size()); for (const auto& item : items) { 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; } 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; 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.openRequested) { pendingOpenTarget = item; break; } } if (visibleItems.empty() && !search.empty()) { UI::DrawEmptyState( "No Search Results", "No assets match the current search"); } Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState); if (pendingSelection) { manager.SetSelectedItem(pendingSelection); } if (pendingContextTarget) { Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu); } if (pendingOpenTarget) { Actions::OpenProjectAsset(*m_context, pendingOpenTarget); } Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu); Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu); Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, [&]() { if (AssetItemPtr createdFolder = Commands::CreateFolder(manager, "New Folder")) { BeginRename(createdFolder); } }); 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; } const float rowHeight = UI::BreadcrumbItemHeight(); const float startY = ImGui::GetCursorPosY(); const float availableHeight = ImGui::GetContentRegionAvail().y; if (availableHeight > rowHeight) { ImGui::SetCursorPosY(startY + (availableHeight - rowHeight) * 0.5f - 1.0f); } UI::DrawToolbarBreadcrumbs( "Assets", manager.GetPathDepth(), [&](size_t index) { return manager.GetPathName(index); }, [&](size_t index) { manager.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"); 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; const UI::AssetTileResult tile = UI::DrawAssetTile( displayName.c_str(), isSelected, isDraggingThisItem, [&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) { if (item && item->type == "Texture" && UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, item->fullPath)) { return; } UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind); }, tileOptions); if (isRenaming) { const ImVec2 restoreCursor = ImGui::GetCursorPos(); ImGui::SetCursorScreenPos(tile.labelMin); ImGui::SetNextItemWidth(tile.labelMax.x - tile.labelMin.x); if (m_renameState.ConsumeFocusRequest()) { ImGui::SetKeyboardFocusHere(); } const bool submitted = ImGui::InputText( "##Rename", m_renameState.Buffer(), m_renameState.BufferSize(), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll); const bool cancelRequested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); const bool deactivated = ImGui::IsItemDeactivated(); ImGui::SetCursorPos(restoreCursor); if (cancelRequested) { CancelRename(); } else if (submitted || deactivated) { CommitRename(m_context->GetProjectManager()); } } else { if (tile.clicked) { interaction.clicked = true; } if (tile.contextRequested) { interaction.contextRequested = true; } RegisterFolderDropTarget(m_context->GetProjectManager(), 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; } } }