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.
This commit is contained in:
OusmBlueNinja 2025-05-21 16:38:18 -05:00
parent 7cee708801
commit c177c8b9ea
7 changed files with 442 additions and 278 deletions

View File

@ -1,189 +1,252 @@
// AssetManager.cpp
#include "AssetManager.h"
#include "assets/Texture2D.h"
#include "Logger.h"
#include <stb/stb_image.h>
#include <GL/glew.h>
#include <fstream>
#include <yaml-cpp/yaml.h>
#include <fstream>
namespace fs = std::filesystem;
namespace OX {
namespace OX
{
// statics
std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets;
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
// Map from actual file path to virtual ID
std::unordered_map<std::string, std::string> AssetManager::s_PathToID;
std::mutex AssetManager::s_AssetMutex;
std::unordered_map<std::string, std::shared_ptr<Asset>> AssetManager::s_LoadedAssets;
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
std::mutex AssetManager::s_TextureQueueMutex;
std::queue<AssetManager::PendingTexture> AssetManager::s_TextureUploadQueue;
std::filesystem::path AssetManager::s_ProjectRoot;
std::atomic<bool> AssetManager::s_Scanning{false};
std::thread AssetManager::s_ScanThread;
fs::path AssetManager::s_ProjectRoot;
std::atomic<bool> AssetManager::s_Scanning = false;
std::thread AssetManager::s_ScanThread;
std::queue<AssetManager::PendingTexture> 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<ResourceTreeNode>();
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<std::mutex> 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<Texture2D>();
tex->SetFromGL(texID, pending.width, pending.height);
s_LoadedAssets[pending.id] = tex;
Logger::LogInfo("%u | %u", tex->GetID(), texID);
s_FileTree = std::make_shared<ResourceTreeNode>();
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<ResourceTreeNode>();
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<ResourceTreeNode>();
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<std::mutex> 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<std::mutex> 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<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});
}
}
}
}
}
s_Scanning = false;
}
}
void AssetManager::AddToTree(const std::string& resPath, bool isDir) {
std::vector<std::string> 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<std::mutex> 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<Texture2D>();
tex->SetFromGL(texID, pending.width, pending.height);
std::lock_guard<std::mutex> 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<ResourceTreeNode>();
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<Asset> 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<std::mutex> lock(s_AssetMutex);
std::shared_ptr<Asset> 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<ResourceTreeNode> 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<std::mutex> 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<ResourceTreeNode> 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<std::string> 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<ResourceTreeNode>();
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

View File

@ -1,16 +1,18 @@
/// AssetManager.h
#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <mutex>
#include <filesystem>
#include <thread>
#include <atomic>
#include <vector>
#include <cstdint>
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<Asset> Get(const std::string& resPath);
// -- Queries --
// Lookup by virtual path ("res://…") or real FS path.
static std::shared_ptr<Asset> Get(const std::string& path);
static std::shared_ptr<ResourceTreeNode> 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<std::string, std::string> s_PathToID;
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;
// directory tree
static std::shared_ptr<ResourceTreeNode> s_FileTree;
static std::mutex s_TextureQueueMutex;
static std::queue<PendingTexture> s_TextureUploadQueue;
// background scan
static std::filesystem::path s_ProjectRoot;
static std::atomic<bool> s_Scanning;
static std::thread s_ScanThread;
};
static std::atomic<bool> s_Scanning;
static std::thread s_ScanThread;
} // namespace OX
// texture upload queue
static std::queue<PendingTexture> s_TextureQueue;
static std::mutex s_QueueMutex;
};
}

View File

@ -4,7 +4,6 @@
#pragma once
#include "systems/Asset.h"
#include <string>
#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; }

View File

@ -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();

View File

@ -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

View File

@ -1,99 +1,147 @@
// File: src/FileBrowser.cpp
#include "FileBrowser.h"
#include "systems/AssetManager.h"
#include "systems/assets/Texture2D.h"
#include "imgui.h"
#include <filesystem>
namespace OX
{
static std::string s_CurrentPath = "res://";
FileBrowser::FileBrowser() = default;
static void DrawGrid(const std::shared_ptr<ResourceTreeNode> &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<ResourceTreeNode>(std::shared_ptr<ResourceTreeNode>, const std::string &)> find =
[&](auto node, const std::string &path)-> std::shared_ptr<ResourceTreeNode>
{
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<ResourceTreeNode> &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<Texture2D>(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<Texture2D>(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<ResourceTreeNode> FindNode(const std::shared_ptr<ResourceTreeNode> &root,
const std::string &targetPath)
// ——— Simple list view ———
void FileBrowser::DrawListView(const std::shared_ptr<ResourceTreeNode> &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

View File

@ -1,12 +1,65 @@
// File: src/FileBrowser.h
#pragma once
#include <functional>
#include <string>
#include <vector>
#include <memory>
#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<void(const std::string& path)>;
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<ResourceTreeNode>& node);
void DrawListView(const std::shared_ptr<ResourceTreeNode>& 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