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.
This commit is contained in:
OusmBlueNinja 2025-05-22 11:51:28 -05:00
parent 3ecfa15e4a
commit 9641de9b2c
13 changed files with 536 additions and 97 deletions

View File

@ -100,6 +100,13 @@ add_library(Core STATIC
src/core/systems/assets/Texture2D.cpp src/core/systems/assets/Texture2D.cpp
src/core/systems/assets/Texture2D.h src/core/systems/assets/Texture2D.h
src/core/systems/Shader.cpp 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) target_include_directories(Core PUBLIC src/core)

View File

@ -16,6 +16,10 @@ namespace OX
std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets; std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets;
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap; std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
std::atomic<size_t> OX::AssetManager::s_TotalTexturesToLoad{0};
std::atomic<size_t> OX::AssetManager::s_LoadedTexturesCount{0};
// Map from an actual file path to virtual ID // Map from an actual file path to virtual ID
std::unordered_map<std::string, std::string> AssetManager::s_PathToID; std::unordered_map<std::string, std::string> AssetManager::s_PathToID;
std::mutex AssetManager::s_AssetMutex; std::mutex AssetManager::s_AssetMutex;
@ -39,13 +43,13 @@ namespace OX
s_FileTree->name = "res://"; s_FileTree->name = "res://";
s_FileTree->path = "res://"; s_FileTree->path = "res://";
s_FileTree->isDirectory = true; s_FileTree->isDirectory = true;
s_TotalTexturesToLoad.store(0);
s_LoadedTexturesCount.store(0);
s_ScanThread = std::thread(BackgroundScan); s_ScanThread = std::thread(BackgroundScan);
} }
void AssetManager::Shutdown() void AssetManager::Shutdown()
{ {
s_Scanning = false; s_Scanning = false;
if (s_ScanThread.joinable()) { if (s_ScanThread.joinable()) {
@ -62,56 +66,76 @@ namespace OX
s_FileTree->name = "res://"; s_FileTree->name = "res://";
s_FileTree->path = "res://"; s_FileTree->path = "res://";
s_FileTree->isDirectory = true; s_FileTree->isDirectory = true;
s_TotalTexturesToLoad.store(0);
s_LoadedTexturesCount.store(0);
s_ScanThread = std::thread(BackgroundScan); s_ScanThread = std::thread(BackgroundScan);
} }
void AssetManager::BackgroundScan() void AssetManager::BackgroundScan()
{ {
while (s_Scanning) { namespace fs = std::filesystem;
for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) {
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<fs::directory_entry> allEntries;
allEntries.reserve(1024);
for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) {
if (!s_Scanning) break;
allEntries.push_back(entry);
}
std::vector<std::pair<std::string, AssetMetadata> > newMetadata;
newMetadata.reserve(256);
for (auto &entry: allEntries) {
if (!s_Scanning) break;
const auto &path = entry.path(); const auto &path = entry.path();
std::string resPath = MakeVirtualPath(path); std::string resPath = MakeVirtualPath(path);
if (entry.is_directory()) { 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); AddToTree(resPath, true);
} else { continue;
{ } {
// quick check: have we already seen this asset? std::lock_guard<std::mutex> lock(s_AssetMutex);
std::lock_guard<std::mutex> lock(s_AssetMutex); if (s_MetadataMap.count(resPath))
if (s_MetadataMap.find(resPath) != s_MetadataMap.end()) continue;
continue; s_MetadataMap.emplace(resPath, AssetMetadata{"", ""});
// reserve a spot so other threads know it's in-flight }
s_MetadataMap[resPath] = {"", ""}; 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); newMetadata.emplace_back(resPath, AssetMetadata{"texture2D", path.string()}); {
if (type == "texture2D") { std::lock_guard<std::mutex> qlock(s_QueueMutex);
int w, h, ch; s_TextureQueue.push({resPath, w, h, ch, data});
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<std::mutex> lock(s_AssetMutex);
// now fill in the real metadata
s_MetadataMap[resPath] = {type, path.string()};
}
std::lock_guard<std::mutex> qlock(s_QueueMutex);
s_TextureQueue.push({resPath, w, h, ch, data});
}
} }
} }
} }
if (!newMetadata.empty()) {
std::lock_guard<std::mutex> 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; s_Scanning = false;
@ -124,6 +148,8 @@ namespace OX
while (!s_TextureQueue.empty()) { while (!s_TextureQueue.empty()) {
auto pending = s_TextureQueue.front(); auto pending = s_TextureQueue.front();
s_TextureQueue.pop(); s_TextureQueue.pop();
s_LoadedTexturesCount.fetch_add(1, std::memory_order_relaxed);
GLuint texID; GLuint texID;
glGenTextures(1, &texID); glGenTextures(1, &texID);

View File

@ -12,66 +12,85 @@
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
namespace OX { namespace OX
{
class Asset; class Asset;
struct ResourceTreeNode { struct ResourceTreeNode
{
std::string name; std::string name;
std::string path; std::string path;
bool isDirectory = false; bool isDirectory = false;
std::vector<std::shared_ptr<ResourceTreeNode>> children; std::vector<std::shared_ptr<ResourceTreeNode> > children;
}; };
class AssetManager { class AssetManager
{
public: public:
// -- Lifecycle -- // -- Lifecycle --
static void Init(const std::string& projectRoot); static void Init(const std::string &projectRoot);
static void Shutdown(); static void Shutdown();
static void Tick(); static void Tick();
static void Rescan(); static void Rescan();
static size_t GetTotalTexturesToLoad() { return s_TotalTexturesToLoad.load(); }
static size_t GetLoadedTexturesCount() { return s_LoadedTexturesCount.load(); }
// -- Queries -- // -- Queries --
// Lookup by virtual path ("res://…") or real FS path. // Lookup by virtual path ("res://…") or real FS path.
static std::shared_ptr<Asset> Get(const std::string& path); static std::shared_ptr<Asset> Get(const std::string &path);
static std::shared_ptr<ResourceTreeNode> GetFileTree(); static std::shared_ptr<ResourceTreeNode> 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: private:
struct AssetMetadata { struct AssetMetadata
{
std::string type; std::string type;
std::string absolutePath; std::string absolutePath;
}; };
struct PendingTexture {
struct PendingTexture
{
std::string id; std::string id;
int width, height, channels; int width, height, channels;
unsigned char* data; unsigned char *data;
}; };
static void BackgroundScan(); static void BackgroundScan();
static void AddToTree(const std::string& virtualPath, bool isDir);
static std::string DetectAssetType(const std::filesystem::path& path); static void AddToTree(const std::string &virtualPath, bool isDir);
static std::string MakeVirtualPath(const std::filesystem::path& full);
static std::string DetectAssetType(const std::filesystem::path &path);
static std::string MakeVirtualPath(const std::filesystem::path &full);
// loaded assets & metadata // loaded assets & metadata
static std::unordered_map<std::string, std::string> s_PathToID; static std::unordered_map<std::string, std::string> s_PathToID;
static std::unordered_map<std::string, std::shared_ptr<Asset>> s_LoadedAssets; static std::unordered_map<std::string, std::shared_ptr<Asset> > s_LoadedAssets;
static std::unordered_map<std::string, AssetMetadata> s_MetadataMap; static std::unordered_map<std::string, AssetMetadata> s_MetadataMap;
static std::mutex s_AssetMutex; static std::mutex s_AssetMutex;
// directory tree // directory tree
static std::shared_ptr<ResourceTreeNode> s_FileTree; static std::shared_ptr<ResourceTreeNode> s_FileTree;
// background scan // background scan
static std::filesystem::path s_ProjectRoot; static std::filesystem::path s_ProjectRoot;
static std::atomic<bool> s_Scanning; static std::atomic<bool> s_Scanning;
static std::thread s_ScanThread; static std::thread s_ScanThread;
// texture upload queue // texture upload queue
static std::queue<PendingTexture> s_TextureQueue; static std::queue<PendingTexture> s_TextureQueue;
static std::mutex s_QueueMutex; static std::mutex s_QueueMutex;
static std::atomic<size_t> s_TotalTexturesToLoad;
static std::atomic<size_t> s_LoadedTexturesCount;
}; };
} }

View File

@ -0,0 +1,36 @@
//
// Created by spenc on 5/22/2025.
//
#pragma once
#include <yaml-cpp/yaml.h>
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 components state into a YAML node.
[[nodiscard]] virtual YAML::Node Serialize() const = 0;
/// Restore this components state from a YAML node.
virtual void Deserialize(const YAML::Node &node) = 0;
protected:
GameObject *m_owner = nullptr;
};
};

View File

@ -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<std::string>();
}
} // namespace OX

