diff --git a/common/Path.cpp b/common/Path.cpp index f6585a125..e2cac85db 100644 --- a/common/Path.cpp +++ b/common/Path.cpp @@ -13,6 +13,7 @@ /////////////////////////////////////////////////////////////////////////////// #include "Path.hpp" +#include namespace nexo { @@ -53,4 +54,16 @@ namespace nexo { size_t end = s.find_last_not_of('/'); return s.substr(start, end - start + 1); } + + std::vector splitPath(const std::filesystem::path& path) + { + std::vector result; + + for (const auto& part : path) { + if (part != path.root_name() && part != path.root_directory()) + result.push_back(part.string()); + } + + return result; + } } diff --git a/common/Path.hpp b/common/Path.hpp index 636fd7682..b1ed4246d 100644 --- a/common/Path.hpp +++ b/common/Path.hpp @@ -53,4 +53,6 @@ namespace nexo { }; std::string normalizePathAndRemovePrefixSlash(const std::string &rawPath); + + std::vector splitPath(const std::filesystem::path& path); } // namespace nexo diff --git a/common/String.hpp b/common/String.hpp index 810010dab..6d96d18b8 100644 --- a/common/String.hpp +++ b/common/String.hpp @@ -15,10 +15,9 @@ #pragma once #include -#include +#include namespace nexo { - /** * @brief Compare two strings case-insensitively. * diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index bafafa3cd..921a6c546 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -56,6 +56,11 @@ set(SRCS editor/src/DocumentWindows/AssetManager/Update.cpp editor/src/DocumentWindows/AssetManager/FolderTree.cpp editor/src/DocumentWindows/AssetManager/FileDrop.cpp + editor/src/DocumentWindows/AssetManager/FolderCreation.cpp + editor/src/DocumentWindows/AssetManager/Selection.cpp + editor/src/DocumentWindows/AssetManager/AssetGrid.cpp + editor/src/DocumentWindows/AssetManager/Utils.cpp + editor/src/DocumentWindows/AssetManager/FolderManager.cpp editor/src/DocumentWindows/ConsoleWindow/Init.cpp editor/src/DocumentWindows/ConsoleWindow/Log.cpp editor/src/DocumentWindows/ConsoleWindow/Show.cpp diff --git a/editor/src/DocumentWindows/AssetManager/AssetGrid.cpp b/editor/src/DocumentWindows/AssetManager/AssetGrid.cpp new file mode 100644 index 000000000..f49b36ca8 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/AssetGrid.cpp @@ -0,0 +1,362 @@ +//// AssetGrid.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 27/07/2025 +// Description: Source file for the asset grid +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" +#include "assets/Asset.hpp" +#include "assets/AssetCatalog.hpp" +#include "assets/AssetRef.hpp" +#include "context/ThumbnailCache.hpp" +#include "ImNexo/Elements.hpp" + +namespace nexo::editor { + + static constexpr ImU32 getAssetTypeOverlayColor(const assets::AssetType type) + { + switch (type) { + case assets::AssetType::TEXTURE: return IM_COL32(60, 40, 40, 255); + case assets::AssetType::MODEL: return IM_COL32(40, 60, 40, 255); + case assets::AssetType::MATERIAL: return IM_COL32(40, 40, 60, 255); + case assets::AssetType::UNKNOWN: + case assets::AssetType::FONT: + case assets::AssetType::MUSIC: + case assets::AssetType::SCRIPT: + case assets::AssetType::SHADER: + case assets::AssetType::SOUND: + default: return IM_COL32(0, 0, 0, 0); + } + } + + static void calculateGridLayout(LayoutSettings &layout) + { + const float availWidth = ImGui::GetContentRegionAvail().x; + + layout.size.columnCount = std::max( + static_cast(availWidth / layout.size.itemStep.x), 1 + ); + layout.size.itemSize = ImVec2( + layout.size.iconSize + ImGui::GetFontSize() * 1.5f, // width + layout.size.iconSize + ImGui::GetFontSize() * 1.7f // height + ); + layout.size.itemStep = ImVec2( + layout.size.itemSize.x + static_cast(layout.size.iconSpacing), + layout.size.itemSize.y + static_cast(layout.size.iconSpacing) + ); + } + + static void drawAssetThumbnail( + const assets::GenericAssetRef& asset, + const LayoutSettings &layout, + const AssetLayoutParams& params, + const bool isSelected + ) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const ImU32 bgColor = isSelected ? layout.color.selectedBoxColor : layout.color.thumbnailBg; + ImNexo::ButtonBorder(bgColor, bgColor, bgColor); + + if (const ImTextureID textureId = ThumbnailCache::getInstance().getThumbnail(asset); !textureId) { + drawList->AddRectFilled(params.itemPos, params.thumbnailEnd, layout.color.thumbnailBg); + } else { + constexpr float padding = 4.0f; + const ImVec2 imageStart(params.itemPos.x + padding, params.itemPos.y + padding); + const ImVec2 imageEnd(params.thumbnailEnd.x - padding, params.thumbnailEnd.y - padding); + + drawList->AddImage( + textureId, + imageStart, + imageEnd, + ImVec2(0, 1), + ImVec2(1, 0), + IM_COL32(255, 255, 255, 255) + ); + } + } + + static void cropText(const std::string& assetName, std::string& displayText, float availableTextWidth) + { + const std::string ellipsis = "..."; + if (ImGui::CalcTextSize(assetName.c_str()).x <= availableTextWidth) { + displayText = assetName; + return; + } + + for (size_t length = assetName.size(); length > 0; --length) { + displayText = assetName.substr(0, length) + ellipsis; + if (ImGui::CalcTextSize(displayText.c_str()).x <= availableTextWidth) + return; + } + + displayText = ellipsis; + } + + void AssetManagerWindow::drawAssetTitle( + const std::shared_ptr& assetData, + const AssetLayoutParams& params, + bool isHovered + ) const { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float titleAreaHeight = params.itemSize.y * (1.0f - GridLayoutSizes::THUMBNAIL_HEIGHT_RATIO); + const float titlePadding = std::max(2.0f, titleAreaHeight * 0.1f); + const float availableTextWidth = params.itemSize.x - (titlePadding * 2); + + ImU32 titleBgColor = isHovered ? + m_layout.color.titleBgHovered : + getAssetTypeOverlayColor(assetData->getType()); + + // title background + drawList->AddRectFilled( + ImVec2(params.itemPos.x, params.thumbnailEnd.y), + ImVec2(params.itemEnd.x, params.itemEnd.y), + titleBgColor + ); + + const std::string assetName = assetData->getMetadata().location.getName().data(); + const ImVec2 fullTextSize = ImGui::CalcTextSize(assetName.c_str()); + std::string displayText = assetName; + + // Crop text if it's too wide + if (fullTextSize.x > availableTextWidth) + cropText(assetName, displayText, availableTextWidth); + + const ImVec2 displayTextSize = ImGui::CalcTextSize(displayText.c_str()); + const ImVec2 textPos( + params.itemPos.x + (params.itemSize.x - displayTextSize.x) * 0.5f, + params.thumbnailEnd.y + (titleAreaHeight - displayTextSize.y) * 0.5f + ); + drawList->AddText(textPos, m_layout.color.titleText, displayText.c_str()); + + if (isHovered) { + if (fullTextSize.x > availableTextWidth) + ImGui::SetTooltip("%s\n%s", assetName.c_str(), assetData->getMetadata().location.getFullLocation().c_str()); + else + ImGui::SetTooltip("%s", assetData->getMetadata().location.getFullLocation().c_str()); + } + } + + void AssetManagerWindow::drawAsset( + const assets::GenericAssetRef& asset, + const unsigned int index, + const ImVec2& itemPos, + const ImVec2& itemSize + ) { + const auto assetData = asset.lock(); + if (!assetData) + return; + + ImGui::PushID(static_cast(index)); + ImGui::SetCursorScreenPos(itemPos); + + const bool clicked = ImGui::InvisibleButton("##item", itemSize); + const bool isHovered = ImGui::IsItemHovered(); + const bool isSelected = m_selectedAssets.contains(index); + const auto itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); + const auto thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * GridLayoutSizes::THUMBNAIL_HEIGHT_RATIO); + const AssetLayoutParams assetLayoutParams{itemPos, itemSize, itemEnd, thumbnailEnd}; + + drawAssetThumbnail(asset, m_layout, assetLayoutParams, isSelected); + drawAssetTitle(assetData, assetLayoutParams, isHovered); + + if (clicked) + handleSelection(index, isSelected); + + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + AssetDragDropPayload payload; + payload.type = assetData->getType(); + payload.id = assetData->getID(); + const auto& fullPath = assetData->getMetadata().location.getFullLocation(); + const auto& name = assetData->getMetadata().location.getName().data(); + std::strncpy(payload.path, fullPath.c_str(), sizeof(payload.path) - 1); + payload.path[sizeof(payload.path) - 1] = '\0'; + std::strncpy(payload.name, name.c_str(), sizeof(payload.name) - 1); + payload.name[sizeof(payload.name) - 1] = '\0'; + + ImGui::SetDragDropPayload("ASSET_DRAG", &payload, sizeof(payload)); + ImTextureID textureID = ThumbnailCache::getInstance().getThumbnail(asset); + if (textureID) + ImGui::Image(textureID, {64, 64}, ImVec2(0, 1), ImVec2(1, 0)); + + ImGui::EndDragDropSource(); + } + + ImGui::PopID(); + } + + void AssetManagerWindow::drawFolderIcon(const AssetLayoutParams& params) const + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + constexpr float padding = 10.0f; + + const float availWidth = params.thumbnailEnd.x - params.itemPos.x - (padding * 2.0f); + const float availHeight = params.thumbnailEnd.y - params.itemPos.y - (padding * 2.0f); + + const float displaySize = std::min(availWidth, availHeight); + + const float xOffset = (availWidth - displaySize) * 0.5f + padding; + const float yOffset = (availHeight - displaySize) * 0.5f + padding; + + const ImVec2 imageStart( + params.itemPos.x + xOffset, + params.itemPos.y + yOffset + ); + const ImVec2 imageEnd( + imageStart.x + displaySize, + imageStart.y + displaySize + ); + + if (const ImTextureID folderIconTexture = getIconTexture(m_folderIcon)) { + drawList->AddImage( + folderIconTexture, + imageStart, + imageEnd, + ImVec2(0, 1), // UV0 (top-left) + ImVec2(1, 0), // UV1 (bottom-right) + IM_COL32(255, 255, 255, 255) // White tint for default color + ); + } + } + + static void drawFolderTitle( + const std::string& folderName, + const LayoutSettings& layout, + const AssetLayoutParams& params, + bool isHovered + ) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const ImU32 titleBgColor = isHovered ? layout.color.titleBgHovered : IM_COL32(0, 0, 0, 0); + const float titleAreaHeight = params.itemSize.y * (1.0f - GridLayoutSizes::THUMBNAIL_HEIGHT_RATIO); + + drawList->AddRectFilled( + ImVec2(params.itemPos.x, params.thumbnailEnd.y), + ImVec2(params.itemEnd.x, params.itemEnd.y), + titleBgColor + ); + + const ImVec2 textSize = ImGui::CalcTextSize(folderName.c_str()); + const float textY = params.thumbnailEnd.y + ((titleAreaHeight - textSize.y) * 0.5f); + const float textX = params.itemPos.x + (params.itemSize.x - textSize.x) * 0.5f; + + drawList->AddText( + ImVec2(textX, textY), + layout.color.titleText, + folderName.c_str() + ); + } + + void AssetManagerWindow::drawFolder( + const std::string& folderPath, + const std::string& folderName, + const ImVec2& itemPos, + const ImVec2& itemSize + ) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const auto itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); + const auto thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * GridLayoutSizes::THUMBNAIL_HEIGHT_RATIO); + const AssetLayoutParams folderLayoutParams{itemPos, itemSize, itemEnd, thumbnailEnd}; + + ImGui::PushID(("folder_" + folderPath).c_str()); + ImGui::SetCursorScreenPos(itemPos); + + const bool clicked = ImGui::InvisibleButton("##folder", itemSize); + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + m_hoveredFolder = folderPath; + else if (m_hoveredFolder == folderPath) + m_hoveredFolder.clear(); + + handleAssetDrop(folderPath); + + const ImU32 bgColor = isHovered ? m_layout.color.thumbnailBgHovered : IM_COL32(0, 0, 0, 0); + drawList->AddRectFilled(itemPos, itemEnd, bgColor, GridLayoutSizes::CORNER_RADIUS); + drawFolderIcon(folderLayoutParams); + drawFolderTitle(folderName, m_layout, folderLayoutParams, isHovered); + + if (clicked) + m_currentFolder = folderPath; + + ImGui::PopID(); + } + + static std::vector getFilteredAsset(std::string_view currentFolder, const assets::AssetType selectedType) + { + std::vector filtered; + + for (const auto& ref : assets::AssetCatalog::getInstance().getAssets()) { + const auto d = ref.lock(); + if (!d) + continue; + const auto& folder = d->getMetadata().location.getPath(); + if (folder == INTERNAL_FOLDER_PREFIX) + continue; + if (selectedType != assets::AssetType::UNKNOWN && d->getType() != selectedType) + continue; + if (folder == currentFolder) + filtered.push_back(ref); + } + + return filtered; + } + + void AssetManagerWindow::drawAssetsGrid() + { + calculateGridLayout(m_layout); + + const ImVec2 startPos = ImGui::GetCursorScreenPos(); + auto subfolders = m_folderManager.getChildren(m_currentFolder); + const std::vector filtered = getFilteredAsset(m_currentFolder, m_selectedType); + + const size_t totalItems = subfolders.size() + filtered.size(); + const int columnCount = m_layout.size.columnCount; + const auto rows = static_cast((totalItems + columnCount - 1) / columnCount); + + ImGuiListClipper clipper; + clipper.Begin(rows, m_layout.size.itemStep.y); + + while (clipper.Step()) { + unsigned int visibleStart = clipper.DisplayStart * columnCount; + unsigned int visibleEnd = std::min(clipper.DisplayEnd * columnCount, static_cast(totalItems)); + + for (unsigned int i = visibleStart; i < visibleEnd; ++i) { + unsigned int col = i % columnCount; + unsigned int row = i / columnCount; + ImVec2 itemPos{ + startPos.x + static_cast(col) * m_layout.size.itemStep.x, + startPos.y + static_cast(row) * m_layout.size.itemStep.y + }; + + if (i < subfolders.size()) { + drawFolder( + subfolders[i].first, + subfolders[i].second, + itemPos, + m_layout.size.itemSize + ); + } else { + const auto assetIdx = i - static_cast(subfolders.size()); + drawAsset( + filtered[assetIdx], + assetIdx, + itemPos, + m_layout.size.itemSize + ); + } + } + } + clipper.End(); + } +} diff --git a/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp index 16e7ac7c0..242f16cb6 100644 --- a/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp +++ b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp @@ -17,12 +17,88 @@ #include #include #include +#include "DocumentWindows/PopupManager.hpp" #include "utils/TransparentStringHash.hpp" #include #include "assets/Asset.hpp" +#include "FolderManager.hpp" namespace nexo::editor { + static constexpr std::string_view INTERNAL_FOLDER_PREFIX = "_internal"; + static constexpr float ERROR_DISPLAY_TIMEOUT = 3.0f; + + struct FolderCreationState { + bool isCreatingFolder = false; + std::string folderName = "New Folder"; + std::string parentPath; + bool showError = false; + std::string errorMessage; + float errorTimer = ERROR_DISPLAY_TIMEOUT; + + void reset() + { + isCreatingFolder = false; + folderName = "New Folder"; + parentPath = ""; + showError = false; + errorMessage = ""; + errorTimer = 3.0f; + } + }; + + struct GridLayoutSizes { + float iconSize = 64.0f; + int iconSpacing = 8; + + ImVec2 itemSize; + ImVec2 itemStep; + int columnCount; + + static constexpr float THUMBNAIL_HEIGHT_RATIO = 0.8f; + static constexpr float TITLE_PADDING = 5.0f; + static constexpr float OVERLAY_SIZE = 24.0f; + static constexpr float OVERLAY_PADDING = 5.0f; + static constexpr float CORNER_RADIUS = 5.0f; + static constexpr float SELECTED_BOX_THICKNESS = 4.0f; + }; + + struct LayoutColors { + ImU32 thumbnailBg; + ImU32 thumbnailBgHovered; + ImU32 thumbnailBgSelected; + ImU32 thumbnailBgSelectedHovered; + + ImU32 selectedBoxColor; + + ImU32 titleBg; + ImU32 titleBgHovered; + ImU32 titleBgSelected; + ImU32 titleBgSelectedHovered; + + ImU32 titleText; + }; + + struct LayoutSettings { + GridLayoutSizes size; + LayoutColors color; + + float leftPanelWidth = 200.0f; + }; + + struct AssetTypeInfo { + ImTextureID iconTexture; + ImU32 backgroundColor; + std::string tooltip; + }; + + struct AssetLayoutParams { + ImVec2 itemPos; + ImVec2 itemSize; + ImVec2 itemEnd; + ImVec2 thumbnailEnd; + }; + class AssetManagerWindow final : public ADocumentWindow, LISTENS_TO(event::EventFileDrop) { public: using ADocumentWindow::ADocumentWindow; @@ -35,73 +111,44 @@ namespace nexo::editor { void handleEvent(event::EventFileDrop& event) override; private: - struct LayoutSettings { - struct LayoutSizes { - float iconSize = 64.0f; - int iconSpacing = 8; - ImVec2 itemSize; - ImVec2 itemStep; - int columnCount; - float thumbnailHeightRatio = 0.8f; - float titlePadding = 5.0f; - float overlaySize = 6.0f; - float overlayPadding = 5.0f; - float cornerRadius = 5.0f; - float selectedBoxThickness = 4.0f; - } size; - - struct LayoutColors { - ImU32 thumbnailBg; - ImU32 thumbnailBgHovered; - ImU32 thumbnailBgSelected; - ImU32 thumbnailBgSelectedHovered; - - ImU32 selectedBoxColor; - - ImU32 titleBg; - ImU32 titleBgHovered; - ImU32 titleBgSelected; - ImU32 titleBgSelectedHovered; - - ImU32 titleText; - } color; - }; - std::set m_selectedAssets; - std::unordered_map, TransparentStringHash, std::equal_to<>> m_folderChildren; + LayoutSettings m_layout; - void calculateLayout(float availWidth); void drawMenuBar(); + void drawPanelSplitter(); + void drawBreadcrumbs(); + void drawAssetsGrid(); + void drawAssetTitle( + const std::shared_ptr& assetData, + const AssetLayoutParams& params, + bool isHovered + ) const; void drawAsset(const assets::GenericAssetRef& asset, unsigned int index, const ImVec2& itemPos, const ImVec2& itemSize); void handleSelection(unsigned int index, bool isSelected); assets::AssetType m_selectedType = assets::AssetType::UNKNOWN; std::string m_currentFolder; // Currently selected folder std::string m_hoveredFolder; // Currently hovered folder - std::vector> m_folderStructure; // Pairs of (path, name) - char m_searchBuffer[256] = ""; + std::string m_searchBuffer = ""; + + PopupManager m_popupManager; void buildFolderStructure(); + + void folderTreeContextMenu(); void drawFolderTree(); void drawFolderTreeItem(const std::string& name, const std::string& path); - struct FolderCreationState { - bool isCreatingFolder = false; - char folderName[256] = ""; - std::string parentPath; - bool showError = false; - std::string errorMessage; - float errorTimer = 3.0f; - }; - FolderCreationState m_folderCreationState; assets::AssetRef m_folderIcon; - ImTextureID getFolderIconTexture() const; + ImTextureID getIconTexture(const assets::AssetRef &texture) const; - void handleNewFolderCreation(); + void newFolderMenu(); + bool handleNewFolderCreation(); + void drawFolderIcon(const AssetLayoutParams& params) const; void drawFolder( const std::string& folderPath, const std::string& folderName, @@ -110,11 +157,13 @@ namespace nexo::editor { ); std::vector m_pendingDroppedFiles; - bool m_showDropIndicator = false; void handleDroppedFiles(); + void handleAssetDrop(const std::string &path) const; assets::AssetLocation getAssetLocation(const std::filesystem::path &path) const; void importDroppedFile(const std::string& filePath) const; + + FolderManager m_folderManager; }; /** @@ -126,7 +175,7 @@ namespace nexo::editor { { assets::AssetType type; ///< Type of the asset assets::AssetID id; ///< ID of the asset - std::string path; ///< Path to the asset - std::string name; ///< Display name of the asset + char path[256]; ///< Path to the asset + char name[64]; ///< Display name of the asset }; } diff --git a/editor/src/DocumentWindows/AssetManager/FileDrop.cpp b/editor/src/DocumentWindows/AssetManager/FileDrop.cpp index a170f742f..408dd4cbd 100644 --- a/editor/src/DocumentWindows/AssetManager/FileDrop.cpp +++ b/editor/src/DocumentWindows/AssetManager/FileDrop.cpp @@ -15,12 +15,26 @@ #include "AssetManagerWindow.hpp" #include "assets/AssetImporter.hpp" #include "assets/AssetLocation.hpp" +#include "assets/AssetCatalog.hpp" #include "Logger.hpp" #include #include namespace nexo::editor { + void AssetManagerWindow::handleAssetDrop(const std::string &path) const + { + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) + { + const auto data = static_cast(payload->Data); + assets::AssetCatalog::getInstance().moveAsset(data->id, path); + } + ImGui::EndDragDropTarget(); + } + } + assets::AssetLocation AssetManagerWindow::getAssetLocation(const std::filesystem::path &path) const { const std::string assetName = path.stem().string(); @@ -54,9 +68,6 @@ namespace nexo::editor { for (const auto& filePath : m_pendingDroppedFiles) importDroppedFile(filePath); m_pendingDroppedFiles.clear(); - - m_folderStructure.clear(); - buildFolderStructure(); } void AssetManagerWindow::importDroppedFile(const std::string& filePath) const @@ -68,9 +79,6 @@ namespace nexo::editor { return; } - std::string extension = path.extension().string(); - std::ranges::transform(extension, extension.begin(), ::tolower); - const assets::AssetLocation location = getAssetLocation(path); assets::ImporterFileInput fileInput{path}; diff --git a/editor/src/DocumentWindows/AssetManager/FolderCreation.cpp b/editor/src/DocumentWindows/AssetManager/FolderCreation.cpp new file mode 100644 index 000000000..754dd32dc --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/FolderCreation.cpp @@ -0,0 +1,71 @@ +//// FolderCreation.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 27/07/2025 +// Description: Source file for the method used to create a new folder +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" +#include "ImNexo/Elements.hpp" + +namespace nexo::editor { + + bool AssetManagerWindow::handleNewFolderCreation() + { + if (m_folderCreationState.folderName.empty()) { + m_folderCreationState.showError = true; + m_folderCreationState.errorMessage = "Folder name cannot be empty"; + return false; + } + + if (!m_folderManager.createFolder(m_folderCreationState.parentPath, m_folderCreationState.folderName)) { + m_folderCreationState.showError = true; + m_folderCreationState.errorMessage = "Failed to create folder (may already exist)"; + return false; + } + + return true; + } + + void AssetManagerWindow::newFolderMenu() + { + ImGui::Text("Enter name for the new folder:"); + constexpr size_t MAX_FOLDER_NAME_LENGTH = 256; + m_folderCreationState.folderName.resize(MAX_FOLDER_NAME_LENGTH); + ImGui::InputText("##FolderName", m_folderCreationState.folderName.data(), m_folderCreationState.folderName.capacity()); + m_folderCreationState.folderName.resize(strlen(m_folderCreationState.folderName.c_str())); + ImGui::Separator(); + + if (ImNexo::Button("Create") && handleNewFolderCreation()) { + m_folderCreationState.reset(); + PopupManager::closePopupInContext(); + } + ImGui::SameLine(); + if (ImNexo::Button("Cancel")) { + m_folderCreationState.reset(); + PopupManager::closePopupInContext(); + } + + if (m_folderCreationState.showError) { + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); + ImGui::Text("%s", m_folderCreationState.errorMessage.c_str()); + ImGui::PopStyleColor(); + + if (m_folderCreationState.errorTimer <= 0.0f) { + m_folderCreationState.showError = false; + m_folderCreationState.errorTimer = ERROR_DISPLAY_TIMEOUT; // Reset timer + } else + m_folderCreationState.errorTimer -= ImGui::GetIO().DeltaTime; + } + PopupManager::closePopup(); + } + +} diff --git a/editor/src/DocumentWindows/AssetManager/FolderManager.cpp b/editor/src/DocumentWindows/AssetManager/FolderManager.cpp new file mode 100644 index 000000000..796980284 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/FolderManager.cpp @@ -0,0 +1,228 @@ +//// FolderManager.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/07/2025 +// Description: Source file for the folder manager +// +/////////////////////////////////////////////////////////////////////////////// + +#include "FolderManager.hpp" + +namespace nexo::editor { + + static bool isNameValid(std::string_view folderName) + { + return !(folderName.empty() || folderName.front() == '_' || folderName.find('/') != std::string::npos); + } + + FolderManager::FolderManager() + { + m_pathToName[""] = "Assets"; + m_children[""] = {}; + } + + void FolderManager::buildFromAssets() + { + clear(); + std::unordered_set allPaths; + + for (const auto& ref : assets::AssetCatalog::getInstance().getAssets()) { + if (const auto assetData = ref.lock()) { + const std::string& folderPath = assetData->getMetadata().location.getPath(); + if (folderPath.empty() || folderPath.front() == '_') + continue; + addPathAndParents(folderPath, allPaths); + } + } + + buildMapsFromPaths(allPaths); + } + + std::vector> FolderManager::getChildren(const std::string& path) const + { + std::vector> result; + + if (auto it = m_children.find(path); it != m_children.end()) { + result.reserve(it->second.size()); + for (const std::string& childPath : it->second) { + auto nameIt = m_pathToName.find(childPath); + if (nameIt != m_pathToName.end()) { + result.emplace_back(childPath, nameIt->second); + } + } + } + + return result; + } + + std::string FolderManager::getName(const std::string& path) const + { + if (auto it = m_pathToName.find(path); it != m_pathToName.end()) + return it->second; + return extractNameFromPath(path); + } + + bool FolderManager::exists(const std::string& path) const + { + return m_pathToName.contains(path); + } + + bool FolderManager::createFolder(const std::string& parentPath, const std::string& folderName) + { + if (!exists(parentPath)) + return false; + + std::string newFolderPath = parentPath.empty() ? folderName : parentPath + "/" + folderName; + if (exists(newFolderPath)) + return false; + + if (!isNameValid(folderName)) + return false; + + m_pathToName[newFolderPath] = folderName; + m_children[newFolderPath] = {}; + m_children[parentPath].push_back(newFolderPath); + + std::ranges::sort(m_children[parentPath]); + + return true; + } + + bool FolderManager::deleteFolder(const std::string& folderPath) + { + if (folderPath.empty() || !exists(folderPath)) + return false; + + // TODO: Check if folder contains assets - you might want to prevent deletion + // if (!getFolderAssets(folderPath).empty()) return false; + + // Recursively delete all children first + auto childrenCopy = m_children[folderPath]; + for (const std::string& childPath : childrenCopy) + deleteFolder(childPath); + + // Remove from parent's children list + std::string parentPath = getParentPath(folderPath); + if (auto parentIt = m_children.find(parentPath); parentIt != m_children.end()) { + auto& parentChildren = parentIt->second; + std::erase(parentChildren, folderPath); + } + + m_pathToName.erase(folderPath); + m_children.erase(folderPath); + return true; + } + + bool FolderManager::renameFolder(const std::string& folderPath, const std::string& newName) + { + if (folderPath.empty() || !exists(folderPath)) + return false; + + if (!isNameValid(newName)) + return false; + + std::string parentPath = getParentPath(folderPath); + std::string newFolderPath = parentPath.empty() ? newName : parentPath + "/" + newName; + + if (newFolderPath != folderPath && exists(newFolderPath)) + return false; + + // If the path doesn't change, just update the display name + if (newFolderPath == folderPath) { + m_pathToName[folderPath] = newName; + return true; + } + + // TODO: This gets complex if you need to update all child paths + // For now, just update the display name + m_pathToName[folderPath] = newName; + return true; + } + + std::vector FolderManager::getAllPaths() const + { + std::vector paths; + paths.reserve(m_pathToName.size()); + for (const auto& [path, name] : m_pathToName) { + paths.push_back(path); + } + std::ranges::sort(paths); + return paths; + } + + size_t FolderManager::getChildCount(const std::string& path) const + { + if (auto it = m_children.find(path); it != m_children.end()) { + return it->second.size(); + } + return 0; + } + + void FolderManager::clear() + { + m_pathToName.clear(); + m_children.clear(); + + m_pathToName[""] = "Assets"; + m_children[""] = {}; + } + + void FolderManager::addPathAndParents(const std::string& fullPath, std::unordered_set& allPaths) const + { + if (fullPath.empty()) return; + + std::string currentPath = ""; + std::stringstream ss(fullPath); + std::string part; + + while (std::getline(ss, part, '/')) { + if (!part.empty()) { + currentPath = currentPath.empty() ? part : currentPath + "/" + part; + allPaths.insert(currentPath); + } + } + } + + void FolderManager::buildMapsFromPaths(const std::unordered_set& allPaths) + { + // Build path->name mapping + for (const std::string& path : allPaths) { + m_pathToName[path] = extractNameFromPath(path); + m_children[path] = {}; // Initialize empty children vector + } + + // Build parent->children relationships + for (const std::string& path : allPaths) { + std::string parentPath = getParentPath(path); + m_children[parentPath].push_back(path); + } + + // Sort all children vectors + for (auto& [parent, children] : m_children) + std::ranges::sort(children); + } + + std::string FolderManager::extractNameFromPath(const std::string& path) const + { + if (path.empty()) + return "Assets"; + + size_t lastSlash = path.find_last_of('/'); + return (lastSlash == std::string::npos) ? path : path.substr(lastSlash + 1); + } + + std::string FolderManager::getParentPath(const std::string& path) const + { + if (path.empty()) + return ""; // Root has no parent + + size_t lastSlash = path.find_last_of('/'); + return (lastSlash == std::string::npos) ? "" : path.substr(0, lastSlash); + } +} diff --git a/editor/src/DocumentWindows/AssetManager/FolderManager.hpp b/editor/src/DocumentWindows/AssetManager/FolderManager.hpp new file mode 100644 index 000000000..b76a11d44 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/FolderManager.hpp @@ -0,0 +1,64 @@ +//// FolderManager.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/07/2025 +// Description: Header file for the folder manager +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once +#include +#include +#include +#include +#include +#include + +#include "assets/AssetCatalog.hpp" + +namespace nexo::editor { + + class FolderManager { + private: + std::unordered_map m_pathToName; // path -> display name + std::unordered_map> m_children; // path -> direct children paths + + public: + FolderManager(); + + void buildFromAssets(); + + std::vector> getChildren(const std::string& path) const; + + std::string getName(const std::string& path) const; + + bool exists(const std::string& path) const; + + bool createFolder(const std::string& parentPath, const std::string& folderName); + + bool deleteFolder(const std::string& folderPath); + + bool renameFolder(const std::string& folderPath, const std::string& newName); + + std::vector getAllPaths() const; + + size_t getChildCount(const std::string& path) const; + + private: + void clear(); + + void addPathAndParents(const std::string& fullPath, std::unordered_set& allPaths) const; + + void buildMapsFromPaths(const std::unordered_set& allPaths); + + std::string extractNameFromPath(const std::string& path) const; + + std::string getParentPath(const std::string& path) const; + }; + +} // namespace nexo::editor diff --git a/editor/src/DocumentWindows/AssetManager/FolderTree.cpp b/editor/src/DocumentWindows/AssetManager/FolderTree.cpp index 68179b6ea..e8523297e 100644 --- a/editor/src/DocumentWindows/AssetManager/FolderTree.cpp +++ b/editor/src/DocumentWindows/AssetManager/FolderTree.cpp @@ -18,260 +18,129 @@ #include "assets/AssetCatalog.hpp" #include +#include #include namespace nexo::editor { - void AssetManagerWindow::drawFolderTreeItem(const std::string& name, const std::string& path) + + static void drawSearchBar(std::string &searchBuffer) { - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + constexpr size_t MAX_SEARCH_LENGTH = 256; + searchBuffer.resize(MAX_SEARCH_LENGTH); + ImGui::PushItemWidth(-1); + ImGui::InputTextWithHint("##search", "Search...", searchBuffer.data(), searchBuffer.capacity()); + searchBuffer.resize(strlen(searchBuffer.c_str())); + ImGui::PopItemWidth(); + ImGui::Separator(); + } - // Check if this is the selected folder - if (path == m_currentFolder) - flags |= ImGuiTreeNodeFlags_Selected; + struct FavoriteItem { + std::string_view icon; + std::string_view name; + assets::AssetType type; - if (!m_folderChildren.contains(path)) - flags |= ImGuiTreeNodeFlags_Leaf; + [[nodiscard]] std::string getLabel(bool selected) const { + return std::format("{} {}{}", icon, name, selected ? " " ICON_FA_CHECK : ""); + } + }; - // Folder icon - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 180, 80, 255)); - ImGui::Text(ICON_FA_FOLDER); - ImGui::PopStyleColor(); - ImGui::SameLine(); + static void drawFavorites(assets::AssetType &selectedType) + { + ImGuiTreeNodeFlags rootFlags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (!ImGui::TreeNodeEx(ICON_FA_STAR " Favorites", rootFlags)) + return; - bool opened = ImGui::TreeNodeEx(name.c_str(), flags); + static constexpr FavoriteItem favorites[]{ + {ICON_FA_ADJUST, "Materials", assets::AssetType::MATERIAL}, + {ICON_FA_CUBE, "Models", assets::AssetType::MODEL}, + {ICON_FA_SQUARE, "Textures", assets::AssetType::TEXTURE} + }; - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) - m_currentFolder = path; + for (const auto& fav : favorites) { + const bool isSelected = (fav.type == selectedType); - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("New Folder")) { - m_folderCreationState.parentPath = path; - m_folderCreationState.isCreatingFolder = true; - ImGui::OpenPopup("Create New Folder"); - std::format_to_n( - m_folderCreationState.folderName, - sizeof(m_folderCreationState.folderName) - 1, // Ensure null termination - "New Folder" - ); - } - ImGui::EndPopup(); - } + ImGuiTreeNodeFlags itemFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (isSelected) itemFlags |= ImGuiTreeNodeFlags_Selected; + + const auto label = fav.getLabel(isSelected); + ImGui::TreeNodeEx(label.c_str(), itemFlags); - if (opened) { - // Use the precomputed children list - if (const auto it = m_folderChildren.find(path); it != m_folderChildren.end()) { - for (const auto& childPath : it->second) { - // Find the name of the child from m_folderStructure - std::string childName; - for (const auto& [p, n] : m_folderStructure) { - if (p == childPath) { - childName = n; - break; - } - } - drawFolderTreeItem(childName, childPath); - } + if (ImGui::IsItemClicked()) { + selectedType = isSelected ? assets::AssetType::UNKNOWN : fav.type; } - ImGui::TreePop(); } + ImGui::TreePop(); } - void AssetManagerWindow::handleNewFolderCreation() + void AssetManagerWindow::folderTreeContextMenu() { - if (m_folderCreationState.isCreatingFolder) { - ImGui::OpenPopup("Create New Folder"); - - // Center the popup - const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - if (ImGui::BeginPopupModal("Create New Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Enter name for new folder:"); - ImGui::InputText("##FolderName", m_folderCreationState.folderName, sizeof(m_folderCreationState.folderName)); - - ImGui::Separator(); - - if (ImGui::Button("Create", ImVec2(120, 0))) { - if (strnlen(m_folderCreationState.folderName, sizeof(m_folderCreationState.folderName)) > 0) { - std::string newFolderPath; - if (m_folderCreationState.parentPath.empty()) - newFolderPath = m_folderCreationState.folderName; - else - newFolderPath = m_folderCreationState.parentPath + "/" + m_folderCreationState.folderName; + if (ImGui::MenuItem("New Folder")) + m_popupManager.openPopup("Create new folder"); - // Check if folder already exists - bool folderExists = false; - for (const auto &path: m_folderStructure | std::views::keys) { - if (path == newFolderPath) { - folderExists = true; - break; - } - } - - if (!folderExists) { - m_folderStructure.emplace_back(newFolderPath, m_folderCreationState.folderName); - LOG(NEXO_INFO, "Created new folder: {}", newFolderPath); + PopupManager::closePopup(); + } - m_folderCreationState.isCreatingFolder = false; - ImGui::CloseCurrentPopup(); - } else { - m_folderCreationState.showError = true; - m_folderCreationState.errorMessage = "Folder already exists"; - } - } else { - m_folderCreationState.showError = true; - m_folderCreationState.errorMessage = "Folder name cannot be empty"; - } - } + void AssetManagerWindow::drawFolderTreeItem(const std::string& name, const std::string& path) + { + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (path == m_currentFolder) + flags |= ImGuiTreeNodeFlags_Selected; - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) { - m_folderCreationState.isCreatingFolder = false; - ImGui::CloseCurrentPopup(); - } + auto children = m_folderManager.getChildren(path); + if (children.empty()) + flags |= ImGuiTreeNodeFlags_Leaf; - // Display error message if needed - if (m_folderCreationState.showError) { - ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); - ImGui::Text("%s", m_folderCreationState.errorMessage.c_str()); - ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 180, 80, 255)); + ImGui::Text(ICON_FA_FOLDER); + ImGui::PopStyleColor(); + ImGui::SameLine(); - // Clear error after a few seconds - if (m_folderCreationState.errorTimer <= 0.0f) { - m_folderCreationState.showError = false; - m_folderCreationState.errorTimer = 3.0f; // Reset timer - } else { - m_folderCreationState.errorTimer -= ImGui::GetIO().DeltaTime; - } - } + bool opened = ImGui::TreeNodeEx(name.c_str(), flags); - ImGui::EndPopup(); - } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !ImGui::IsItemToggledOpen()) + m_currentFolder = path; + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + m_folderCreationState.reset(); + m_folderCreationState.parentPath = path; + m_popupManager.openPopup("Folder Tree Context Menu"); } - } - void AssetManagerWindow::buildFolderStructure() - { - m_folderStructure.clear(); - // Root entry - m_folderStructure.emplace_back("", "Assets"); - m_folderChildren.clear(); // Clear the folder children map + if (!opened) + return; - // First pass: build the folder structure - std::set> uniqueFolderPaths; - - std::unordered_set seen{""}; - - for (const auto assets = assets::AssetCatalog::getInstance().getAssets(); auto& ref : assets) { - if (const auto assetData = ref.lock()) { - // normalized path: e.g. "Random/Sub" - std::filesystem::path p{ assetData->getMetadata().location.getPath() }; - std::filesystem::path curr; - for (auto const& part : p) { - // skip empty or “_internal” style parts - if (auto s = part.string(); s.empty() || s.front() == '_') - continue; - curr /= part; - if (auto folderPath = curr.string(); seen.emplace(folderPath).second) { - m_folderStructure.emplace_back( - folderPath, - curr.filename().string() - ); - } - } - } + for (const auto& [childPath, childName] : children) { + drawFolderTreeItem(childName, childPath); } - - std::sort( - m_folderStructure.begin() + 1, - m_folderStructure.end(), - [](auto const& a, auto const& b){ - return a.first < b.first; - } - ); + ImGui::TreePop(); } - void AssetManagerWindow::drawFolderTree() { - handleNewFolderCreation(); + drawSearchBar(m_searchBuffer); + drawFavorites(m_selectedType); - ImGui::PushItemWidth(-1); - ImGui::InputTextWithHint("##search", "Search...", m_searchBuffer, sizeof(m_searchBuffer)); - ImGui::PopItemWidth(); - ImGui::Separator(); - - // favorites section - { - if (ImGui::TreeNodeEx(ICON_FA_STAR " Favorites", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - struct FavoriteItem { - std::string label; - assets::AssetType type; - }; - - static const FavoriteItem favorites[] = { - {ICON_FA_ADJUST " Materials", assets::AssetType::MATERIAL}, - {ICON_FA_CUBE " Models", assets::AssetType::MODEL}, - {ICON_FA_SQUARE " Textures", assets::AssetType::TEXTURE} - }; + ImGuiTreeNodeFlags headerFlags = ImGuiTreeNodeFlags_OpenOnDoubleClick; - for (const auto& fav : favorites) { - const bool isSelected = (fav.type == m_selectedType); + if (m_currentFolder.empty()) + headerFlags |= ImGuiTreeNodeFlags_Selected; - ImGuiTreeNodeFlags itemFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; - if (isSelected) - itemFlags |= ImGuiTreeNodeFlags_Selected; - - const std::string labelName = fav.label + (isSelected ? " " ICON_FA_CHECK : ""); - ImGui::TreeNodeEx(labelName.c_str(), itemFlags); - - if (ImGui::IsItemClicked()) { - if (isSelected) - m_selectedType = assets::AssetType::UNKNOWN; - else - m_selectedType = fav.type; - } - } - ImGui::TreePop(); - } - } + bool assetsOpen = ImGui::TreeNodeEx(ICON_FA_FOLDER " Assets", headerFlags); - // folder structure + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - ImGuiTreeNodeFlags headerFlags = ImGuiTreeNodeFlags_OpenOnDoubleClick; - - if (m_currentFolder.empty()) { - headerFlags |= ImGuiTreeNodeFlags_Selected; - } - - bool assetsOpen = ImGui::TreeNodeEx(ICON_FA_FOLDER " Assets", headerFlags); - - // Handle right-click on Assets root - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("New Folder")) { - m_folderCreationState.parentPath = ""; - m_folderCreationState.isCreatingFolder = true; - std::format_to_n( - m_folderCreationState.folderName, - sizeof(m_folderCreationState.folderName) - 1, // Ensure null termination - "New Folder" - ); - } - ImGui::EndPopup(); - } + m_folderCreationState.reset(); + m_popupManager.openPopup("Folder Tree Context Menu"); + } + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + m_currentFolder = ""; - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) - m_currentFolder = ""; + if (!assetsOpen) + return; - if (assetsOpen) { - for (const auto& [path, name] : m_folderStructure) { - if (!path.empty() && path.find('/') == std::string::npos) { - drawFolderTreeItem(name, path); - } - } - ImGui::TreePop(); - } + auto rootChildren = m_folderManager.getChildren(""); + for (const auto& [path, name] : rootChildren) { + drawFolderTreeItem(name, path); } + ImGui::TreePop(); } } diff --git a/editor/src/DocumentWindows/AssetManager/Init.cpp b/editor/src/DocumentWindows/AssetManager/Init.cpp index 9df8c5ea0..8d6471563 100644 --- a/editor/src/DocumentWindows/AssetManager/Init.cpp +++ b/editor/src/DocumentWindows/AssetManager/Init.cpp @@ -22,11 +22,6 @@ namespace nexo::editor { void AssetManagerWindow::setup() { - auto& catalog = assets::AssetCatalog::getInstance(); - auto asset = std::make_unique(); - assets::AssetLocation location{"my_package::My_Model@Random"}; - catalog.registerAsset(location, std::move(asset)); - { assets::AssetImporter importer; std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/models/9mn/scene.gltf"); @@ -48,5 +43,20 @@ namespace nexo::editor { } // Register for file drop events Application::getInstance().getEventManager()->registerListener(this); + + m_layout.color.thumbnailBg = ImGui::GetColorU32(ImGuiCol_Button); + m_layout.color.thumbnailBgHovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered); + m_layout.color.thumbnailBgSelected = ImGui::GetColorU32(ImGuiCol_Header); + m_layout.color.thumbnailBgSelectedHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + + m_layout.color.selectedBoxColor = ImGui::GetColorU32(ImGuiCol_TabSelectedOverline); + + m_layout.color.titleBg = ImGui::GetColorU32(ImGuiCol_Header); + m_layout.color.titleBgHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + m_layout.color.titleBgSelected = ImGui::GetColorU32(ImGuiCol_Header); + m_layout.color.titleBgSelectedHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + + m_layout.color.titleText = ImGui::GetColorU32(ImGuiCol_Text); + buildFolderStructure(); } } diff --git a/editor/src/DocumentWindows/AssetManager/Selection.cpp b/editor/src/DocumentWindows/AssetManager/Selection.cpp new file mode 100644 index 000000000..289ab95fe --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/Selection.cpp @@ -0,0 +1,39 @@ +//// Selection.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 27/07/2025 +// Description: Source file for the selection handling in the asset manager +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" + +namespace nexo::editor { + void AssetManagerWindow::handleSelection(const unsigned int index, const bool isSelected) + { + if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) { + if (isSelected) + m_selectedAssets.erase(index); + else + m_selectedAssets.insert(index); + return; + } + + if (ImGui::IsKeyDown(ImGuiKey_ModShift) && !m_selectedAssets.empty()) { + const unsigned int latestSelected = *m_selectedAssets.rbegin(); + const auto [start, end] = std::minmax(latestSelected, index); + const auto range = std::views::iota(start, end + 1); + m_selectedAssets.insert(range.begin(), range.end()); + return; + } + + m_selectedAssets.clear(); + m_selectedAssets.insert(index); + } +} diff --git a/editor/src/DocumentWindows/AssetManager/Show.cpp b/editor/src/DocumentWindows/AssetManager/Show.cpp index b52a99160..c36108a65 100644 --- a/editor/src/DocumentWindows/AssetManager/Show.cpp +++ b/editor/src/DocumentWindows/AssetManager/Show.cpp @@ -16,14 +16,17 @@ #include "assets/Asset.hpp" #include "assets/AssetCatalog.hpp" #include "IconsFontAwesome.h" +#include "Path.hpp" #include "assets/Assets/Texture/Texture.hpp" #include "context/ThumbnailCache.hpp" #include "context/ActionManager.hpp" #include "context/actions/AssetActions.hpp" +#include "ImNexo/Elements.hpp" #include #include namespace nexo::editor { + void AssetManagerWindow::drawMenuBar() { if (ImGui::BeginMenuBar()) { @@ -36,496 +39,89 @@ namespace nexo::editor { } } - void AssetManagerWindow::calculateLayout(const float availWidth) + void AssetManagerWindow::drawPanelSplitter() { - // Sizes - m_layout.size.columnCount = std::max( - static_cast(availWidth / m_layout.size.itemStep.x), 1 - ); - m_layout.size.itemSize = ImVec2( - m_layout.size.iconSize + ImGui::GetFontSize() * 1.5f, // width - m_layout.size.iconSize + ImGui::GetFontSize() * 1.7f // height - ); - m_layout.size.itemStep = ImVec2( - m_layout.size.itemSize.x + static_cast(m_layout.size.iconSpacing), - m_layout.size.itemSize.y + static_cast(m_layout.size.iconSpacing) - ); - // Colors - m_layout.color.thumbnailBg = ImGui::GetColorU32(ImGuiCol_Button); - m_layout.color.thumbnailBgHovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered); - m_layout.color.thumbnailBgSelected = ImGui::GetColorU32(ImGuiCol_Header); - m_layout.color.thumbnailBgSelectedHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); - - m_layout.color.selectedBoxColor = ImGui::GetColorU32(ImGuiCol_TabSelectedOverline); + constexpr float splitterWidth = 5.0f; - m_layout.color.titleBg = ImGui::GetColorU32(ImGuiCol_Header); - m_layout.color.titleBgHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); - m_layout.color.titleBgSelected = ImGui::GetColorU32(ImGuiCol_Header); - m_layout.color.titleBgSelectedHovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_Separator)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyleColorVec4(ImGuiCol_SeparatorHovered)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_SeparatorActive)); - m_layout.color.titleText = ImGui::GetColorU32(ImGuiCol_Text); - } + ImGui::Button("##Splitter", ImVec2(splitterWidth, -1)); + ImGui::PopStyleColor(3); - void AssetManagerWindow::handleSelection(const unsigned int index, const bool isSelected) - { - if (ImGui::GetIO().KeyCtrl) { - if (isSelected) - m_selectedAssets.erase(index); - else - m_selectedAssets.insert(index); - } else if (ImGui::GetIO().KeyShift) { - const unsigned int latestSelected = m_selectedAssets.empty() ? 0 : *m_selectedAssets.rbegin(); - if (latestSelected <= index) { - for (unsigned int i = latestSelected ; i <= index; ++i) { - m_selectedAssets.insert(i); - } - } else { - for (unsigned int i = index; i <= latestSelected; ++i) { - m_selectedAssets.insert(i); - } - } - } else { - m_selectedAssets.clear(); - m_selectedAssets.insert(index); - } + if (ImGui::IsItemActive()) + m_layout.leftPanelWidth += ImGui::GetIO().MouseDelta.x; } - static ImU32 getAssetTypeOverlayColor(const assets::AssetType type) + void AssetManagerWindow::drawBreadcrumbs() { - switch (type) { - case assets::AssetType::TEXTURE: return IM_COL32(200, 70, 70, 255); - case assets::AssetType::MODEL: return IM_COL32(70, 170, 70, 255); - default: return IM_COL32(0, 0, 0, 0); - } - } - - void AssetManagerWindow::drawAsset( - const assets::GenericAssetRef& asset, - const unsigned int index, - const ImVec2& itemPos, - const ImVec2& itemSize - ) { - const auto assetData = asset.lock(); - if (!assetData) - return; - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const auto itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); - - ImGui::PushID(static_cast(index)); - - ImGui::SetCursorScreenPos(itemPos); - - const bool clicked = ImGui::InvisibleButton("##item", itemSize); - const bool isHovered = ImGui::IsItemHovered(); - - const bool isSelected = std::ranges::find(m_selectedAssets, index) != m_selectedAssets.end(); - const ImU32 bgColor = isSelected ? m_layout.color.thumbnailBgSelected : m_layout.color.thumbnailBg; - drawList->AddRectFilled(itemPos, itemEnd, bgColor, m_layout.size.cornerRadius); - - if (isSelected) { - // Draw a distinctive border around selected items - drawList->AddRect( - ImVec2(itemPos.x - 1, itemPos.y - 1), - ImVec2(itemEnd.x + 1, itemEnd.y + 1), - m_layout.color.selectedBoxColor, - m_layout.size.cornerRadius, - 0, - m_layout.size.selectedBoxThickness - ); - } - - // Draw thumbnail area - const auto thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * m_layout.size.thumbnailHeightRatio); - - if (const ImTextureID textureId = ThumbnailCache::getInstance().getThumbnail(asset); !textureId) { - drawList->AddRectFilled(itemPos, thumbnailEnd, m_layout.color.thumbnailBg); - } else { - constexpr float padding = 4.0f; - const ImVec2 imageStart(itemPos.x + padding, itemPos.y + padding); - const ImVec2 imageEnd(thumbnailEnd.x - padding, thumbnailEnd.y - padding); - - drawList->AddImage( - textureId, - imageStart, - imageEnd, - ImVec2(0, 1), // UV0 (top-left) - ImVec2(1, 0), // UV1 (bottom-right) - IM_COL32(255, 255, 255, 255) // White tint - ); - } - - // Draw type overlay (maybe later modify it to an icon) - const auto overlayPos = ImVec2(thumbnailEnd.x - m_layout.size.overlayPadding, itemPos.y + m_layout.size.overlayPadding); - const ImU32 overlayColor = getAssetTypeOverlayColor(assetData->getType()); - drawList->AddRectFilled(overlayPos, ImVec2(overlayPos.x + m_layout.size.overlaySize, overlayPos.y + m_layout.size.overlaySize), overlayColor); - - // Draw title - const char *assetName = assetData->getMetadata().location.getName().c_str(); - const auto textPos = ImVec2(itemPos.x + (itemSize.x - ImGui::CalcTextSize(assetName).x) * 0.5f, - thumbnailEnd.y + m_layout.size.titlePadding); - - // Background rectangle for text - const ImU32 titleBgColor = isHovered ? m_layout.color.titleBgHovered : m_layout.color.titleBg; - drawList->AddRectFilled(ImVec2(itemPos.x, thumbnailEnd.y), ImVec2(itemEnd.x, itemEnd.y), titleBgColor); - drawList->AddText(textPos, m_layout.color.titleText, assetName); - - // Handle selection when clicked - if (clicked) - handleSelection(index, isSelected); - - // On Hover show asset location - if (isHovered) - ImGui::SetTooltip("%s", assetData->getMetadata().location.getFullLocation().c_str()); - - // Handle drag source for assets - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) - { - AssetDragDropPayload payload; - payload.type = assetData->getType(); - payload.id = assetData->getID(); - payload.path = assetData->getMetadata().location.getFullLocation(); - payload.name = assetName; - - ImGui::SetDragDropPayload("ASSET_DRAG", &payload, sizeof(payload)); - - // Show preview while dragging - //TODO: Add asset preview thanks to thumbnail cache after rebasing - if (assetData->getType() == assets::AssetType::TEXTURE) { - const auto textureAsset = asset.as(); - if (const auto textureData = textureAsset.lock(); - textureData && textureData->getData() && textureData->getData()->texture) { - const ImTextureID textureId = textureData->getData()->texture->getId(); - ImGui::Image(textureId, {64, 64}); - } - } - - ImGui::EndDragDropSource(); - } + ImGui::PushID("breadcrumb_root"); + if (ImGui::Button("Assets")) + m_currentFolder.clear(); + handleAssetDrop(""); ImGui::PopID(); - } - ImTextureID AssetManagerWindow::getFolderIconTexture() const - { - if (const auto texRef = m_folderIcon.lock()) { - const auto &texData = texRef->getData(); - if (texData && texData->texture) { - return texData->texture->getId(); - } - } - return 0; - } - - void AssetManagerWindow::drawFolder( - const std::string& folderPath, - const std::string& folderName, - const ImVec2& itemPos, - const ImVec2& itemSize - ) { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const auto itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); - - ImGui::PushID(("folder_" + folderPath).c_str()); - - ImGui::SetCursorScreenPos(itemPos); - - const bool clicked = ImGui::InvisibleButton("##folder", itemSize); - const bool isHovered = ImGui::IsItemHovered(); - - if (isHovered) { - m_hoveredFolder = folderPath; - } else if (m_hoveredFolder == folderPath) { - m_hoveredFolder.clear(); - } - - if (ImGui::BeginDragDropTarget()) + std::string path = m_currentFolder; + std::vector crumbs = splitPath(m_currentFolder); + std::string fullPath; + for (const auto &crumb : crumbs) { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) - { - const auto* data = static_cast(payload->Data); - assets::AssetCatalog::getInstance().moveAsset(data->id, folderPath); - } - ImGui::EndDragDropTarget(); - } - - // Background - use hover color when hovered - const ImU32 bgColor = isHovered ? m_layout.color.thumbnailBgHovered : IM_COL32(0, 0, 0, 0); - drawList->AddRectFilled(itemPos, itemEnd, bgColor, m_layout.size.cornerRadius); - - const auto thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * m_layout.size.thumbnailHeightRatio); - - // Calculate padding for the icon - constexpr float padding = 10.0f; - - // Calculate available area dimensions - const float availWidth = thumbnailEnd.x - itemPos.x - (padding * 2.0f); - const float availHeight = thumbnailEnd.y - itemPos.y - (padding * 2.0f); - - // Maintain aspect ratio by using the smaller dimension - const float displaySize = std::min(availWidth, availHeight); - - // Calculate centered position - const float xOffset = (availWidth - displaySize) * 0.5f + padding; - const float yOffset = (availHeight - displaySize) * 0.5f + padding; - - // Final image coordinates maintaining aspect ratio - const ImVec2 imageStart( - itemPos.x + xOffset, - itemPos.y + yOffset - ); - const ImVec2 imageEnd( - imageStart.x + displaySize, - imageStart.y + displaySize - ); - - // Draw folder PNG icon - - if (const ImTextureID folderIconTexture = getFolderIconTexture()) { - drawList->AddImage( - folderIconTexture, - imageStart, - imageEnd, - ImVec2(0, 1), // UV0 (top-left) - ImVec2(1, 0), // UV1 (bottom-right) - IM_COL32(255, 255, 255, 255) // White tint for default color - ); - } - - // Calculate text size to ensure it fits - const ImVec2 textSize = ImGui::CalcTextSize(folderName.c_str()); - - // Draw title background - const ImU32 titleBgColor = isHovered ? m_layout.color.titleBgHovered : IM_COL32(0, 0, 0, 0); - const float titleAreaHeight = itemSize.y * (1.0f - m_layout.size.thumbnailHeightRatio); - - drawList->AddRectFilled( - ImVec2(itemPos.x, thumbnailEnd.y), - ImVec2(itemEnd.x, itemEnd.y), - titleBgColor - ); - - // Position text with proper vertical alignment - const float textY = thumbnailEnd.y + ((titleAreaHeight - textSize.y) * 0.5f); - const float textX = itemPos.x + (itemSize.x - textSize.x) * 0.5f; - - drawList->AddText( - ImVec2(textX, textY), - m_layout.color.titleText, - folderName.c_str() - ); - - if (clicked) - m_currentFolder = folderPath; // Navigate into this folder - - ImGui::PopID(); - } - - void AssetManagerWindow::drawAssetsGrid() - { - const ImVec2 startPos = ImGui::GetCursorScreenPos(); - - std::vector> subfolders; - for (auto& [path,name] : m_folderStructure) { - if (path.empty() || path.front() == '_') - continue; - - if (m_currentFolder.empty()) { - if (path.find('/') == std::string::npos) - subfolders.emplace_back(path, name); - } else { - if (std::string prefix = m_currentFolder + "/"; path.rfind(prefix, 0) == 0 && - path.find('/', prefix.size()) == std::string::npos) - { - subfolders.emplace_back(path, path.substr(prefix.size())); - } - } - } - - std::vector filtered; - for (auto& ref : assets::AssetCatalog::getInstance().getAssets()) { - if (const auto d = ref.lock()) { - const auto& folder = d->getMetadata().location.getPath(); - if (folder == "_internal") - continue; - if (m_selectedType != assets::AssetType::UNKNOWN && - d->getType() != m_selectedType) continue; - - if (folder == m_currentFolder) - filtered.push_back(ref); - } - } - - const size_t totalItems = subfolders.size() + filtered.size(); - ImGuiListClipper clipper; - const auto rows = static_cast((totalItems + m_layout.size.columnCount - 1) / m_layout.size.columnCount); - clipper.Begin(rows, m_layout.size.itemStep.y); - - while (clipper.Step()) { - for (int line = clipper.DisplayStart; line < clipper.DisplayEnd; ++line) { - const unsigned int startIdx = line * m_layout.size.columnCount; - const unsigned int endIdx = std::min(startIdx + m_layout.size.columnCount, static_cast(totalItems)); - - for (unsigned int i = startIdx; i < endIdx; ++i) { - unsigned int col = i % m_layout.size.columnCount; - unsigned int row = i / m_layout.size.columnCount; - ImVec2 itemPos{ - startPos.x + static_cast(col) * m_layout.size.itemStep.x, - startPos.y + static_cast(row) * m_layout.size.itemStep.y - }; + fullPath += (fullPath.empty() ? "" : "/") + crumb; + ImGui::SameLine(); ImGui::Text(" > "); ImGui::SameLine(); + ImGui::PushID(("breadcrumb_" + crumb).c_str()); + if (crumb == *std::prev(crumbs.end())) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", crumb.c_str()); + else if (ImNexo::Button(crumb)) + m_currentFolder = fullPath; - if (i < static_cast(subfolders.size())) { - // draw folder thumbnail - drawFolder( - subfolders[i].first, - subfolders[i].second, - itemPos, - m_layout.size.itemSize - ); - } else { - // draw asset thumbnail - const auto assetIdx = i - static_cast(subfolders.size()); - drawAsset( - filtered[assetIdx], - assetIdx, - itemPos, - m_layout.size.itemSize - ); - } - } - } + handleAssetDrop(fullPath); + ImGui::PopID(); } - clipper.End(); } void AssetManagerWindow::show() { - m_hoveredFolder.clear(); - if (m_folderStructure.empty()) - buildFolderStructure(); - ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); - ImGui::Begin(ICON_FA_FOLDER_OPEN " Asset Manager" NEXO_WND_USTRID_ASSET_MANAGER, &m_opened, ImGuiWindowFlags_MenuBar); - beginRender(NEXO_WND_USTRID_ASSET_MANAGER); + if (!ImGui::Begin(ICON_FA_FOLDER_OPEN " Asset Manager" NEXO_WND_USTRID_ASSET_MANAGER, &m_opened, ImGuiWindowFlags_MenuBar)) + return; + beginRender(NEXO_WND_USTRID_ASSET_MANAGER); drawMenuBar(); - // Calculate sizes for splitter - constexpr float splitterWidth = 5.0f; - static float leftPanelWidth = 200.0f; // Default width - - // Left panel (folder hierarchy) - ImGui::BeginChild("LeftPanel", ImVec2(leftPanelWidth, 0), true); - drawFolderTree(); - ImGui::EndChild(); - - // Splitter - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_Separator)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyleColorVec4(ImGuiCol_SeparatorHovered)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_SeparatorActive)); - - ImGui::Button("##Splitter", ImVec2(splitterWidth, -1)); - ImGui::PopStyleColor(3); - - // Handle splitter drag - if (ImGui::IsItemActive()) - leftPanelWidth += ImGui::GetIO().MouseDelta.x; - - // Right panel (asset grid) - ImGui::SameLine(); - ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); - - // Handle file drops - if (ImGui::BeginDragDropTarget()) { - m_showDropIndicator = true; - // Only to show the drop indicator - ImGui::EndDragDropTarget(); - } else { - m_showDropIndicator = false; - } - - // Draw drop indicator - if (m_showDropIndicator || !m_pendingDroppedFiles.empty()) + // Left panel { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const ImVec2 windowPos = ImGui::GetWindowPos(); - const ImVec2 windowSize = ImGui::GetWindowSize(); - - // Draw semi-transparent overlay - drawList->AddRectFilled(windowPos, ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y), - IM_COL32(100, 100, 255, 50)); - - // Draw border - drawList->AddRect(windowPos, ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y), - IM_COL32(100, 100, 255, 200), 0.0f, 0, 3.0f); - - // Draw text - const std::string dropText = "Drop files here to import"; - const ImVec2 textSize = ImGui::CalcTextSize(dropText.c_str()); - const auto textPos = ImVec2(windowPos.x + (windowSize.x - textSize.x) * 0.5f, - windowPos.y + (windowSize.y - textSize.y) * 0.5f); - drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize() * 1.5f, textPos, - IM_COL32(255, 255, 255, 255), dropText.c_str()); + ImGui::BeginChild("LeftPanel", ImVec2(m_layout.leftPanelWidth, 0), true); + drawFolderTree(); + ImGui::EndChild(); } - ImGui::Text(ICON_FA_FOLDER " "); + drawPanelSplitter(); ImGui::SameLine(); + // Right panel { - ImGui::PushID("breadcrumb_root"); - if (ImGui::Button("Assets")) - m_currentFolder.clear(); + ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) - { - const auto data = static_cast(payload->Data); - assets::AssetCatalog::getInstance().moveAsset(data->id, ""); - } - ImGui::EndDragDropTarget(); - } - ImGui::PopID(); - } - - // Intermediate breadcrumbs - std::string path = m_currentFolder; - size_t pos = 0; - std::string segment; - std::string fullPath; - while ((pos = path.find('/')) != std::string::npos) { - segment = path.substr(0, pos); - fullPath += (fullPath.empty() ? "" : "/") + segment; - ImGui::SameLine(); ImGui::Text(" > "); ImGui::SameLine(); - - ImGui::PushID(("breadcrumb_" + fullPath).c_str()); - if (ImGui::Button(segment.c_str())) - m_currentFolder = fullPath; + ImGui::Text(ICON_FA_FOLDER " "); + ImGui::SameLine(); - if (ImGui::BeginDragDropTarget()) - { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ASSET_DRAG")) - { - const auto data = static_cast(payload->Data); - assets::AssetCatalog::getInstance().moveAsset(data->id, fullPath); - } - ImGui::EndDragDropTarget(); - } - ImGui::PopID(); - - path.erase(0, pos + 1); - } - - if (!path.empty()) { - ImGui::SameLine(); ImGui::Text(" > "); ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", path.c_str()); + drawBreadcrumbs(); + ImGui::Separator(); + drawAssetsGrid(); + ImGui::EndChild(); } - ImGui::Separator(); + // Popups + { + if (m_popupManager.showPopup("Folder Tree Context Menu")) + folderTreeContextMenu(); - calculateLayout(ImGui::GetContentRegionAvail().x); - drawAssetsGrid(); - ImGui::EndChild(); + if (m_popupManager.showPopupModal("Create new folder")) + newFolderMenu(); + } ImGui::End(); } diff --git a/editor/src/DocumentWindows/AssetManager/Update.cpp b/editor/src/DocumentWindows/AssetManager/Update.cpp index 41442d0a1..25132dfc7 100644 --- a/editor/src/DocumentWindows/AssetManager/Update.cpp +++ b/editor/src/DocumentWindows/AssetManager/Update.cpp @@ -13,9 +13,15 @@ /////////////////////////////////////////////////////////////////////////////// #include "AssetManagerWindow.hpp" +#include "assets/AssetCatalog.hpp" namespace nexo::editor { + void AssetManagerWindow::buildFolderStructure() + { + m_folderManager.buildFromAssets(); + } + void AssetManagerWindow::update() { handleDroppedFiles(); diff --git a/editor/src/DocumentWindows/AssetManager/Utils.cpp b/editor/src/DocumentWindows/AssetManager/Utils.cpp new file mode 100644 index 000000000..5dcf2f513 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/Utils.cpp @@ -0,0 +1,29 @@ +//// Utils.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 27/05/2025 +// Description: Source file for the utils function of the asset manager +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" + +namespace nexo::editor { + + ImTextureID AssetManagerWindow::getIconTexture(const assets::AssetRef &texture) const + { + if (const auto texRef = texture.lock()) { + const auto &texData = texRef->getData(); + if (texData && texData->texture) { + return texData->texture->getId(); + } + } + return 0; + } +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp b/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp index 172f01900..4df72cace 100644 --- a/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp +++ b/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp @@ -62,6 +62,9 @@ namespace nexo::editor { */ explicit ConsoleWindow(const std::string &windowName, WindowRegistry ®istry); + ConsoleWindow(const ConsoleWindow&) = delete; + ConsoleWindow& operator=(const ConsoleWindow&) = delete; + /** * @brief Destructor that cleans up the ConsoleWindow. * diff --git a/editor/src/DocumentWindows/EditorScene/Gizmo.cpp b/editor/src/DocumentWindows/EditorScene/Gizmo.cpp index 10e5202a6..8c0d451bc 100644 --- a/editor/src/DocumentWindows/EditorScene/Gizmo.cpp +++ b/editor/src/DocumentWindows/EditorScene/Gizmo.cpp @@ -264,7 +264,9 @@ namespace nexo::editor { auto primaryTransform = coord->tryGetComponent(primaryEntity); if (!primaryTransform) { const auto entityWithTransform = findEntityWithTransform(selectedEntities); - if (!entityWithTransform) return; // No entity with transform found + if (!entityWithTransform.has_value()) { + return; // No entity with transform found + } primaryEntity = *entityWithTransform; primaryTransform = coord->tryGetComponent(primaryEntity); diff --git a/editor/src/DocumentWindows/EditorScene/Init.cpp b/editor/src/DocumentWindows/EditorScene/Init.cpp index 93f6cc245..d8af86ccf 100644 --- a/editor/src/DocumentWindows/EditorScene/Init.cpp +++ b/editor/src/DocumentWindows/EditorScene/Init.cpp @@ -190,12 +190,6 @@ namespace nexo::editor } lightsScene(m_sceneId); - // 9mn - assets::AssetImporter importer; - std::filesystem::path path9mn = Path::resolvePathRelativeToExe("../resources/models/9mn/scene.gltf"); - assets::ImporterFileInput fileInput9mn{path9mn}; - auto assetRef9mn = importer.importAsset(assets::AssetLocation("my_package::9mn@DefaultScene/"), fileInput9mn); - // Background createAndAddEntity({0.0f, 40.0f, -2.5f}, {44.0f, 80.0f, 0.5f}, {0, 0, 0}, {0.91f, 0.91f, 0.91f, 1.0f}, system::ShapeType::Box, JPH::EMotionType::Static); diff --git a/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp b/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp index b455f5424..fdfb48927 100644 --- a/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp +++ b/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp @@ -65,7 +65,7 @@ namespace nexo::editor { const std::vector &editorSceneInConfig = findAllEditorScenes(); if (!editorSceneInConfig.empty()) { const auto dockId = m_windowRegistry.getDockId(editorSceneInConfig[0]); - if (!dockId) + if (!dockId.has_value()) return false; m_windowRegistry.setDockId(std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, newScene->getSceneId()), *dockId); return true; @@ -78,7 +78,7 @@ namespace nexo::editor { const std::string windowName = std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, currentEditorSceneWindow[0]->getSceneId()); const auto dockId = m_windowRegistry.getDockId(windowName); // If we dont find the dockId, it means the scene is floating, so we create a new dock space node - if (!dockId) { + if (!dockId.has_value()) { setupNewDockSpaceNode(windowName, std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, newScene->getSceneId())); return true; } diff --git a/editor/src/ImNexo/Components.cpp b/editor/src/ImNexo/Components.cpp index 45a76ca12..6d6fddcab 100644 --- a/editor/src/ImNexo/Components.cpp +++ b/editor/src/ImNexo/Components.cpp @@ -169,95 +169,6 @@ namespace ImNexo { return clicked; } - bool RowEntityDropdown( - const std::string &label, - nexo::ecs::Entity &targetEntity, - const std::vector& entities, - const std::function& getNameFunc - ) - { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(label.c_str()); - - ImGui::TableNextColumn(); - IdGuard idGuard(label); - - bool changed = false; - - // Build entity-name mapping - static std::vector> entityNamePairs; - static nexo::ecs::Entity lastTargetEntity = 0; - static std::vector lastEntities; - - // Only rebuild the mapping if entities list changed or target entity changed - bool needRebuild = lastTargetEntity != targetEntity || lastEntities.size() != entities.size(); - - if (!needRebuild) { - for (size_t i = 0; i < entities.size() && !needRebuild; i++) { - needRebuild = lastEntities[i] != entities[i]; - } - } - - if (needRebuild) { - entityNamePairs.clear(); - entityNamePairs.reserve(entities.size()); - lastEntities = entities; - lastTargetEntity = targetEntity; - - for (nexo::ecs::Entity entity : entities) { - std::string name = getNameFunc(entity); - entityNamePairs.emplace_back(entity, name); - } - } - - // Find current index - int currentIndex = -1; - for (size_t i = 0; i < entityNamePairs.size(); i++) { - if (entityNamePairs[i].first == targetEntity) { - currentIndex = static_cast(i); - break; - } - } - - // Add a "None" option if we want to allow null selection - const std::string currentItemName = currentIndex >= 0 ? entityNamePairs[currentIndex].second : "None"; - - // Draw the combo box - ImGui::SetNextItemWidth(-FLT_MIN); // Use all available width - if (ImGui::BeginCombo("##entity_dropdown", currentItemName.c_str())) - { - // Optional: Add a "None" option for clearing the target - if (ImGui::Selectable("None", targetEntity == nexo::ecs::MAX_ENTITIES)) { - targetEntity = nexo::ecs::MAX_ENTITIES; - changed = true; - } - - for (size_t i = 0; i < entityNamePairs.size(); i++) - { - const bool isSelected = (currentIndex == static_cast(i)); - if (ImGui::Selectable(entityNamePairs[i].second.c_str(), isSelected)) - { - targetEntity = entityNamePairs[i].first; - changed = true; - } - - if (isSelected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - if (ImGui::IsItemActive()) - setItemActive(); - if (ImGui::IsItemActivated()) - setItemActivated(); - if (ImGui::IsItemDeactivated()) - setItemDeactivated(); - - return changed; - } - bool RowDragFloat(const Channels &channels) { bool modified = false; diff --git a/editor/src/ImNexo/Components.hpp b/editor/src/ImNexo/Components.hpp index 7a1037d04..0447c4a59 100644 --- a/editor/src/ImNexo/Components.hpp +++ b/editor/src/ImNexo/Components.hpp @@ -22,6 +22,8 @@ #include "ecs/Coordinator.hpp" #include "renderer/Texture.hpp" #include "Elements.hpp" +#include "Guard.hpp" +#include "ImNexo.hpp" namespace ImNexo { @@ -107,9 +109,95 @@ namespace ImNexo { * @param getNameFunc Function that converts an entity ID to a displayable name string * @return true if an entity was selected (value changed), false otherwise */ - bool RowEntityDropdown(const std::string &label, nexo::ecs::Entity &targetEntity, - const std::vector& entities, - const std::function& getNameFunc); + template + bool RowEntityDropdown( + const std::string& label, + nexo::ecs::Entity& targetEntity, + const std::vector& entities, + GetNameFunc&& getNameFunc + ) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label.c_str()); + + ImGui::TableNextColumn(); + IdGuard idGuard(label); + + bool changed = false; + + // Build entity-name mapping + static std::vector> entityNamePairs; + static nexo::ecs::Entity lastTargetEntity = 0; + static std::vector lastEntities; + + // Only rebuild the mapping if entities list changed or target entity changed + bool needRebuild = lastTargetEntity != targetEntity || lastEntities.size() != entities.size(); + + if (!needRebuild) { + for (size_t i = 0; i < entities.size() && !needRebuild; i++) { + needRebuild = lastEntities[i] != entities[i]; + } + } + + if (needRebuild) { + entityNamePairs.clear(); + entityNamePairs.reserve(entities.size()); + lastEntities = entities; + lastTargetEntity = targetEntity; + + for (nexo::ecs::Entity entity : entities) { + std::string name = getNameFunc(entity); + entityNamePairs.emplace_back(entity, name); + } + } + + // Find current index + int currentIndex = -1; + for (size_t i = 0; i < entityNamePairs.size(); i++) { + if (entityNamePairs[i].first == targetEntity) { + currentIndex = static_cast(i); + break; + } + } + + // Add a "None" option if we want to allow null selection + const std::string currentItemName = currentIndex >= 0 ? entityNamePairs[currentIndex].second : "None"; + + // Draw the combo box + ImGui::SetNextItemWidth(-FLT_MIN); // Use all available width + if (ImGui::BeginCombo("##entity_dropdown", currentItemName.c_str())) + { + // Optional: Add a "None" option for clearing the target + if (ImGui::Selectable("None", targetEntity == nexo::ecs::MAX_ENTITIES)) { + targetEntity = nexo::ecs::MAX_ENTITIES; + changed = true; + } + + for (size_t i = 0; i < entityNamePairs.size(); i++) + { + const bool isSelected = (currentIndex == static_cast(i)); + if (ImGui::Selectable(entityNamePairs[i].second.c_str(), isSelected)) + { + targetEntity = entityNamePairs[i].first; + changed = true; + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemActive()) + setItemActive(); + if (ImGui::IsItemActivated()) + setItemActivated(); + if (ImGui::IsItemDeactivated()) + setItemDeactivated(); + + return changed; + } /** * @brief Draws a row with multiple channels (badge + slider pairs) diff --git a/engine/src/assets/AssetLocation.hpp b/engine/src/assets/AssetLocation.hpp index 710c42651..ccc95d158 100644 --- a/engine/src/assets/AssetLocation.hpp +++ b/engine/src/assets/AssetLocation.hpp @@ -46,6 +46,12 @@ namespace nexo::assets { setLocation(fullLocation); } + AssetLocation(const AssetLocation&) = default; + AssetLocation& operator=(const AssetLocation&) = default; + + AssetLocation(AssetLocation&&) noexcept = default; + AssetLocation& operator=(AssetLocation&&) noexcept = default; + AssetLocation& setName(const AssetName& name) { _name = name; diff --git a/engine/src/ecs/ComponentArray.hpp b/engine/src/ecs/ComponentArray.hpp index 29124185b..dc3e81f45 100644 --- a/engine/src/ecs/ComponentArray.hpp +++ b/engine/src/ecs/ComponentArray.hpp @@ -256,7 +256,7 @@ namespace nexo::ecs { std::memcpy(&m_componentArray[newIndex], componentData, sizeof(T)); ++m_size; } else { - THROW_EXCEPTION(InternalError, "Component type is not trivially copyable for raw insertion"); + THROW_EXCEPTION(InternalError, "Component type is not trivially copyable, raw insertion is not supported"); } } diff --git a/engine/src/systems/RenderBillboardSystem.cpp b/engine/src/systems/RenderBillboardSystem.cpp index ff2292a0d..a58de3f41 100644 --- a/engine/src/systems/RenderBillboardSystem.cpp +++ b/engine/src/systems/RenderBillboardSystem.cpp @@ -211,7 +211,7 @@ namespace nexo::system { const auto &transform = transformComponentArray->get(entitySpan[i]); const auto &materialAsset = materialComponentArray->get(entitySpan[i]).material.lock(); const auto &billboard = billboardSpan[i]; - auto shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; + std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr); auto cmd = createDrawCommand( entity, diff --git a/engine/src/systems/RenderCommandSystem.cpp b/engine/src/systems/RenderCommandSystem.cpp index a6812a62b..a24173b70 100644 --- a/engine/src/systems/RenderCommandSystem.cpp +++ b/engine/src/systems/RenderCommandSystem.cpp @@ -293,7 +293,7 @@ namespace nexo::system { continue; const auto &transform = transformSpan[i]; const auto &materialAsset = materialSpan[i].material.lock(); - auto shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; + std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; const auto &mesh = meshSpan[i]; auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr); if (!shader)