#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/ISceneManager.h" #include "Core/AssetItem.h" #include "Platform/Win32Utf8.h" #include "Utils/ProjectFileUtils.h" #include "UI/UI.h" #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace { constexpr float kProjectToolbarHeight = 26.0f; constexpr float kProjectToolbarPaddingY = 3.0f; std::uint64_t ResolveImportStatusElapsedMs( const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { if (!status.HasValue() || status.startedAtMs == 0) { return 0; } if (!status.inProgress) { return static_cast(status.durationMs); } const auto nowMs = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); return nowMs >= status.startedAtMs ? nowMs - static_cast(status.startedAtMs) : 0; } ImVec4 ResolveImportStatusColor(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { if (!status.HasValue()) { return XCEngine::Editor::UI::ConsoleSecondaryTextColor(); } if (status.inProgress) { return XCEngine::Editor::UI::ConsoleWarningColor(); } return status.success ? XCEngine::Editor::UI::ConsoleSecondaryTextColor() : XCEngine::Editor::UI::ConsoleErrorColor(); } std::uint64_t ResolveSceneLoadElapsedMs(const XCEngine::Editor::SceneLoadProgressSnapshot& status) { if (!status.HasValue() || status.startedAtMs == 0) { return 0; } if (status.inProgress) { const auto nowMs = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); return nowMs >= status.startedAtMs ? nowMs - status.startedAtMs : 0; } if (status.streamingCompletedAtMs >= status.startedAtMs && status.streamingCompletedAtMs != 0) { return status.streamingCompletedAtMs - status.startedAtMs; } if (status.firstFrameAtMs >= status.startedAtMs && status.firstFrameAtMs != 0) { return status.firstFrameAtMs - status.startedAtMs; } if (status.structureReadyAtMs >= status.startedAtMs && status.structureReadyAtMs != 0) { return status.structureReadyAtMs - status.startedAtMs; } return 0; } std::uint64_t ResolveSceneLoadMarkerDeltaMs(std::uint64_t startMs, std::uint64_t endMs) { return endMs >= startMs && startMs != 0 ? endMs - startMs : 0; } ImVec4 ResolveSceneLoadStatusColor(const XCEngine::Editor::SceneLoadProgressSnapshot& status) { if (!status.HasValue()) { return XCEngine::Editor::UI::ConsoleSecondaryTextColor(); } if (status.inProgress) { return XCEngine::Editor::UI::ConsoleWarningColor(); } return status.success ? XCEngine::Editor::UI::ConsoleSecondaryTextColor() : XCEngine::Editor::UI::ConsoleErrorColor(); } std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { if (!status.HasValue()) { return "Library: ready"; } std::string text = "Library: "; text += status.message.CStr(); const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status); if (elapsedMs > 0) { text += " ("; text += std::to_string(elapsedMs); text += " ms)"; } return text; } std::string BuildSceneLoadStatusText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) { if (!status.HasValue()) { return "Scene: ready"; } std::string text = "Scene: "; if (!status.success) { text += status.message.empty() ? "failed" : status.message; return text; } if (status.inProgress) { text += status.message.empty() ? "loading..." : status.message; } else { text += "ready"; } const std::uint64_t elapsedMs = ResolveSceneLoadElapsedMs(status); if (elapsedMs > 0) { text += " ("; text += std::to_string(elapsedMs); text += " ms)"; } return text; } std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { if (!status.HasValue()) { return "No import operations have been recorded in this session."; } std::string tooltip; tooltip += "Operation: "; tooltip += status.operation.CStr(); tooltip += "\nState: "; tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed"); const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status); if (elapsedMs > 0) { tooltip += "\nDuration: "; tooltip += std::to_string(elapsedMs); tooltip += " ms"; } if (!status.targetPath.Empty()) { tooltip += "\nTarget: "; tooltip += status.targetPath.CStr(); } if (status.importedAssetCount > 0) { tooltip += "\nImported: "; tooltip += std::to_string(status.importedAssetCount); } if (status.removedArtifactCount > 0) { tooltip += "\nRemoved orphans: "; tooltip += std::to_string(status.removedArtifactCount); } return tooltip; } std::string BuildSceneLoadStatusTooltipText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) { if (!status.HasValue()) { return "No scene load operations have been recorded in this session."; } std::string tooltip; tooltip += "Operation: "; tooltip += status.operation.empty() ? "Scene Load" : status.operation; if (!status.scenePath.empty()) { tooltip += "\nScene: "; tooltip += status.scenePath; } tooltip += "\nState: "; tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed"); const std::uint64_t structureMs = ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.structureReadyAtMs); if (structureMs > 0) { tooltip += "\nStructure restore: "; tooltip += std::to_string(structureMs); tooltip += " ms"; } const std::uint64_t firstFrameMs = ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.firstFrameAtMs); if (firstFrameMs > 0) { tooltip += "\nFirst viewport frame: "; tooltip += std::to_string(firstFrameMs); tooltip += " ms"; } const std::uint64_t interactiveMs = ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.interactiveAtMs); if (interactiveMs > 0) { tooltip += "\nFirst interactive frame: "; tooltip += std::to_string(interactiveMs); tooltip += " ms"; } const std::uint64_t streamingMs = ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.streamingCompletedAtMs); if (streamingMs > 0) { tooltip += "\nRuntime streaming complete: "; tooltip += std::to_string(streamingMs); tooltip += " ms"; } if (status.peakPendingAsyncLoads > 0) { tooltip += "\nPeak async loads: "; tooltip += std::to_string(status.peakPendingAsyncLoads); } if (status.inProgress && status.currentPendingAsyncLoads > 0) { tooltip += "\nPending async loads: "; tooltip += std::to_string(status.currentPendingAsyncLoads); } if (!status.message.empty()) { tooltip += "\nStatus: "; tooltip += status.message; } return tooltip; } 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; } std::string BuildProjectRelativeAssetPath(const std::string& projectPath, const std::string& fullPath) { if (projectPath.empty() || fullPath.empty()) { return {}; } return ProjectFileUtils::MakeProjectRelativePath(projectPath, fullPath); } bool ShowPathInExplorer(const std::string& fullPath, bool selectTarget) { if (fullPath.empty()) { return false; } namespace fs = std::filesystem; std::error_code ec; const fs::path targetPath = fs::path(Platform::Utf8ToWide(fullPath)).lexically_normal(); if (targetPath.empty() || !fs::exists(targetPath, ec)) { return false; } HINSTANCE result = nullptr; if (selectTarget) { const std::wstring parameters = L"/select,\"" + targetPath.native() + L"\""; const std::wstring workingDirectory = targetPath.parent_path().native(); result = ShellExecuteW( nullptr, L"open", L"explorer.exe", parameters.c_str(), workingDirectory.empty() ? nullptr : workingDirectory.c_str(), SW_SHOWNORMAL); } else { const std::wstring workingDirectory = targetPath.parent_path().native(); result = ShellExecuteW( nullptr, L"open", targetPath.c_str(), nullptr, workingDirectory.empty() ? nullptr : workingDirectory.c_str(), SW_SHOWNORMAL); } return reinterpret_cast(result) > 32; } } // 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(); } ProjectPanel::ContextMenuTarget ProjectPanel::BuildContextMenuTarget( IProjectManager& manager, const AssetItemPtr& item) const { ContextMenuTarget target; target.item = item; if (item) { target.subjectPath = item->fullPath; target.createFolderPath = item->isFolder ? item->fullPath : std::string(); target.showInExplorerSelect = true; return target; } if (const AssetItemPtr currentFolder = manager.GetCurrentFolder()) { target.subjectPath = currentFolder->fullPath; target.createFolderPath = currentFolder->fullPath; } return target; } void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target) { auto* managerPtr = &manager; const bool canCreate = !target.createFolderPath.empty(); const bool canShowInExplorer = !target.subjectPath.empty(); const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item); const bool canInstantiateModel = target.item != nullptr && m_context != nullptr && Commands::CanInstantiateModelAsset(*m_context, target.item); const bool canDelete = target.item != nullptr; const bool canRename = target.item != nullptr; const std::string copyPath = BuildProjectRelativeAssetPath( m_context ? m_context->GetProjectPath() : std::string(), target.subjectPath); const bool canCopyPath = !copyPath.empty(); const auto queueCreateAsset = [this, managerPtr, target](auto createFn) { QueueDeferredAction(m_deferredContextAction, [this, managerPtr, target, createFn]() { if (!target.createFolderPath.empty() && target.item && target.item->isFolder) { managerPtr->NavigateToFolder(target.item); } if (AssetItemPtr createdItem = createFn(*managerPtr)) { BeginRename(createdItem); } }); }; UI::DrawContextSubmenu("Create", [&]() { Actions::DrawMenuAction(Actions::MakeAction("Folder", nullptr, false, canCreate), [&]() { queueCreateAsset([](IProjectManager& createManager) { return Commands::CreateFolder(createManager, "New Folder"); }); }); Actions::DrawMenuAction(Actions::MakeAction("Material", nullptr, false, canCreate), [&]() { queueCreateAsset([](IProjectManager& createManager) { return Commands::CreateMaterial(createManager, "New Material"); }); }); }, canCreate); Actions::DrawMenuAction(Actions::MakeAction("Show in Explore", nullptr, false, canShowInExplorer), [&]() { QueueDeferredAction(m_deferredContextAction, [target]() { ShowPathInExplorer(target.subjectPath, target.showInExplorerSelect); }); }); Actions::DrawMenuAction(Actions::MakeAction("Instantiate In Scene", nullptr, false, canInstantiateModel), [&]() { QueueDeferredAction(m_deferredContextAction, [this, target]() { Commands::InstantiateModelAsset(*m_context, target.item); }); }); Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() { QueueDeferredAction(m_deferredContextAction, [this, target]() { Actions::OpenProjectAsset(*m_context, target.item); }); }); Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(canDelete), [&]() { QueueDeferredAction(m_deferredContextAction, [this, target]() { Commands::DeleteAsset(m_context->GetProjectManager(), target.item); }); }); Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, canRename), [&]() { QueueDeferredAction(m_deferredContextAction, [this, target]() { BeginRename(target.item); }); }); Actions::DrawMenuAction(Actions::MakeAction("Copy Path", nullptr, false, canCopyPath), [copyPath]() { ImGui::SetClipboardText(copyPath.c_str()); }); } 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() { const auto importStatus = ::XCEngine::Resources::ResourceManager::Get().GetProjectAssetImportStatus(); const SceneLoadProgressSnapshot sceneLoadStatus = m_context != nullptr ? m_context->GetSceneManager().GetSceneLoadProgress() : SceneLoadProgressSnapshot{}; const std::string importStatusText = BuildImportStatusText(importStatus); const std::string importStatusTooltip = BuildImportStatusTooltipText(importStatus); const std::string sceneLoadStatusText = BuildSceneLoadStatusText(sceneLoadStatus); const std::string sceneLoadStatusTooltip = BuildSceneLoadStatusTooltipText(sceneLoadStatus); 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", 3, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("##ImportStatus", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("##SceneLoadStatus", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, ResolveImportStatusColor(importStatus)); ImGui::TextUnformatted(importStatusText.c_str()); ImGui::PopStyleColor(); if (ImGui::IsItemHovered() && !importStatusTooltip.empty()) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f); ImGui::TextUnformatted(importStatusTooltip.c_str()); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, ResolveSceneLoadStatusColor(sceneLoadStatus)); ImGui::TextUnformatted(sceneLoadStatusText.c_str()); ImGui::PopStyleColor(); if (ImGui::IsItemHovered() && !sceneLoadStatusTooltip.empty()) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f); ImGui::TextUnformatted(sceneLoadStatusTooltip.c_str()); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } ImGui::TableNextColumn(); UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer)); ImGui::EndTable(); } ImGui::PopStyleVar(); } 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"); } if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) { DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr)); 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) { 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 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; } const int rowCount = visibleItems.empty() ? 0 : (static_cast(visibleItems.size()) + columns - 1) / columns; std::vector rowHeights(static_cast(rowCount), UI::AssetTileSize().y); 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 AssetItemPtr& item = visibleItems[visibleIndex]; const bool isRenaming = item && m_renameState.IsEditing(item->fullPath); UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions(); tileOptions.drawLabel = !isRenaming; const ImVec2 tileSize = UI::ComputeAssetTileSize(GetProjectAssetDisplayName(item).c_str(), tileOptions); const int row = visibleIndex / columns; rowHeights[static_cast(row)] = (std::max)(rowHeights[static_cast(row)], tileSize.y); } std::vector rowOffsets(static_cast(rowCount), gridOrigin.y); float nextRowY = gridOrigin.y; for (int row = 0; row < rowCount; ++row) { rowOffsets[static_cast(row)] = nextRowY; nextRowY += rowHeights[static_cast(row)] + rowSpacing; } int renderedItemCount = 0; 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), rowOffsets[static_cast(row)])); const AssetItemPtr& item = visibleItems[visibleIndex]; const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath); ++renderedItemCount; if (interaction.clicked) { pendingSelection = item; } if (interaction.openRequested) { pendingOpenTarget = item; break; } if (m_deferredContextAction) { break; } } if (renderedItemCount > 0) { const int renderedRowCount = (renderedItemCount + columns - 1) / columns; float contentBottom = gridOrigin.y; for (int row = 0; row < renderedRowCount; ++row) { contentBottom = rowOffsets[static_cast(row)] + rowHeights[static_cast(row)]; } ImGui::SetCursorPos(ImVec2(gridOrigin.x, contentBottom)); ImGui::Dummy(ImVec2(0.0f, 0.0f)); } 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")) { DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr)); 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")) { DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(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; } } }