View File

@ -0,0 +1,30 @@
// src/h/Components/TagComponent.h
#pragma once
#include <string>
#include <utility>
#include <yaml-cpp/yaml.h>
#include "Component.h"
namespace OX
{
class TagComponent : public Component
{
public:
TagComponent() = default;
explicit TagComponent(std::string tag)
: name(std::move(tag))
{
}
/// The tag string youll look up by
std::string name;
/// Serialize into YAML: writes { type: "TagComponent", name: "<your tag>" }
[[nodiscard]] YAML::Node Serialize() const override;
/// Deserialize from YAML: reads `.name` from node["name"]
void Deserialize(const YAML::Node &node) override;
};
} // namespace OX

View File

@ -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<GameObject> child)
{
child->m_parent = this;
m_children.push_back(std::move(child));
}
std::unique_ptr<GameObject> GameObject::removeChild(GameObject *child)
{
auto it = std::find_if(m_children.begin(), m_children.end(),
[child](const std::unique_ptr<GameObject> &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<std::unique_ptr<GameObject> > &GameObject::children() const
{
return m_children;
}
void GameObject::addComponent(std::unique_ptr<Component> comp)
{
comp->OnAttach(this);
m_components.push_back(std::move(comp));
}
const std::vector<std::unique_ptr<Component> > &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<std::string>();
if (node["components"]) {
for (auto &cnode: node["components"]) {
// e.g.:
// std::string type = cnode["type"].as<std::string>();
// 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<GameObject>();
child->Deserialize(cnode);
addChild(std::move(child));
}
}
}
} // namespace OX

View File

@ -0,0 +1,82 @@
// src/h/GameObject.h
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <yaml-cpp/yaml.h>
#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<GameObject> child);
std::unique_ptr<GameObject> removeChild(GameObject *child);
const std::vector<std::unique_ptr<GameObject> > &children() const;
// Components
void addComponent(std::unique_ptr<Component> comp);
template<typename T>
T *getComponent() const;
template<typename T>
std::unique_ptr<Component> removeComponent();
const std::vector<std::unique_ptr<Component> > &components() const;
// Serialization
YAML::Node Serialize() const;
void Deserialize(const YAML::Node &node);
private:
std::string m_name;
GameObject *m_parent = nullptr;
std::vector<std::unique_ptr<GameObject> > m_children;
std::vector<std::unique_ptr<Component> > m_components;
};
template<typename T>
T *GameObject::getComponent() const
{
for (auto &c: m_components) {
if (auto ptr = dynamic_cast<T *>(c.get()))
return ptr;
}
return nullptr;
}
template<typename T>
std::unique_ptr<Component> GameObject::removeComponent()
{
auto it = std::find_if(m_components.begin(), m_components.end(),
[](const std::unique_ptr<Component> &c)
{
return dynamic_cast<T *>(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

View File

@ -0,0 +1,60 @@
// src/Scene.cpp
#include "Scene.h"
#include <algorithm> // for std::find_if
namespace OX
{
GameObject *Scene::createRootObject(const std::string &name)
{
auto go = std::make_unique<GameObject>(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<GameObject> &u) { return u.get() == go; }
);
if (it == m_roots.end())
return false;
unregisterByTag(go);
m_roots.erase(it);
return true;
}
const std::vector<std::unique_ptr<GameObject> > &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<TagComponent>()) {
m_tagIndex[tc->name] = go;
}
for (auto &child: go->children()) {
registerByTag(child.get());
}
}
void Scene::unregisterByTag(GameObject *go)
{
if (auto tc = go->getComponent<TagComponent>()) {
m_tagIndex.erase(tc->name);
}
for (auto &child: go->children()) {
unregisterByTag(child.get());
}
}
} // namespace OX

View File

@ -0,0 +1,44 @@
// src/h/Scene.h
#pragma once
#include <vector>
#include <memory>
#include <unordered_map>
#include <string>
#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<std::unique_ptr<GameObject> > &roots() const;
/// O(1)
GameObject *findByTag(const std::string &tag) const;
/// Must be called whenever a GameObjects 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<std::unique_ptr<GameObject> > m_roots;
std::unordered_map<std::string, GameObject *> m_tagIndex;
};
} // namespace OX

