#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 { constexpr float kProjectToolbarHeight = 26.0f; constexpr float kProjectToolbarPaddingY = 3.0f; template void QueueDeferredAction(std::function& pendingAction, Fn&& fn) { if (!pendingAction) { pendingAction = std::forward(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); } 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(); 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); FinalizeAssetDragDrop(manager); ImGui::PopStyleColor(); if (m_deferredContextAction) { auto deferredAction = std::move(m_deferredContextAction); m_deferredContextAction = {}; deferredAction(); } } void ProjectPanel::RenderToolbar() { UI::PanelToolbarScope toolbar( "ProjectToolbar", kProjectToolbarHeight, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse, true, ImVec2(UI::ToolbarPadding().x, kProjectToolbarPaddingY), UI::ToolbarItemSpacing(), UI::ProjectPanelToolbarBackgroundColor()); if (!toolbar.IsOpen()) { return; } ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); 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(); } ImGui::PopStyleVar(); } 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); } }; 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) { 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 visibleItems; const auto& items = manager.GetCurrentItems(); const UI::SearchQuery searchQuery(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, searchQuery)) { 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), ImGuiChildFlags_AlwaysUseWindowPadding); ImGui::PopStyleVar(2); ImGui::PopStyleColor(); if (!bodyOpen) { ImGui::EndChild(); ImGui::EndChild(); return; } const float tileWidth = UI::AssetTileSize().x; const float tileHeight = UI::AssetTileSize().y; const float spacing = UI::AssetGridSpacing().x; const float rowSpacing = UI::AssetGridSpacing().y; const float panelWidth = ImGui::GetContentRegionAvail().x; int columns = static_cast((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(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(visibleItems.size()) + columns - 1) / columns; ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing); } if (visibleItems.empty() && !searchQuery.Empty()) { UI::DrawEmptyState( "No Search Results", "No assets match the current search"); } 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(); }); }); } UI::EndContextMenu(); } 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) { 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"); 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->canUseImagePreview && UI::DrawTextureAssetPreview( drawList, iconMin, iconMax, item->fullPath, m_context ? m_context->GetProjectPath() : std::string())) { return; } UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind); }, tileOptions); const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right); 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( "##Rename", ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY), m_renameState.Buffer(), m_renameState.BufferSize(), renameWidth, m_renameState.ConsumeFocusRequest()); if (renameField.cancelRequested) { CancelRename(); } else if (renameField.submitted || renameField.deactivated) { CommitRename(m_context->GetProjectManager()); } } else { if (tile.clicked) { interaction.clicked = true; } if (secondaryClicked && item) { m_context->GetProjectManager().SetSelectedItem(item); } 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(); } if (tile.openRequested) { interaction.openRequested = true; } } ImGui::PopID(); return interaction; } bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery) { if (!item) { return false; } if (searchQuery.Empty()) { return true; } return searchQuery.Matches(item->name); } 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; } } }