From c177c8b9eaf3bd6fa8bbcf4da419982b3f55a08f Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Wed, 21 May 2025 16:38:18 -0500 Subject: [PATCH] Improves asset loading and file browsing Adds background asset scanning and loading to improve editor responsiveness. Updates the file browser with grid and list views, filtering, and callbacks for file selection. Fixes an issue where the asset manager would block the main thread during asset loading. --- src/core/systems/AssetManager.cpp | 357 ++++++++++++++++------------ src/core/systems/AssetManager.h | 42 ++-- src/core/systems/assets/Texture2D.h | 3 +- src/editor/Editor.cpp | 7 +- src/editor/Editor.h | 2 + src/editor/Windows/FileBrowser.cpp | 250 ++++++++++--------- src/editor/Windows/FileBrowser.h | 59 ++++- 7 files changed, 442 insertions(+), 278 deletions(-) diff --git a/src/core/systems/AssetManager.cpp b/src/core/systems/AssetManager.cpp index 6411e6b..1fa0387 100644 --- a/src/core/systems/AssetManager.cpp +++ b/src/core/systems/AssetManager.cpp @@ -1,189 +1,252 @@ +// AssetManager.cpp #include "AssetManager.h" #include "assets/Texture2D.h" #include "Logger.h" + #include #include -#include #include +#include namespace fs = std::filesystem; -namespace OX { +namespace OX +{ + // statics + std::unordered_map > AssetManager::s_LoadedAssets; + std::unordered_map AssetManager::s_MetadataMap; + // Map from actual file path to virtual ID + std::unordered_map AssetManager::s_PathToID; + std::mutex AssetManager::s_AssetMutex; -std::unordered_map> AssetManager::s_LoadedAssets; -std::unordered_map AssetManager::s_MetadataMap; -std::shared_ptr AssetManager::s_FileTree = std::make_shared(); + std::shared_ptr AssetManager::s_FileTree = std::make_shared(); -std::mutex AssetManager::s_TextureQueueMutex; -std::queue AssetManager::s_TextureUploadQueue; + std::filesystem::path AssetManager::s_ProjectRoot; + std::atomic AssetManager::s_Scanning{false}; + std::thread AssetManager::s_ScanThread; -fs::path AssetManager::s_ProjectRoot; -std::atomic AssetManager::s_Scanning = false; -std::thread AssetManager::s_ScanThread; + std::queue AssetManager::s_TextureQueue; + std::mutex AssetManager::s_QueueMutex; -void AssetManager::Init(const std::string& projectRoot) { - s_ProjectRoot = fs::absolute(projectRoot); - s_Scanning = true; + void AssetManager::Init(const std::string &projectRoot) + { + s_ProjectRoot = fs::absolute(projectRoot); + s_Scanning = true; - s_FileTree = std::make_shared(); - s_FileTree->name = "res://"; - s_FileTree->path = "res://"; - s_FileTree->isDirectory = true; - - s_ScanThread = std::thread([=] { - BackgroundScan(s_ProjectRoot); - s_Scanning = false; - }); -} - -void AssetManager::Shutdown() { - if (s_ScanThread.joinable()) - s_ScanThread.join(); -} - -void AssetManager::Tick() { - std::lock_guard lock(s_TextureQueueMutex); - if (!s_TextureUploadQueue.empty()) { - auto pending = s_TextureUploadQueue.front(); - s_TextureUploadQueue.pop(); - - GLuint texID; - glGenTextures(1, &texID); - glBindTexture(GL_TEXTURE_2D, texID); - - GLenum format = pending.channels == 4 ? GL_RGBA : GL_RGB; - glTexImage2D(GL_TEXTURE_2D, 0, format, pending.width, pending.height, 0, format, GL_UNSIGNED_BYTE, pending.data); - glGenerateMipmap(GL_TEXTURE_2D); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - stbi_image_free(pending.data); - - auto tex = std::make_shared(); - tex->SetFromGL(texID, pending.width, pending.height); - s_LoadedAssets[pending.id] = tex; - Logger::LogInfo("%u | %u", tex->GetID(), texID); + s_FileTree = std::make_shared(); + s_FileTree->name = "res://"; + s_FileTree->path = "res://"; + s_FileTree->isDirectory = true; + s_ScanThread = std::thread(BackgroundScan); } -} -void AssetManager::Rescan() { - if (s_Scanning) return; - s_Scanning = true; + void AssetManager::Shutdown() + { + if (s_ScanThread.joinable()) { + s_ScanThread.join(); + } + } - s_FileTree = std::make_shared(); - s_FileTree->name = "res://"; - s_FileTree->path = "res://"; - s_FileTree->isDirectory = true; + void AssetManager::Rescan() + { + if (s_Scanning) return; + s_Scanning = true; - s_ScanThread = std::thread([=] { - BackgroundScan(s_ProjectRoot); - s_Scanning = false; - }); -} + s_FileTree = std::make_shared(); + s_FileTree->name = "res://"; + s_FileTree->path = "res://"; + s_FileTree->isDirectory = true; -void AssetManager::BackgroundScan(const fs::path& root) { - for (const auto& entry : fs::recursive_directory_iterator(root)) { - const auto& path = entry.path(); - std::string resPath = MakeVirtualPath(path); + s_ScanThread = std::thread(BackgroundScan); + } - if (entry.is_directory()) { - AddToTree(resPath, true); - } else { - AddToTree(resPath, false); + void AssetManager::BackgroundScan() + { + while (s_Scanning) { + for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) { + const auto &path = entry.path(); + std::string resPath = MakeVirtualPath(path); - std::string type = DetectAssetType(path); - if (!type.empty()) { - if (type == "texture2D") { - int w, h, ch; - stbi_set_flip_vertically_on_load(1); - unsigned char* data = stbi_load(path.string().c_str(), &w, &h, &ch, 0); - if (data) { - std::lock_guard lock(s_TextureQueueMutex); - s_TextureUploadQueue.push({ resPath, path.string(), w, h, ch, data }); - s_MetadataMap[resPath] = { type, path.string() }; + if (entry.is_directory()) { + // you can also guard directories if you like: + // std::lock_guard g(s_AssetMutex); + // if (s_MetadataMap.count(resPath)) continue; + AddToTree(resPath, true); + } else { + { + // quick check: have we already seen this asset? + std::lock_guard lock(s_AssetMutex); + if (s_MetadataMap.find(resPath) != s_MetadataMap.end()) + continue; + // reserve a spot so other threads know it's in-flight + s_MetadataMap[resPath] = {"", ""}; + } + + AddToTree(resPath, false); + + auto type = DetectAssetType(path); + if (type == "texture2D") { + int w, h, ch; + stbi_set_flip_vertically_on_load(1); + unsigned char *data = stbi_load( + path.string().c_str(), &w, &h, &ch, 0 + ); + if (data) { + { + std::lock_guard lock(s_AssetMutex); + // now fill in the real metadata + s_MetadataMap[resPath] = { type, path.string() }; + } + std::lock_guard qlock(s_QueueMutex); + s_TextureQueue.push({resPath, w, h, ch, data}); + } } } } } + + s_Scanning = false; } -} -void AssetManager::AddToTree(const std::string& resPath, bool isDir) { - std::vector parts; - size_t pos = 0, next; - while ((next = resPath.find('/', pos)) != std::string::npos) { - if (next != pos) parts.push_back(resPath.substr(pos, next - pos)); - pos = next + 1; - } - if (pos < resPath.length()) parts.push_back(resPath.substr(pos)); - auto current = s_FileTree; - for (size_t i = 1; i < parts.size(); ++i) { - auto& name = parts[i]; - auto it = std::find_if(current->children.begin(), current->children.end(), [&](auto& child) { - return child->name == name; - }); + void AssetManager::Tick() + { + std::lock_guard qlock(s_QueueMutex); + while (!s_TextureQueue.empty()) { + auto pending = s_TextureQueue.front(); + s_TextureQueue.pop(); + + GLuint texID; + glGenTextures(1, &texID); + glBindTexture(GL_TEXTURE_2D, texID); + GLenum fmt = (pending.channels == 4 ? GL_RGBA : GL_RGB); + glTexImage2D(GL_TEXTURE_2D, 0, fmt, pending.width, pending.height, 0, fmt, GL_UNSIGNED_BYTE, pending.data); + glGenerateMipmap(GL_TEXTURE_2D); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + stbi_image_free(pending.data); + + auto tex = std::make_shared(); + tex->SetFromGL(texID, pending.width, pending.height); + + std::lock_guard lock(s_AssetMutex); + // Store asset by ID + s_LoadedAssets[pending.id] = tex; + // Also map original file path to this ID + auto meta = s_MetadataMap[pending.id]; + s_PathToID[meta.absolutePath] = pending.id; - if (it == current->children.end()) { - auto newNode = std::make_shared(); - newNode->name = name; - newNode->path = current->path + "/" + name; - newNode->isDirectory = (i < parts.size() - 1) || isDir; - current->children.push_back(newNode); - current = newNode; - } else { - current = *it; } } -} -std::string AssetManager::MakeVirtualPath(const fs::path& full) { - fs::path rel = fs::relative(full, s_ProjectRoot); - return "res://" + rel.generic_string(); -} + // Get asset by virtual ID or file path + std::shared_ptr AssetManager::Get(const std::string &keyOrPath) + { + std::string key = keyOrPath; + // Normalize accidental triple slashes in resource path: "res:///..." -> "res://..." + const std::string triple = "res:///"; + if (key.rfind(triple, 0) == 0) { + key = std::string("res://") + key.substr(triple.size()); + } -std::string AssetManager::DetectAssetType(const fs::path& ext) { - auto e = ext.extension().string(); - if (e == ".png" || e == ".jpg") return "texture2D"; - return ""; -} + std::lock_guard lock(s_AssetMutex); -std::shared_ptr AssetManager::Get(const std::string& resPath) { - auto it = s_LoadedAssets.find(resPath); - return (it != s_LoadedAssets.end()) ? it->second : nullptr; -} - -void AssetManager::SaveAssetPack(const std::string& outputPath) { - YAML::Emitter out; - out << YAML::BeginMap; - out << YAML::Key << "assets" << YAML::Value << YAML::BeginSeq; - - for (const auto& [resPath, meta] : s_MetadataMap) { - fs::path relative = fs::relative(meta.absolutePath, s_ProjectRoot); - - out << YAML::BeginMap; - out << YAML::Key << "id" << YAML::Value << resPath; - out << YAML::Key << "type" << YAML::Value << meta.type; - out << YAML::Key << "path" << YAML::Value << relative.generic_string(); - out << YAML::EndMap; + // 1) Direct ID lookup + auto it = s_LoadedAssets.find(key); + if (it != s_LoadedAssets.end()) { + return it->second; + } + // 2) Path-to-ID mapping + auto pit = s_PathToID.find(key); + if (pit != s_PathToID.end()) { + auto ait = s_LoadedAssets.find(pit->second); + if (ait != s_LoadedAssets.end()) { + return ait->second; + } + } + // 3) Convert filesystem path to virtual and lookup + try { + std::string virt = MakeVirtualPath(fs::absolute(key)); + // Normalize if virt had leading slash + if (!virt.empty() && virt.front() == '/') virt.erase(0, 1); + auto vit = s_LoadedAssets.find(virt); + if (vit != s_LoadedAssets.end()) { + return vit->second; + } + } catch (...) { + // ignore + } + return nullptr; } - out << YAML::EndSeq; - out << YAML::EndMap; - std::ofstream fout(outputPath); - fout << out.c_str(); - fout.close(); + std::shared_ptr AssetManager::GetFileTree() + { + return s_FileTree; + } - Logger::LogInfo("Saved asset pack: %s", outputPath.c_str()); -} + void AssetManager::SaveAssetPack(const std::string &outputPath) + { + YAML::Emitter out; + out << YAML::BeginMap << YAML::Key << "assets" << YAML::Value << YAML::BeginSeq; { + std::lock_guard lock(s_AssetMutex); + for (auto &[id, meta]: s_MetadataMap) { + fs::path rel = fs::relative(meta.absolutePath, s_ProjectRoot); + out << YAML::BeginMap + << YAML::Key << "id" << YAML::Value << id + << YAML::Key << "type" << YAML::Value << meta.type + << YAML::Key << "path" << YAML::Value << rel.generic_string() + << YAML::EndMap; + } + } + out << YAML::EndSeq << YAML::EndMap; -std::shared_ptr AssetManager::GetFileTree() { - return s_FileTree; -} + std::ofstream fout(outputPath); + fout << out.c_str(); + Logger::LogInfo("Saved asset pack: %s", outputPath.c_str()); + } + std::string AssetManager::MakeVirtualPath(const fs::path &full) + { + auto rel = fs::relative(full, s_ProjectRoot); + return "res://" + rel.generic_string(); + } + + void AssetManager::AddToTree(const std::string &resPath, bool isDir) + { + std::vector parts; + size_t pos = 0, next; + while ((next = resPath.find('/', pos)) != std::string::npos) { + if (next != pos) parts.push_back(resPath.substr(pos, next - pos)); + pos = next + 1; + } + if (pos < resPath.size()) parts.push_back(resPath.substr(pos)); + + auto current = s_FileTree; + for (size_t i = 1; i < parts.size(); ++i) { + auto &name = parts[i]; + auto it = std::find_if(current->children.begin(), current->children.end(), + [&](auto &c) { return c->name == name; }); + if (it == current->children.end()) { + auto node = std::make_shared(); + node->name = name; + node->path = current->path + "/" + name; + node->isDirectory = (i + 1 < parts.size()) || isDir; + current->children.push_back(node); + current = node; + } else { + current = *it; + } + } + } + + std::string AssetManager::DetectAssetType(const fs::path &path) + { + auto e = path.extension().string(); + if (e == ".png" || e == ".jpg" || e == ".jpeg") return "texture2D"; + return ""; + } } // namespace OX diff --git a/src/core/systems/AssetManager.h b/src/core/systems/AssetManager.h index a6bcca8..c9daae5 100644 --- a/src/core/systems/AssetManager.h +++ b/src/core/systems/AssetManager.h @@ -1,16 +1,18 @@ +/// AssetManager.h #pragma once + #include #include #include -#include #include #include #include #include #include +#include +#include namespace OX { - class Asset; struct ResourceTreeNode { @@ -22,12 +24,16 @@ namespace OX { class AssetManager { public: + // -- Lifecycle -- static void Init(const std::string& projectRoot); static void Shutdown(); - static void Tick(); // Main-thread + static void Tick(); static void Rescan(); - static std::shared_ptr Get(const std::string& resPath); + // -- Queries -- + // Lookup by virtual path ("res://…") or real FS path. + static std::shared_ptr Get(const std::string& path); + static std::shared_ptr GetFileTree(); static void SaveAssetPack(const std::string& outputPath); @@ -36,29 +42,33 @@ namespace OX { std::string type; std::string absolutePath; }; - struct PendingTexture { std::string id; - std::string path; int width, height, channels; unsigned char* data; }; - static void BackgroundScan(const std::filesystem::path& root); + static void BackgroundScan(); static void AddToTree(const std::string& virtualPath, bool isDir); - static std::string DetectAssetType(const std::filesystem::path& ext); + static std::string DetectAssetType(const std::filesystem::path& path); static std::string MakeVirtualPath(const std::filesystem::path& full); + // loaded assets & metadata + static std::unordered_map s_PathToID; static std::unordered_map> s_LoadedAssets; - static std::unordered_map s_MetadataMap; + static std::unordered_map s_MetadataMap; + static std::mutex s_AssetMutex; + + // directory tree static std::shared_ptr s_FileTree; - static std::mutex s_TextureQueueMutex; - static std::queue s_TextureUploadQueue; - + // background scan static std::filesystem::path s_ProjectRoot; - static std::atomic s_Scanning; - static std::thread s_ScanThread; - }; + static std::atomic s_Scanning; + static std::thread s_ScanThread; -} // namespace OX + // texture upload queue + static std::queue s_TextureQueue; + static std::mutex s_QueueMutex; + }; +} diff --git a/src/core/systems/assets/Texture2D.h b/src/core/systems/assets/Texture2D.h index 16d548d..b9a5c32 100644 --- a/src/core/systems/assets/Texture2D.h +++ b/src/core/systems/assets/Texture2D.h @@ -4,7 +4,6 @@ #pragma once #include "systems/Asset.h" #include -#include "GL/glew.h" namespace OX { class Texture2D : public Asset { @@ -17,7 +16,7 @@ namespace OX { void SetFromGL(uint32_t texID, int width, int height); - GLuint GetID() const { return m_TextureID; } + uint32_t GetID() const { return m_TextureID; } int GetWidth() const { return m_Width; } int GetHeight() const { return m_Height; } diff --git a/src/editor/Editor.cpp b/src/editor/Editor.cpp index 869781c..21c5fdd 100644 --- a/src/editor/Editor.cpp +++ b/src/editor/Editor.cpp @@ -36,7 +36,8 @@ namespace OX ImGui_ImplGlfw_InitForOpenGL(core.GetWindow().GetHandle(), true); ImGui_ImplOpenGL3_Init("#version 330 core"); - primaryViewport = new Viewport(); // The first time ive ever use the new keywork... + primaryViewport = new Viewport(); // The first time ive ever use the new keyword... + fileBrowser = new FileBrowser(); } void Editor::Update(Core &core) @@ -76,7 +77,7 @@ namespace OX ImGui::DockSpace(dockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode); LoggerWindow::Draw(); - FileBrowser::Draw(); + fileBrowser->Draw(); primaryViewport->Draw(core); @@ -159,6 +160,8 @@ namespace OX delete primaryViewport; primaryViewport = nullptr; + delete fileBrowser; + fileBrowser = nullptr; Logger::LogOk("Editor::Shutdown"); ImGui_ImplOpenGL3_Shutdown(); diff --git a/src/editor/Editor.h b/src/editor/Editor.h index 7834577..bd1b7d8 100644 --- a/src/editor/Editor.h +++ b/src/editor/Editor.h @@ -12,6 +12,7 @@ namespace OX { class Viewport; + class FileBrowser; class Editor final : public Layer { @@ -28,6 +29,7 @@ namespace OX private: Viewport* primaryViewport; + FileBrowser* fileBrowser; }; } // OX diff --git a/src/editor/Windows/FileBrowser.cpp b/src/editor/Windows/FileBrowser.cpp index 92509cd..6218871 100644 --- a/src/editor/Windows/FileBrowser.cpp +++ b/src/editor/Windows/FileBrowser.cpp @@ -1,99 +1,147 @@ +// File: src/FileBrowser.cpp #include "FileBrowser.h" -#include "systems/AssetManager.h" -#include "systems/assets/Texture2D.h" -#include "imgui.h" #include namespace OX { - static std::string s_CurrentPath = "res://"; + FileBrowser::FileBrowser() = default; - static void DrawGrid(const std::shared_ptr &node) + FileBrowser::~FileBrowser() = default; + + void FileBrowser::SetFilter(const std::string &f) { _filter = f; } + void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); } + + void FileBrowser::Draw(const char *title) { - const float thumbSize = 64.0f; - const float padding = 8.0f; - const float labelHeight = 20.0f; - const float cellWidth = thumbSize + padding * 2; - const float cellHeight = thumbSize + labelHeight + padding; + ImGui::Begin(title); - ImVec2 region = ImGui::GetContentRegionAvail(); - int columns = std::max(1, (int) (region.x / cellWidth)); + // --- toolbar now contains back button, inline path, filter & view toggle all on one row --- + DrawToolbar(); - ImGui::BeginChild("FileGrid", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding); - ImGui::Columns(columns, nullptr, false); + // main content + auto root = AssetManager::GetFileTree(); + std::function(std::shared_ptr, const std::string &)> find = + [&](auto node, const std::string &path)-> std::shared_ptr + { + if (node->path == path) return node; + for (auto &c: node->children) { + if (c->isDirectory) { + if (auto f = find(c, path)) return f; + } + } + return nullptr; + }; + auto node = find(root, _currentPath); + if (!node) { + ImGui::TextColored(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled], + "Path not found: %s", _currentPath.c_str()); + } else if (_gridMode) { + DrawGridView(node); + } else { + DrawListView(node); + } - for (const auto &child: node->children) { - ImGui::PushID(child->path.c_str()); // Unique ID per item + ImGui::End(); + } + // ——— Combined toolbar & inline path ——— + void FileBrowser::DrawToolbar() + { + // back button + if (_currentPath != "res://") { + if (ImGui::Button("Back")) { + auto pos = _currentPath.find_last_of('/'); + _currentPath = (pos != std::string::npos && pos > 6) + ? _currentPath.substr(0, pos) + : "res://"; + } + } + ImGui::SameLine(); + + // inline, non-interactive path text + ImGui::Text("%s", _currentPath.c_str()); + + ImGui::SameLine(); + + // filter input takes all remaining space + float avail = ImGui::GetContentRegionAvail().x; + ImGui::PushItemWidth(avail * 0.5f); + //ImGui::InputTextWithHint("##filter", "Filter files...", _filter.c_str(), ImGuiInputTextFlags_AutoSelectAll); + + + ImGui::PopItemWidth(); + + ImGui::SameLine(); + + // grid/list toggle + if (_gridMode) { + if (ImGui::Button("List View")) _gridMode = false; + } else { + if (ImGui::Button("Grid View")) _gridMode = true; + } + + ImGui::Separator(); + } + + // ——— Polished grid view ——— + void FileBrowser::DrawGridView(const std::shared_ptr &node) + { + const float cellW = _cfg.thumbnailSize + _cfg.padding * 2; + ImVec2 avail = ImGui::GetContentRegionAvail(); + int cols = std::max(1, int(avail.x / cellW)); + + ImGui::BeginChild("GridRegion", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding); + ImGui::Columns(cols, nullptr, false); + + for (auto &c: node->children) { + if (!_filter.empty() && !PassesFilter(c->name)) continue; + if (!c) return; + ImGui::PushID(c->path.c_str()); ImGui::BeginGroup(); - std::string tooltip; - std::string label = child->name; - ImVec2 cellSize(cellWidth, cellHeight); - - ImGui::InvisibleButton("Cell", cellSize); + // invisible button as hit target + ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding); + ImGui::InvisibleButton("cell", btnSize); bool hovered = ImGui::IsItemHovered(); bool clicked = ImGui::IsItemClicked(); - // Background - ImVec2 min = ImGui::GetItemRectMin(); - ImVec2 max = ImGui::GetItemRectMax(); - ImGui::GetWindowDrawList()->AddRectFilled(min, max, IM_COL32(40, 40, 40, 255), 4.0f); + // background rect + ImVec2 mn = ImGui::GetItemRectMin(); + ImVec2 mx = ImGui::GetItemRectMax(); + ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor; + ImGui::GetWindowDrawList() + ->AddRectFilled(mn, mx, bg, 4.0f); - ImVec2 center = ImVec2(min.x + padding, min.y + padding); - - // === Thumbnail === - if (child->isDirectory) { - ImGui::SetCursorScreenPos(center); - ImGui::Text("[Folder]"); + // thumbnail or icon + ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding}); + if (c->isDirectory) { + ImGui::Image(GetIconTexture(*c), + {_cfg.thumbnailSize, _cfg.thumbnailSize}); } else { - auto asset = AssetManager::Get(child->path); - if (asset) { - std::string type = asset->GetTypeName(); - if (type == "texture2D") { - auto tex = std::static_pointer_cast(asset); - if (tex->GetID() != 0) { - ImGui::SetCursorScreenPos(center); - ImGui::Image((ImTextureID) (intptr_t) tex->GetID(), ImVec2(thumbSize, thumbSize), - ImVec2(0, 1), ImVec2(1, 0)); - - tooltip += "Name: " + child->name + "\n"; - tooltip += "ID: " + std::to_string(tex->GetID()) + "\n"; - tooltip += "Size: " + std::to_string(tex->GetWidth()) + "x" + std::to_string( - tex->GetHeight()) + "\n"; - tooltip += "Path: " + child->path + "\n"; - - label += " (ID: " + std::to_string(tex->GetID()) + ")"; - } else { - ImGui::SetCursorScreenPos(center); - ImGui::Text("[Loading]"); - } - } else { - ImGui::SetCursorScreenPos(center); - ImGui::Text("[Type: %s]", type.c_str()); - } + auto asset = AssetManager::Get(c->path); + if (asset && asset->GetTypeName() == "texture2D") { + auto tex = std::static_pointer_cast(asset); + ImGui::Image((ImTextureID) (intptr_t) tex->GetID(), + {_cfg.thumbnailSize, _cfg.thumbnailSize}, + {0, 1}, {1, 0}); } else { - ImGui::SetCursorScreenPos(center); - ImGui::Text("[Unloaded]"); + ImGui::Dummy({_cfg.thumbnailSize, _cfg.thumbnailSize}); } } - - // === Label === - ImGui::SetCursorScreenPos(ImVec2(min.x + padding, max.y - labelHeight)); - ImGui::PushTextWrapPos(min.x + cellWidth); - ImGui::TextUnformatted(label.c_str()); + // label + ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mx.y - _cfg.labelHeight}); + ImGui::PushTextWrapPos(mx.x); + ImGui::TextUnformatted(c->name.c_str()); ImGui::PopTextWrapPos(); - // Click handling - if (clicked && child->isDirectory) { - s_CurrentPath = child->path; - } - - if (hovered && !tooltip.empty()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(tooltip.c_str()); - ImGui::EndTooltip(); + // click handling + if (clicked) { + if (c->isDirectory) { + _currentPath = c->path; + } else if (_onFileSelected) { + _onFileSelected(c->path); + } } ImGui::EndGroup(); @@ -105,49 +153,35 @@ namespace OX ImGui::EndChild(); } - - static std::shared_ptr FindNode(const std::shared_ptr &root, - const std::string &targetPath) + // ——— Simple list view ——— + void FileBrowser::DrawListView(const std::shared_ptr &node) { - if (root->path == targetPath) return root; - for (const auto &child: root->children) { - if (child->isDirectory) { - auto found = FindNode(child, targetPath); - if (found) return found; - } - } - return nullptr; - } + ImGui::BeginChild("ListRegion", ImVec2(0, 0), false); - void FileBrowser::Draw() - { - ImGui::Begin("File Browser"); - - // === Back Button === - if (s_CurrentPath != "res://") { - if (ImGui::Button("..")) { - size_t lastSlash = s_CurrentPath.find_last_of('/'); - if (lastSlash != std::string::npos && lastSlash > 6) { - // skip 'res://' - s_CurrentPath = s_CurrentPath.substr(0, lastSlash); - } else { - s_CurrentPath = "res://"; + for (auto &c: node->children) { + if (!_filter.empty() && !PassesFilter(c->name)) continue; + if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + if (c->isDirectory) { + _currentPath = c->path; + } else if (_onFileSelected) { + _onFileSelected(c->path); + } } } } - ImGui::SameLine(); - ImGui::Text("Current Path: %s", s_CurrentPath.c_str()); - ImGui::Separator(); + ImGui::EndChild(); + } - auto root = AssetManager::GetFileTree(); - auto currentNode = FindNode(root, s_CurrentPath); - if (currentNode) { - DrawGrid(currentNode); - } else { - ImGui::Text("Path not found: %s", s_CurrentPath.c_str()); - } + bool FileBrowser::PassesFilter(const std::string &name) const + { + return _filter.empty() || + (name.find(_filter) != std::string::npos); + } - ImGui::End(); + ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node) + { + return 0; } } // namespace OX diff --git a/src/editor/Windows/FileBrowser.h b/src/editor/Windows/FileBrowser.h index 1e34df9..fac5cef 100644 --- a/src/editor/Windows/FileBrowser.h +++ b/src/editor/Windows/FileBrowser.h @@ -1,12 +1,65 @@ +// File: src/FileBrowser.h #pragma once +#include #include +#include +#include +#include "systems/AssetManager.h" // for ResourceTreeNode +#include "systems/assets/Texture2D.h" // for Texture2D +#include "imgui.h" namespace OX { - class FileBrowser { - public: - static void Draw(); + /// Configuration for look & feel + struct FileBrowserConfig + { + float thumbnailSize = 64.0f; + float padding = 8.0f; + float labelHeight = 20.0f; + ImU32 bgColor = IM_COL32(40,40,40,255); + ImU32 highlightColor = IM_COL32(75,75,75,255); + bool showGrid = true; + bool showThumbnails = true; + bool allowMultiple = false; + // … add more theming or behavioral flags here }; + /// FileBrowser widget, supports grid & list, filtering, breadcrumbs, callbacks. + class FileBrowser + { + public: + using FileSelectedCallback = std::function; + + FileBrowser(); + ~FileBrowser(); + + /// Draw the entire browser window + void Draw(const char* title = "File Browser"); + + /// Set a filter string (wildcards, substrings, etc.) + void SetFilter(const std::string& filter); + + /// Called whenever the user clicks on a file (not directory) + void SetFileSelectedCallback(FileSelectedCallback cb); + + /// Access to tweak colors/sizes + FileBrowserConfig& Config() { return _cfg; } + + private: + // helpers + void DrawToolbar(); + void DrawBreadcrumbs(); + void DrawGridView(const std::shared_ptr& node); + void DrawListView(const std::shared_ptr& node); + bool PassesFilter(const std::string& name) const; + ImTextureID GetIconTexture(const ResourceTreeNode&); // stub for folder/file icons + + // state + FileBrowserConfig _cfg; + std::string _currentPath = "res://"; + std::string _filter; + bool _gridMode = true; + FileSelectedCallback _onFileSelected; + }; } // namespace OX