View File

@ -44,7 +44,6 @@ namespace OX
fileBrowser = std::make_shared<FileBrowser>(); fileBrowser = std::make_shared<FileBrowser>();
primaryViewport = std::make_shared<Viewport>(); primaryViewport = std::make_shared<Viewport>();
} }
void Editor::Update(Core &core) void Editor::Update(Core &core)
@ -153,8 +152,6 @@ namespace OX
// --- Render ImGui onto FBO 0 --- // --- Render ImGui onto FBO 0 ---
{ {
ImGui::EndFrame(); ImGui::EndFrame();
ImGui::Render(); ImGui::Render();
ImGui::UpdatePlatformWindows(); ImGui::UpdatePlatformWindows();
@ -164,11 +161,8 @@ namespace OX
void Editor::Shutdown(Core &core) void Editor::Shutdown(Core &core)
{ {
ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown(); ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
} }
} // OX } // OX

View File

@ -5,40 +5,60 @@
namespace OX namespace OX
{ {
FileBrowser::FileBrowser() = default; FileBrowser::FileBrowser()
: _lastProgress(0.0f)
{
}
FileBrowser::~FileBrowser() = default; 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::SetFilter(const std::string &f) { _filter = f; }
void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); } void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); }
void FileBrowser::Draw(const char *title) void FileBrowser::Draw(const char *title)
{ {
OX_PROFILE_FUNCTION(); OX_PROFILE_FUNCTION();
ImGui::Begin(title); ImGui::Begin(title);
// --- toolbar now contains back button, inline path, filter & view toggle all on one row ---
DrawToolbar(); DrawToolbar();
// main content // ——— main content ———
auto root = AssetManager::GetFileTree(); auto root = AssetManager::GetFileTree();
std::function<std::shared_ptr<ResourceTreeNode>(std::shared_ptr<ResourceTreeNode>, const std::string &)> find = std::function<std::shared_ptr<ResourceTreeNode>(std::shared_ptr<ResourceTreeNode>, const std::string &)> find =
[&](auto node, const std::string &path)-> std::shared_ptr<ResourceTreeNode> [&](auto node, const std::string &path) -> std::shared_ptr<ResourceTreeNode>
{ {
if (node->path == path) return node; if (node->path == path)
for (auto &c: node->children) { return node;
if (c->isDirectory) { for (auto &c: node->children)
if (auto f = find(c, path)) return f; if (c->isDirectory)
} if (auto f = find(c, path))
} return f;
return nullptr; return nullptr;
}; };
auto node = find(root, _currentPath); auto node = find(root, _currentPath);
if (!node) { if (!node) {
ImGui::TextColored(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled], ImGui::TextColored(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled],
"Path not found: %s", _currentPath.c_str()); "Path not found: %s", _currentPath.c_str());
} else if (_gridMode) { } 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); DrawGridView(node);
ImGui::PopStyleVar(2);
} else { } else {
DrawListView(node); DrawListView(node);
} }
@ -49,7 +69,6 @@ namespace OX
// ——— Combined toolbar & inline path ——— // ——— Combined toolbar & inline path ———
void FileBrowser::DrawToolbar() void FileBrowser::DrawToolbar()
{ {
// back button
if (_currentPath != "res://") { if (_currentPath != "res://") {
if (ImGui::Button("Back")) { if (ImGui::Button("Back")) {
auto pos = _currentPath.find_last_of('/'); auto pos = _currentPath.find_last_of('/');
@ -57,31 +76,34 @@ namespace OX
? _currentPath.substr(0, pos) ? _currentPath.substr(0, pos)
: "res://"; : "res://";
} }
ImGui::SameLine();
} }
ImGui::SameLine();
// inline, non-interactive path text
ImGui::Text("%s", _currentPath.c_str()); ImGui::Text("%s", _currentPath.c_str());
ImGui::SameLine(); ImGui::SameLine();
// filter input takes all remaining space
float avail = ImGui::GetContentRegionAvail().x; float avail = ImGui::GetContentRegionAvail().x;
ImGui::PushItemWidth(avail * 0.5f); ImGui::PushItemWidth(avail * 0.5f);
//ImGui::InputTextWithHint("##filter", "Filter files...", _filter.c_str(), ImGuiInputTextFlags_AutoSelectAll); //ImGui::InputTextWithHint("##filter", "Filter files...", &_filter);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::SameLine(); ImGui::SameLine();
// grid/list toggle
if (_gridMode) { if (_gridMode) {
if (ImGui::Button("List View")) _gridMode = false; if (ImGui::Button("List View")) _gridMode = false;
} else { } else {
if (ImGui::Button("Grid View")) _gridMode = true; 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(); ImGui::Separator();
} }
@ -89,39 +111,44 @@ namespace OX
void FileBrowser::DrawGridView(const std::shared_ptr<ResourceTreeNode> &node) void FileBrowser::DrawGridView(const std::shared_ptr<ResourceTreeNode> &node)
{ {
OX_PROFILE_FUNCTION(); OX_PROFILE_FUNCTION();
const float cellW = _cfg.thumbnailSize + _cfg.padding * 2; const float cellW = _cfg.thumbnailSize + _cfg.padding * 2;
ImVec2 avail = ImGui::GetContentRegionAvail(); ImVec2 avail = ImGui::GetContentRegionAvail();
int cols = std::max(1, int(avail.x / cellW)); 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); ImGui::Columns(cols, nullptr, false);
std::vector<std::shared_ptr<ResourceTreeNode> > children; { std::vector<std::shared_ptr<ResourceTreeNode> > children; {
std::lock_guard<std::mutex> lock(AssetManager::s_TreeMutex); // assume you add this mutex std::lock_guard<std::mutex> lock(AssetManager::s_TreeMutex);
children = node->children; children = node->children;
} }
for (auto &c: children) {
if (!c) return;
for (auto &c: children) {
if (!c) continue;
if (!_filter.empty() && !PassesFilter(c->name)) continue; if (!_filter.empty() && !PassesFilter(c->name)) continue;
ImGui::PushID(c->path.c_str()); ImGui::PushID(c->path.c_str());
ImGui::BeginGroup(); ImGui::BeginGroup();
// invisible button as hit target
ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding); ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding);
ImGui::InvisibleButton("cell", btnSize); ImGui::InvisibleButton("cell", btnSize);
bool hovered = ImGui::IsItemHovered(); bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked(); bool clicked = ImGui::IsItemClicked();
// background rect // background
ImVec2 mn = ImGui::GetItemRectMin(); ImVec2 mn = ImGui::GetItemRectMin();
ImVec2 mx = ImGui::GetItemRectMax(); ImVec2 mx = ImGui::GetItemRectMax();
ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor; ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor;
ImGui::GetWindowDrawList() ImGui::GetWindowDrawList()
->AddRectFilled(mn, mx, bg, 4.0f); ->AddRectFilled(mn, mx, bg, 4.0f);
// thumbnail or icon // thumbnail
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding}); ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding});
if (c->isDirectory) { if (c->isDirectory) {
ImGui::Image(GetIconTexture(*c), ImGui::Image(GetIconTexture(*c),
@ -146,11 +173,10 @@ namespace OX
// click handling // click handling
if (clicked) { if (clicked) {
if (c->isDirectory) { if (c->isDirectory)
_currentPath = c->path; _currentPath = c->path;
} else if (_onFileSelected) { else if (_onFileSelected)
_onFileSelected(c->path); _onFileSelected(c->path);
}
} }
ImGui::EndGroup(); ImGui::EndGroup();
@ -171,11 +197,10 @@ namespace OX
if (!_filter.empty() && !PassesFilter(c->name)) continue; if (!_filter.empty() && !PassesFilter(c->name)) continue;
if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) {
if (ImGui::IsMouseDoubleClicked(0)) { if (ImGui::IsMouseDoubleClicked(0)) {
if (c->isDirectory) { if (c->isDirectory)
_currentPath = c->path; _currentPath = c->path;
} else if (_onFileSelected) { else if (_onFileSelected)
_onFileSelected(c->path); _onFileSelected(c->path);
}
} }
} }
} }
@ -185,8 +210,7 @@ namespace OX
bool FileBrowser::PassesFilter(const std::string &name) const bool FileBrowser::PassesFilter(const std::string &name) const
{ {
return _filter.empty() || return _filter.empty() || (name.find(_filter) != std::string::npos);
(name.find(_filter) != std::string::npos);
} }
ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node) ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node)

