From 9641de9b2cbd8c309ab7d4df7d31fc2cf8fba99c Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Thu, 22 May 2025 11:51:28 -0500 Subject: [PATCH] Improves asset loading and scene management Adds scene management system with GameObjects and Components. Improves asset loading by scanning assets in a background thread and queuing textures for upload. Also displays a progress bar in the file browser for texture loading. Adds basic component system with TagComponent to identify GameObjects by a user defined name. --- CMakeLists.txt | 7 ++ src/core/systems/AssetManager.cpp | 96 ++++++++++++------- src/core/systems/AssetManager.h | 59 ++++++++---- src/core/systems/Scene/Components/Component.h | 36 +++++++ .../systems/Scene/Components/TagComponent.cpp | 23 +++++ .../systems/Scene/Components/TagComponent.h | 30 ++++++ src/core/systems/Scene/GameObject.cpp | 88 +++++++++++++++++ src/core/systems/Scene/GameObject.h | 82 ++++++++++++++++ src/core/systems/Scene/Scene.cpp | 60 ++++++++++++ src/core/systems/Scene/Scene.h | 44 +++++++++ src/editor/Editor.cpp | 6 -- src/editor/Windows/FileBrowser.cpp | 94 +++++++++++------- src/editor/Windows/FileBrowser.h | 8 +- 13 files changed, 536 insertions(+), 97 deletions(-) create mode 100644 src/core/systems/Scene/Components/Component.h create mode 100644 src/core/systems/Scene/Components/TagComponent.cpp create mode 100644 src/core/systems/Scene/Components/TagComponent.h create mode 100644 src/core/systems/Scene/GameObject.cpp create mode 100644 src/core/systems/Scene/GameObject.h create mode 100644 src/core/systems/Scene/Scene.cpp create mode 100644 src/core/systems/Scene/Scene.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eb023da..2a64798 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,13 @@ add_library(Core STATIC src/core/systems/assets/Texture2D.cpp src/core/systems/assets/Texture2D.h src/core/systems/Shader.cpp + src/core/systems/Scene/Scene.cpp + src/core/systems/Scene/Scene.h + src/core/systems/Scene/GameObject.cpp + src/core/systems/Scene/GameObject.h + src/core/systems/Scene/Components/TagComponent.cpp + src/core/systems/Scene/Components/TagComponent.h + src/core/systems/Scene/Components/Component.h ) target_include_directories(Core PUBLIC src/core) diff --git a/src/core/systems/AssetManager.cpp b/src/core/systems/AssetManager.cpp index da64161..ff63263 100644 --- a/src/core/systems/AssetManager.cpp +++ b/src/core/systems/AssetManager.cpp @@ -16,6 +16,10 @@ namespace OX std::unordered_map > AssetManager::s_LoadedAssets; std::unordered_map AssetManager::s_MetadataMap; + std::atomic OX::AssetManager::s_TotalTexturesToLoad{0}; + std::atomic OX::AssetManager::s_LoadedTexturesCount{0}; + + // Map from an actual file path to virtual ID std::unordered_map AssetManager::s_PathToID; std::mutex AssetManager::s_AssetMutex; @@ -39,13 +43,13 @@ namespace OX s_FileTree->name = "res://"; s_FileTree->path = "res://"; s_FileTree->isDirectory = true; - + s_TotalTexturesToLoad.store(0); + s_LoadedTexturesCount.store(0); s_ScanThread = std::thread(BackgroundScan); } void AssetManager::Shutdown() { - s_Scanning = false; if (s_ScanThread.joinable()) { @@ -62,56 +66,76 @@ namespace OX s_FileTree->name = "res://"; s_FileTree->path = "res://"; s_FileTree->isDirectory = true; - + s_TotalTexturesToLoad.store(0); + s_LoadedTexturesCount.store(0); s_ScanThread = std::thread(BackgroundScan); } void AssetManager::BackgroundScan() { - while (s_Scanning) { - for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) { + namespace fs = std::filesystem; - if (!s_Scanning) break; // Note: This is needed or shutdown takes ages. + size_t totalTextures = 0; + for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) { + if (!s_Scanning) break; + if (!entry.is_directory()) { + if (DetectAssetType(entry.path()) == "texture2D") + ++totalTextures; + } + } + s_TotalTexturesToLoad.store(totalTextures, std::memory_order_relaxed); + s_LoadedTexturesCount.store(0, std::memory_order_relaxed); + + + while (s_Scanning) { + std::vector allEntries; + allEntries.reserve(1024); + for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) { + if (!s_Scanning) break; + allEntries.push_back(entry); + } + + std::vector > newMetadata; + newMetadata.reserve(256); + + for (auto &entry: allEntries) { + if (!s_Scanning) break; const auto &path = entry.path(); std::string resPath = MakeVirtualPath(path); 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] = {"", ""}; - } + continue; + } { + std::lock_guard lock(s_AssetMutex); + if (s_MetadataMap.count(resPath)) + continue; + s_MetadataMap.emplace(resPath, AssetMetadata{"", ""}); + } + AddToTree(resPath, false); - AddToTree(resPath, false); + if (DetectAssetType(path) == "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) continue; - 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}); - } + newMetadata.emplace_back(resPath, AssetMetadata{"texture2D", path.string()}); { + std::lock_guard qlock(s_QueueMutex); + s_TextureQueue.push({resPath, w, h, ch, data}); } } } + + if (!newMetadata.empty()) { + std::lock_guard lock(s_AssetMutex); + for (auto &kv: newMetadata) + s_MetadataMap[kv.first] = kv.second; + } + + if (s_Scanning) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } s_Scanning = false; @@ -124,6 +148,8 @@ namespace OX while (!s_TextureQueue.empty()) { auto pending = s_TextureQueue.front(); s_TextureQueue.pop(); + s_LoadedTexturesCount.fetch_add(1, std::memory_order_relaxed); + GLuint texID; glGenTextures(1, &texID); diff --git a/src/core/systems/AssetManager.h b/src/core/systems/AssetManager.h index d0759b0..2fba500 100644 --- a/src/core/systems/AssetManager.h +++ b/src/core/systems/AssetManager.h @@ -12,66 +12,85 @@ #include #include -namespace OX { +namespace OX +{ class Asset; - struct ResourceTreeNode { + struct ResourceTreeNode + { std::string name; std::string path; bool isDirectory = false; - std::vector> children; + std::vector > children; }; - class AssetManager { + class AssetManager + { public: // -- Lifecycle -- - static void Init(const std::string& projectRoot); + static void Init(const std::string &projectRoot); + static void Shutdown(); + static void Tick(); + static void Rescan(); + + static size_t GetTotalTexturesToLoad() { return s_TotalTexturesToLoad.load(); } + static size_t GetLoadedTexturesCount() { return s_LoadedTexturesCount.load(); } + // -- Queries -- // Lookup by virtual path ("res://…") or real FS path. - static std::shared_ptr Get(const std::string& path); + static std::shared_ptr Get(const std::string &path); static std::shared_ptr GetFileTree(); - static void SaveAssetPack(const std::string& outputPath); - static std::mutex s_TreeMutex; + static void SaveAssetPack(const std::string &outputPath); + static std::mutex s_TreeMutex; private: - struct AssetMetadata { + struct AssetMetadata + { std::string type; std::string absolutePath; }; - struct PendingTexture { + + struct PendingTexture + { std::string id; int width, height, channels; - unsigned char* data; + unsigned char *data; }; static void BackgroundScan(); - static void AddToTree(const std::string& virtualPath, bool isDir); - static std::string DetectAssetType(const std::filesystem::path& path); - static std::string MakeVirtualPath(const std::filesystem::path& full); + + static void AddToTree(const std::string &virtualPath, bool isDir); + + 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::mutex s_AssetMutex; + static std::unordered_map > s_LoadedAssets; + static std::unordered_map s_MetadataMap; + static std::mutex s_AssetMutex; // directory tree static std::shared_ptr s_FileTree; // 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; // texture upload queue static std::queue s_TextureQueue; - static std::mutex s_QueueMutex; + static std::mutex s_QueueMutex; + + static std::atomic s_TotalTexturesToLoad; + static std::atomic s_LoadedTexturesCount; }; } diff --git a/src/core/systems/Scene/Components/Component.h b/src/core/systems/Scene/Components/Component.h new file mode 100644 index 0000000..8172b49 --- /dev/null +++ b/src/core/systems/Scene/Components/Component.h @@ -0,0 +1,36 @@ +// +// Created by spenc on 5/22/2025. +// + +#pragma once + +#include + +namespace OX +{ + class GameObject; + + class Component + { + public: + virtual ~Component() = default; + + /// Called when this component is attached to a GameObject. + virtual void OnAttach(GameObject *owner) { m_owner = owner; } + + /// Called just before this component is detached or destroyed. + virtual void OnDetach() { m_owner = nullptr; } + + /// Returns the GameObject this component is attached to (or nullptr). + [[nodiscard]] GameObject *GetOwner() const { return m_owner; } + + /// Serialize this component’s state into a YAML node. + [[nodiscard]] virtual YAML::Node Serialize() const = 0; + + /// Restore this component’s state from a YAML node. + virtual void Deserialize(const YAML::Node &node) = 0; + + protected: + GameObject *m_owner = nullptr; + }; +}; diff --git a/src/core/systems/Scene/Components/TagComponent.cpp b/src/core/systems/Scene/Components/TagComponent.cpp new file mode 100644 index 0000000..98a98c6 --- /dev/null +++ b/src/core/systems/Scene/Components/TagComponent.cpp @@ -0,0 +1,23 @@ +// +// Created by spenc on 5/22/2025. +// + +// src/Components/TagComponent.cpp +#include "TagComponent.h" + +namespace OX +{ + YAML::Node TagComponent::Serialize() const + { + YAML::Node node; + node["type"] = "TagComponent"; + node["name"] = name; + return node; + } + + void TagComponent::Deserialize(const YAML::Node &node) + { + if (node["name"]) + name = node["name"].as(); + } +} // namespace OX diff --git a/src/core/systems/Scene/Components/TagComponent.h b/src/core/systems/Scene/Components/TagComponent.h new file mode 100644 index 0000000..9fa2175 --- /dev/null +++ b/src/core/systems/Scene/Components/TagComponent.h @@ -0,0 +1,30 @@ +// src/h/Components/TagComponent.h +#pragma once + +#include +#include +#include +#include "Component.h" + +namespace OX +{ + class TagComponent : public Component + { + public: + TagComponent() = default; + + explicit TagComponent(std::string tag) + : name(std::move(tag)) + { + } + + /// The tag string you’ll look up by + std::string name; + + /// Serialize into YAML: writes { type: "TagComponent", name: "" } + [[nodiscard]] YAML::Node Serialize() const override; + + /// Deserialize from YAML: reads `.name` from node["name"] + void Deserialize(const YAML::Node &node) override; + }; +} // namespace OX diff --git a/src/core/systems/Scene/GameObject.cpp b/src/core/systems/Scene/GameObject.cpp new file mode 100644 index 0000000..500ebb9 --- /dev/null +++ b/src/core/systems/Scene/GameObject.cpp @@ -0,0 +1,88 @@ +// src/GameObject.cpp +#include "GameObject.h" + +namespace OX +{ + GameObject::GameObject(const std::string &name) + : m_name(name) + { + } + + GameObject::~GameObject() = default; + + void GameObject::setName(const std::string &name) { m_name = name; } + const std::string &GameObject::name() const { return m_name; } + + GameObject *GameObject::parent() const { return m_parent; } + + void GameObject::addChild(std::unique_ptr child) + { + child->m_parent = this; + m_children.push_back(std::move(child)); + } + + std::unique_ptr GameObject::removeChild(GameObject *child) + { + auto it = std::find_if(m_children.begin(), m_children.end(), + [child](const std::unique_ptr &u) { return u.get() == child; }); + if (it == m_children.end()) return nullptr; + auto out = std::move(*it); + out->m_parent = nullptr; + m_children.erase(it); + return out; + } + + const std::vector > &GameObject::children() const + { + return m_children; + } + + void GameObject::addComponent(std::unique_ptr comp) + { + comp->OnAttach(this); + m_components.push_back(std::move(comp)); + } + + const std::vector > &GameObject::components() const + { + return m_components; + } + + YAML::Node GameObject::Serialize() const + { + YAML::Node node; + node["name"] = m_name; + + for (auto &comp: m_components) + node["components"].push_back(comp->Serialize()); + + for (auto &child: m_children) + node["children"].push_back(child->Serialize()); + + return node; + } + + void GameObject::Deserialize(const YAML::Node &node) + { + if (node["name"]) + m_name = node["name"].as(); + + if (node["components"]) { + for (auto &cnode: node["components"]) { + // e.g.: + // std::string type = cnode["type"].as(); + // auto comp = ComponentFactory::Create(type); + // comp->Deserialize(cnode); + // addComponent(std::move(comp)); + } + } + + if (node["children"]) { + for (auto &cnode: node["children"]) { + auto child = std::make_unique(); + child->Deserialize(cnode); + addChild(std::move(child)); + } + } + } +} // namespace OX diff --git a/src/core/systems/Scene/GameObject.h b/src/core/systems/Scene/GameObject.h new file mode 100644 index 0000000..5b06c13 --- /dev/null +++ b/src/core/systems/Scene/GameObject.h @@ -0,0 +1,82 @@ +// src/h/GameObject.h +#pragma once + +#include +#include +#include +#include +#include "Components/Component.h" + +namespace OX +{ + class GameObject + { + public: + explicit GameObject(const std::string &name = ""); + + ~GameObject(); + + // Name + void setName(const std::string &name); + + const std::string &name() const; + + // Hierarchy + GameObject *parent() const; + + void addChild(std::unique_ptr child); + + std::unique_ptr removeChild(GameObject *child); + + const std::vector > &children() const; + + // Components + void addComponent(std::unique_ptr comp); + + template + T *getComponent() const; + + template + std::unique_ptr removeComponent(); + + const std::vector > &components() const; + + // Serialization + YAML::Node Serialize() const; + + void Deserialize(const YAML::Node &node); + + private: + std::string m_name; + GameObject *m_parent = nullptr; + std::vector > m_children; + std::vector > m_components; + }; + + template + T *GameObject::getComponent() const + { + for (auto &c: m_components) { + if (auto ptr = dynamic_cast(c.get())) + return ptr; + } + return nullptr; + } + + template + std::unique_ptr GameObject::removeComponent() + { + auto it = std::find_if(m_components.begin(), m_components.end(), + [](const std::unique_ptr &c) + { + return dynamic_cast(c.get()) != nullptr; + }); + if (it == m_components.end()) + return nullptr; + + auto out = std::move(*it); + out->OnDetach(); + m_components.erase(it); + return out; + } +} // namespace OX diff --git a/src/core/systems/Scene/Scene.cpp b/src/core/systems/Scene/Scene.cpp new file mode 100644 index 0000000..2b579de --- /dev/null +++ b/src/core/systems/Scene/Scene.cpp @@ -0,0 +1,60 @@ +// src/Scene.cpp +#include "Scene.h" +#include // for std::find_if + +namespace OX +{ + GameObject *Scene::createRootObject(const std::string &name) + { + auto go = std::make_unique(name); + GameObject *ptr = go.get(); + registerByTag(ptr); + m_roots.push_back(std::move(go)); + return ptr; + } + + bool Scene::removeRootObject(GameObject *go) + { + auto it = std::find_if( + m_roots.begin(), m_roots.end(), + [go](const std::unique_ptr &u) { return u.get() == go; } + ); + if (it == m_roots.end()) + return false; + + unregisterByTag(go); + m_roots.erase(it); + return true; + } + + const std::vector > &Scene::roots() const + { + return m_roots; + } + + GameObject *Scene::findByTag(const std::string &tag) const + { + auto it = m_tagIndex.find(tag); + return (it != m_tagIndex.end()) ? it->second : nullptr; + } + + void Scene::registerByTag(GameObject *go) + { + if (auto tc = go->getComponent()) { + m_tagIndex[tc->name] = go; + } + for (auto &child: go->children()) { + registerByTag(child.get()); + } + } + + void Scene::unregisterByTag(GameObject *go) + { + if (auto tc = go->getComponent()) { + m_tagIndex.erase(tc->name); + } + for (auto &child: go->children()) { + unregisterByTag(child.get()); + } + } +} // namespace OX diff --git a/src/core/systems/Scene/Scene.h b/src/core/systems/Scene/Scene.h new file mode 100644 index 0000000..8e73605 --- /dev/null +++ b/src/core/systems/Scene/Scene.h @@ -0,0 +1,44 @@ +// src/h/Scene.h +#pragma once + +#include +#include +#include +#include + +#include "GameObject.h" +#include "Components/TagComponent.h" +#include "systems/MACROS.h" + +namespace OX +{ + class Scene + { + public: + Scene() = default; + + ~Scene() = default; + + /// Create and own a new root GameObject. Returns the raw pointer. + GameObject *createRootObject(const std::string &name = ""); + + /// Remove (and destroy) a root object by pointer. + bool removeRootObject(GameObject *go); + + /// Access all root objects. + const std::vector > &roots() const; + + /// O(1) + GameObject *findByTag(const std::string &tag) const; + + /// Must be called whenever a GameObject’s tag is set or changed. + void registerByTag(GameObject *go); + + /// Must be called before a GameObject is destroyed or its tag changes. + void unregisterByTag(GameObject *go); + + private: + std::vector > m_roots; + std::unordered_map m_tagIndex; + }; +} // namespace OX diff --git a/src/editor/Editor.cpp b/src/editor/Editor.cpp index eefabca..ab499e8 100644 --- a/src/editor/Editor.cpp +++ b/src/editor/Editor.cpp @@ -44,7 +44,6 @@ namespace OX fileBrowser = std::make_shared(); primaryViewport = std::make_shared(); - } void Editor::Update(Core &core) @@ -153,8 +152,6 @@ namespace OX // --- Render ImGui onto FBO 0 --- { - - ImGui::EndFrame(); ImGui::Render(); ImGui::UpdatePlatformWindows(); @@ -164,11 +161,8 @@ namespace OX void Editor::Shutdown(Core &core) { - - ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); - } } // OX diff --git a/src/editor/Windows/FileBrowser.cpp b/src/editor/Windows/FileBrowser.cpp index 9a89170..6d830a0 100644 --- a/src/editor/Windows/FileBrowser.cpp +++ b/src/editor/Windows/FileBrowser.cpp @@ -5,40 +5,60 @@ namespace OX { - FileBrowser::FileBrowser() = default; + FileBrowser::FileBrowser() + : _lastProgress(0.0f) + { + } FileBrowser::~FileBrowser() = default; + // Call this every frame to report your current loading progress (0.0 → 1.0). + // Passing in 0 resets it and hides the bar. + void FileBrowser::SetProgress(const float p) + { + if (p <= 0.0f) { + _lastProgress = 0.0f; + } else { + _lastProgress = std::max(_lastProgress, p); + } + } + void FileBrowser::SetFilter(const std::string &f) { _filter = f; } void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); } void FileBrowser::Draw(const char *title) { OX_PROFILE_FUNCTION(); + + ImGui::Begin(title); - // --- toolbar now contains back button, inline path, filter & view toggle all on one row --- DrawToolbar(); - // main content + // ——— main content ——— auto root = AssetManager::GetFileTree(); std::function(std::shared_ptr, const std::string &)> find = - [&](auto node, const std::string &path)-> std::shared_ptr + [&](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; - } - } + 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) { + // ——— Grid view with nicer spacing ——— + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(_cfg.padding, _cfg.padding)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(_cfg.padding, _cfg.padding)); DrawGridView(node); + ImGui::PopStyleVar(2); } else { DrawListView(node); } @@ -49,7 +69,6 @@ namespace OX // ——— Combined toolbar & inline path ——— void FileBrowser::DrawToolbar() { - // back button if (_currentPath != "res://") { if (ImGui::Button("Back")) { auto pos = _currentPath.find_last_of('/'); @@ -57,31 +76,34 @@ namespace OX ? _currentPath.substr(0, pos) : "res://"; } + ImGui::SameLine(); } - 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::InputTextWithHint("##filter", "Filter files...", &_filter); ImGui::PopItemWidth(); - ImGui::SameLine(); - // grid/list toggle if (_gridMode) { if (ImGui::Button("List View")) _gridMode = false; } else { if (ImGui::Button("Grid View")) _gridMode = true; } + size_t total = AssetManager::GetTotalTexturesToLoad(); + size_t loaded = AssetManager::GetLoadedTexturesCount(); + + if (total > 0 && loaded < total) { + float progress = float(loaded) / float(total); + ImGui::SameLine(); + ImGui::ProgressBar(progress, ImVec2(-1, 0)); + } + + ImGui::Separator(); } @@ -89,39 +111,44 @@ namespace OX void FileBrowser::DrawGridView(const std::shared_ptr &node) { OX_PROFILE_FUNCTION(); + 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); + // Add a little padding around the child region + ImGui::BeginChild("GridRegion", + ImVec2(0, 0), + false, + ImGuiWindowFlags_AlwaysUseWindowPadding); + ImGui::Columns(cols, nullptr, false); std::vector > children; { - std::lock_guard lock(AssetManager::s_TreeMutex); // assume you add this mutex + std::lock_guard lock(AssetManager::s_TreeMutex); children = node->children; } - for (auto &c: children) { - if (!c) return; + for (auto &c: children) { + if (!c) continue; if (!_filter.empty() && !PassesFilter(c->name)) continue; ImGui::PushID(c->path.c_str()); ImGui::BeginGroup(); - // 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 rect + // background ImVec2 mn = ImGui::GetItemRectMin(); ImVec2 mx = ImGui::GetItemRectMax(); ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor; ImGui::GetWindowDrawList() ->AddRectFilled(mn, mx, bg, 4.0f); - // thumbnail or icon + // thumbnail ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding}); if (c->isDirectory) { ImGui::Image(GetIconTexture(*c), @@ -146,11 +173,10 @@ namespace OX // click handling if (clicked) { - if (c->isDirectory) { + if (c->isDirectory) _currentPath = c->path; - } else if (_onFileSelected) { + else if (_onFileSelected) _onFileSelected(c->path); - } } ImGui::EndGroup(); @@ -171,11 +197,10 @@ namespace OX if (!_filter.empty() && !PassesFilter(c->name)) continue; if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { - if (c->isDirectory) { + if (c->isDirectory) _currentPath = c->path; - } else if (_onFileSelected) { + else if (_onFileSelected) _onFileSelected(c->path); - } } } } @@ -185,8 +210,7 @@ namespace OX bool FileBrowser::PassesFilter(const std::string &name) const { - return _filter.empty() || - (name.find(_filter) != std::string::npos); + return _filter.empty() || (name.find(_filter) != std::string::npos); } ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node) diff --git a/src/editor/Windows/FileBrowser.h b/src/editor/Windows/FileBrowser.h index fac5cef..c1feb94 100644 --- a/src/editor/Windows/FileBrowser.h +++ b/src/editor/Windows/FileBrowser.h @@ -43,6 +43,8 @@ namespace OX { /// Called whenever the user clicks on a file (not directory) void SetFileSelectedCallback(FileSelectedCallback cb); + + /// Access to tweak colors/sizes FileBrowserConfig& Config() { return _cfg; } @@ -52,7 +54,8 @@ namespace OX { void DrawBreadcrumbs(); void DrawGridView(const std::shared_ptr& node); void DrawListView(const std::shared_ptr& node); - bool PassesFilter(const std::string& name) const; + [[nodiscard]] bool PassesFilter(const std::string& name) const; + void SetProgress(float p); ImTextureID GetIconTexture(const ResourceTreeNode&); // stub for folder/file icons // state @@ -61,5 +64,8 @@ namespace OX { std::string _filter; bool _gridMode = true; FileSelectedCallback _onFileSelected; + float _lastProgress; + size_t _maxQueueSize; + }; } // namespace OX