View File

@ -43,6 +43,8 @@ namespace OX {
/// Called whenever the user clicks on a file (not directory) /// Called whenever the user clicks on a file (not directory)
void SetFileSelectedCallback(FileSelectedCallback cb); void SetFileSelectedCallback(FileSelectedCallback cb);
/// Access to tweak colors/sizes /// Access to tweak colors/sizes
FileBrowserConfig& Config() { return _cfg; } FileBrowserConfig& Config() { return _cfg; }
@ -52,7 +54,8 @@ namespace OX {
void DrawBreadcrumbs(); void DrawBreadcrumbs();
void DrawGridView(const std::shared_ptr<ResourceTreeNode>& node); void DrawGridView(const std::shared_ptr<ResourceTreeNode>& node);
void DrawListView(const std::shared_ptr<ResourceTreeNode>& node); void DrawListView(const std::shared_ptr<ResourceTreeNode>& 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 ImTextureID GetIconTexture(const ResourceTreeNode&); // stub for folder/file icons
// state // state
@ -61,5 +64,8 @@ namespace OX {
std::string _filter; std::string _filter;
bool _gridMode = true; bool _gridMode = true;
FileSelectedCallback _onFileSelected; FileSelectedCallback _onFileSelected;
float _lastProgress;
size_t _maxQueueSize;
}; };
} // namespace OX } // namespace OX