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:
parent
7cee708801
commit
c177c8b9ea
@ -1,189 +1,252 @@
|
|||||||
|
// AssetManager.cpp
|
||||||
#include "AssetManager.h"
|
#include "AssetManager.h"
|
||||||
#include "assets/Texture2D.h"
|
#include "assets/Texture2D.h"
|
||||||
#include "Logger.h"
|
#include "Logger.h"
|
||||||
|
|
||||||
#include <stb/stb_image.h>
|
#include <stb/stb_image.h>
|
||||||
#include <GL/glew.h>
|
#include <GL/glew.h>
|
||||||
#include <fstream>
|
|
||||||
#include <yaml-cpp/yaml.h>
|
#include <yaml-cpp/yaml.h>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||||
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
|
|
||||||
std::shared_ptr<ResourceTreeNode> AssetManager::s_FileTree = std::make_shared<ResourceTreeNode>();
|
|
||||||
|
|
||||||
std::mutex AssetManager::s_TextureQueueMutex;
|
std::filesystem::path AssetManager::s_ProjectRoot;
|
||||||
std::queue<AssetManager::PendingTexture> AssetManager::s_TextureUploadQueue;
|
std::atomic<bool> AssetManager::s_Scanning{false};
|
||||||
|
std::thread AssetManager::s_ScanThread;
|
||||||
|
|
||||||
fs::path AssetManager::s_ProjectRoot;
|
std::queue<AssetManager::PendingTexture> AssetManager::s_TextureQueue;
|
||||||
std::atomic<bool> AssetManager::s_Scanning = false;
|
std::mutex AssetManager::s_QueueMutex;
|
||||||
std::thread AssetManager::s_ScanThread;
|
|
||||||
|
|
||||||
void AssetManager::Init(const std::string& projectRoot) {
|
void AssetManager::Init(const std::string &projectRoot)
|
||||||
s_ProjectRoot = fs::absolute(projectRoot);
|
{
|
||||||
s_Scanning = true;
|
s_ProjectRoot = fs::absolute(projectRoot);
|
||||||
|
s_Scanning = true;
|
||||||
|
|
||||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||||
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_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_ScanThread = std::thread(BackgroundScan);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void AssetManager::Rescan() {
|
void AssetManager::Shutdown()
|
||||||
if (s_Scanning) return;
|
{
|
||||||
s_Scanning = true;
|
if (s_ScanThread.joinable()) {
|
||||||
|
s_ScanThread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s_FileTree = std::make_shared<ResourceTreeNode>();
|
void AssetManager::Rescan()
|
||||||
s_FileTree->name = "res://";
|
{
|
||||||
s_FileTree->path = "res://";
|
if (s_Scanning) return;
|
||||||
s_FileTree->isDirectory = true;
|
s_Scanning = true;
|
||||||
|
|
||||||
s_ScanThread = std::thread([=] {
|
s_FileTree = std::make_shared<ResourceTreeNode>();
|
||||||
BackgroundScan(s_ProjectRoot);
|
s_FileTree->name = "res://";
|
||||||
s_Scanning = false;
|
s_FileTree->path = "res://";
|
||||||
});
|
s_FileTree->isDirectory = true;
|
||||||
}
|
|
||||||
|
|
||||||
void AssetManager::BackgroundScan(const fs::path& root) {
|
s_ScanThread = std::thread(BackgroundScan);
|
||||||
for (const auto& entry : fs::recursive_directory_iterator(root)) {
|
}
|
||||||
const auto& path = entry.path();
|
|
||||||
std::string resPath = MakeVirtualPath(path);
|
|
||||||
|
|
||||||
if (entry.is_directory()) {
|
void AssetManager::BackgroundScan()
|
||||||
AddToTree(resPath, true);
|
{
|
||||||
} else {
|
while (s_Scanning) {
|
||||||
AddToTree(resPath, false);
|
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 (entry.is_directory()) {
|
||||||
if (!type.empty()) {
|
// you can also guard directories if you like:
|
||||||
if (type == "texture2D") {
|
// std::lock_guard g(s_AssetMutex);
|
||||||
int w, h, ch;
|
// if (s_MetadataMap.count(resPath)) continue;
|
||||||
stbi_set_flip_vertically_on_load(1);
|
AddToTree(resPath, true);
|
||||||
unsigned char* data = stbi_load(path.string().c_str(), &w, &h, &ch, 0);
|
} else {
|
||||||
if (data) {
|
{
|
||||||
std::lock_guard<std::mutex> lock(s_TextureQueueMutex);
|
// quick check: have we already seen this asset?
|
||||||
s_TextureUploadQueue.push({ resPath, path.string(), w, h, ch, data });
|
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||||
s_MetadataMap[resPath] = { type, path.string() };
|
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;
|
void AssetManager::Tick()
|
||||||
for (size_t i = 1; i < parts.size(); ++i) {
|
{
|
||||||
auto& name = parts[i];
|
std::lock_guard<std::mutex> qlock(s_QueueMutex);
|
||||||
auto it = std::find_if(current->children.begin(), current->children.end(), [&](auto& child) {
|
while (!s_TextureQueue.empty()) {
|
||||||
return child->name == name;
|
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) {
|
// Get asset by virtual ID or file path
|
||||||
fs::path rel = fs::relative(full, s_ProjectRoot);
|
std::shared_ptr<Asset> AssetManager::Get(const std::string &keyOrPath)
|
||||||
return "res://" + rel.generic_string();
|
{
|
||||||
}
|
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) {
|
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||||
auto e = ext.extension().string();
|
|
||||||
if (e == ".png" || e == ".jpg") return "texture2D";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Asset> AssetManager::Get(const std::string& resPath) {
|
// 1) Direct ID lookup
|
||||||
auto it = s_LoadedAssets.find(resPath);
|
auto it = s_LoadedAssets.find(key);
|
||||||
return (it != s_LoadedAssets.end()) ? it->second : nullptr;
|
if (it != s_LoadedAssets.end()) {
|
||||||
}
|
return it->second;
|
||||||
|
}
|
||||||
void AssetManager::SaveAssetPack(const std::string& outputPath) {
|
// 2) Path-to-ID mapping
|
||||||
YAML::Emitter out;
|
auto pit = s_PathToID.find(key);
|
||||||
out << YAML::BeginMap;
|
if (pit != s_PathToID.end()) {
|
||||||
out << YAML::Key << "assets" << YAML::Value << YAML::BeginSeq;
|
auto ait = s_LoadedAssets.find(pit->second);
|
||||||
|
if (ait != s_LoadedAssets.end()) {
|
||||||
for (const auto& [resPath, meta] : s_MetadataMap) {
|
return ait->second;
|
||||||
fs::path relative = fs::relative(meta.absolutePath, s_ProjectRoot);
|
}
|
||||||
|
}
|
||||||
out << YAML::BeginMap;
|
// 3) Convert filesystem path to virtual and lookup
|
||||||
out << YAML::Key << "id" << YAML::Value << resPath;
|
try {
|
||||||
out << YAML::Key << "type" << YAML::Value << meta.type;
|
std::string virt = MakeVirtualPath(fs::absolute(key));
|
||||||
out << YAML::Key << "path" << YAML::Value << relative.generic_string();
|
// Normalize if virt had leading slash
|
||||||
out << YAML::EndMap;
|
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);
|
std::shared_ptr<ResourceTreeNode> AssetManager::GetFileTree()
|
||||||
fout << out.c_str();
|
{
|
||||||
fout.close();
|
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() {
|
std::ofstream fout(outputPath);
|
||||||
return s_FileTree;
|
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
|
} // namespace OX
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
|
/// AssetManager.h
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace OX {
|
namespace OX {
|
||||||
|
|
||||||
class Asset;
|
class Asset;
|
||||||
|
|
||||||
struct ResourceTreeNode {
|
struct ResourceTreeNode {
|
||||||
@ -22,12 +24,16 @@ namespace OX {
|
|||||||
|
|
||||||
class AssetManager {
|
class AssetManager {
|
||||||
public:
|
public:
|
||||||
|
// -- Lifecycle --
|
||||||
static void Init(const std::string& projectRoot);
|
static void Init(const std::string& projectRoot);
|
||||||
static void Shutdown();
|
static void Shutdown();
|
||||||
static void Tick(); // Main-thread
|
static void Tick();
|
||||||
static void Rescan();
|
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 std::shared_ptr<ResourceTreeNode> GetFileTree();
|
||||||
static void SaveAssetPack(const std::string& outputPath);
|
static void SaveAssetPack(const std::string& outputPath);
|
||||||
|
|
||||||
@ -36,29 +42,33 @@ namespace OX {
|
|||||||
std::string type;
|
std::string type;
|
||||||
std::string absolutePath;
|
std::string absolutePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PendingTexture {
|
struct PendingTexture {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string path;
|
|
||||||
int width, height, channels;
|
int width, height, channels;
|
||||||
unsigned char* data;
|
unsigned char* data;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void BackgroundScan(const std::filesystem::path& root);
|
static void BackgroundScan();
|
||||||
static void AddToTree(const std::string& virtualPath, bool isDir);
|
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);
|
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, 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::shared_ptr<ResourceTreeNode> s_FileTree;
|
||||||
|
|
||||||
static std::mutex s_TextureQueueMutex;
|
// background scan
|
||||||
static std::queue<PendingTexture> s_TextureUploadQueue;
|
|
||||||
|
|
||||||
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;
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace OX
|
// texture upload queue
|
||||||
|
static std::queue<PendingTexture> s_TextureQueue;
|
||||||
|
static std::mutex s_QueueMutex;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "systems/Asset.h"
|
#include "systems/Asset.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "GL/glew.h"
|
|
||||||
|
|
||||||
namespace OX {
|
namespace OX {
|
||||||
class Texture2D : public Asset {
|
class Texture2D : public Asset {
|
||||||
@ -17,7 +16,7 @@ namespace OX {
|
|||||||
|
|
||||||
void SetFromGL(uint32_t texID, int width, int height);
|
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 GetWidth() const { return m_Width; }
|
||||||
int GetHeight() const { return m_Height; }
|
int GetHeight() const { return m_Height; }
|
||||||
|
|
||||||
|
@ -36,7 +36,8 @@ namespace OX
|
|||||||
ImGui_ImplGlfw_InitForOpenGL(core.GetWindow().GetHandle(), true);
|
ImGui_ImplGlfw_InitForOpenGL(core.GetWindow().GetHandle(), true);
|
||||||
ImGui_ImplOpenGL3_Init("#version 330 core");
|
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)
|
void Editor::Update(Core &core)
|
||||||
@ -76,7 +77,7 @@ namespace OX
|
|||||||
ImGui::DockSpace(dockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
|
ImGui::DockSpace(dockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
|
||||||
|
|
||||||
LoggerWindow::Draw();
|
LoggerWindow::Draw();
|
||||||
FileBrowser::Draw();
|
fileBrowser->Draw();
|
||||||
primaryViewport->Draw(core);
|
primaryViewport->Draw(core);
|
||||||
|
|
||||||
|
|
||||||
@ -159,6 +160,8 @@ namespace OX
|
|||||||
delete primaryViewport;
|
delete primaryViewport;
|
||||||
primaryViewport = nullptr;
|
primaryViewport = nullptr;
|
||||||
|
|
||||||
|
delete fileBrowser;
|
||||||
|
fileBrowser = nullptr;
|
||||||
|
|
||||||
Logger::LogOk("Editor::Shutdown");
|
Logger::LogOk("Editor::Shutdown");
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace OX
|
namespace OX
|
||||||
{
|
{
|
||||||
class Viewport;
|
class Viewport;
|
||||||
|
class FileBrowser;
|
||||||
|
|
||||||
class Editor final : public Layer
|
class Editor final : public Layer
|
||||||
{
|
{
|
||||||
@ -28,6 +29,7 @@ namespace OX
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Viewport* primaryViewport;
|
Viewport* primaryViewport;
|
||||||
|
FileBrowser* fileBrowser;
|
||||||
};
|
};
|
||||||
} // OX
|
} // OX
|
||||||
|
|
||||||
|
@ -1,99 +1,147 @@
|
|||||||
|
// File: src/FileBrowser.cpp
|
||||||
#include "FileBrowser.h"
|
#include "FileBrowser.h"
|
||||||
#include "systems/AssetManager.h"
|
|
||||||
#include "systems/assets/Texture2D.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
namespace OX
|
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;
|
ImGui::Begin(title);
|
||||||
const float padding = 8.0f;
|
|
||||||
const float labelHeight = 20.0f;
|
|
||||||
const float cellWidth = thumbSize + padding * 2;
|
|
||||||
const float cellHeight = thumbSize + labelHeight + padding;
|
|
||||||
|
|
||||||
ImVec2 region = ImGui::GetContentRegionAvail();
|
// --- toolbar now contains back button, inline path, filter & view toggle all on one row ---
|
||||||
int columns = std::max(1, (int) (region.x / cellWidth));
|
DrawToolbar();
|
||||||
|
|
||||||
ImGui::BeginChild("FileGrid", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding);
|
// main content
|
||||||
ImGui::Columns(columns, nullptr, false);
|
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::End();
|
||||||
ImGui::PushID(child->path.c_str()); // Unique ID per item
|
}
|
||||||
|
|
||||||
|
// ——— 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();
|
ImGui::BeginGroup();
|
||||||
|
|
||||||
std::string tooltip;
|
// invisible button as hit target
|
||||||
std::string label = child->name;
|
ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding);
|
||||||
ImVec2 cellSize(cellWidth, cellHeight);
|
ImGui::InvisibleButton("cell", btnSize);
|
||||||
|
|
||||||
ImGui::InvisibleButton("Cell", cellSize);
|
|
||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
bool clicked = ImGui::IsItemClicked();
|
bool clicked = ImGui::IsItemClicked();
|
||||||
|
|
||||||
// Background
|
// background rect
|
||||||
ImVec2 min = ImGui::GetItemRectMin();
|
ImVec2 mn = ImGui::GetItemRectMin();
|
||||||
ImVec2 max = ImGui::GetItemRectMax();
|
ImVec2 mx = ImGui::GetItemRectMax();
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(min, max, IM_COL32(40, 40, 40, 255), 4.0f);
|
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 or icon
|
||||||
|
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding});
|
||||||
// === Thumbnail ===
|
if (c->isDirectory) {
|
||||||
if (child->isDirectory) {
|
ImGui::Image(GetIconTexture(*c),
|
||||||
ImGui::SetCursorScreenPos(center);
|
{_cfg.thumbnailSize, _cfg.thumbnailSize});
|
||||||
ImGui::Text("[Folder]");
|
|
||||||
} else {
|
} else {
|
||||||
auto asset = AssetManager::Get(child->path);
|
auto asset = AssetManager::Get(c->path);
|
||||||
if (asset) {
|
if (asset && asset->GetTypeName() == "texture2D") {
|
||||||
std::string type = asset->GetTypeName();
|
auto tex = std::static_pointer_cast<Texture2D>(asset);
|
||||||
if (type == "texture2D") {
|
ImGui::Image((ImTextureID) (intptr_t) tex->GetID(),
|
||||||
auto tex = std::static_pointer_cast<Texture2D>(asset);
|
{_cfg.thumbnailSize, _cfg.thumbnailSize},
|
||||||
if (tex->GetID() != 0) {
|
{0, 1}, {1, 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());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ImGui::SetCursorScreenPos(center);
|
ImGui::Dummy({_cfg.thumbnailSize, _cfg.thumbnailSize});
|
||||||
ImGui::Text("[Unloaded]");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// label
|
||||||
// === Label ===
|
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mx.y - _cfg.labelHeight});
|
||||||
ImGui::SetCursorScreenPos(ImVec2(min.x + padding, max.y - labelHeight));
|
ImGui::PushTextWrapPos(mx.x);
|
||||||
ImGui::PushTextWrapPos(min.x + cellWidth);
|
ImGui::TextUnformatted(c->name.c_str());
|
||||||
ImGui::TextUnformatted(label.c_str());
|
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
|
|
||||||
// Click handling
|
// click handling
|
||||||
if (clicked && child->isDirectory) {
|
if (clicked) {
|
||||||
s_CurrentPath = child->path;
|
if (c->isDirectory) {
|
||||||
}
|
_currentPath = c->path;
|
||||||
|
} else if (_onFileSelected) {
|
||||||
if (hovered && !tooltip.empty()) {
|
_onFileSelected(c->path);
|
||||||
ImGui::BeginTooltip();
|
}
|
||||||
ImGui::TextUnformatted(tooltip.c_str());
|
|
||||||
ImGui::EndTooltip();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndGroup();
|
ImGui::EndGroup();
|
||||||
@ -105,49 +153,35 @@ namespace OX
|
|||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ——— Simple list view ———
|
||||||
static std::shared_ptr<ResourceTreeNode> FindNode(const std::shared_ptr<ResourceTreeNode> &root,
|
void FileBrowser::DrawListView(const std::shared_ptr<ResourceTreeNode> &node)
|
||||||
const std::string &targetPath)
|
|
||||||
{
|
{
|
||||||
if (root->path == targetPath) return root;
|
ImGui::BeginChild("ListRegion", ImVec2(0, 0), false);
|
||||||
for (const auto &child: root->children) {
|
|
||||||
if (child->isDirectory) {
|
|
||||||
auto found = FindNode(child, targetPath);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileBrowser::Draw()
|
for (auto &c: node->children) {
|
||||||
{
|
if (!_filter.empty() && !PassesFilter(c->name)) continue;
|
||||||
ImGui::Begin("File Browser");
|
if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
|
if (ImGui::IsMouseDoubleClicked(0)) {
|
||||||
// === Back Button ===
|
if (c->isDirectory) {
|
||||||
if (s_CurrentPath != "res://") {
|
_currentPath = c->path;
|
||||||
if (ImGui::Button("..")) {
|
} else if (_onFileSelected) {
|
||||||
size_t lastSlash = s_CurrentPath.find_last_of('/');
|
_onFileSelected(c->path);
|
||||||
if (lastSlash != std::string::npos && lastSlash > 6) {
|
}
|
||||||
// skip 'res://'
|
|
||||||
s_CurrentPath = s_CurrentPath.substr(0, lastSlash);
|
|
||||||
} else {
|
|
||||||
s_CurrentPath = "res://";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::EndChild();
|
||||||
ImGui::Text("Current Path: %s", s_CurrentPath.c_str());
|
}
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
auto root = AssetManager::GetFileTree();
|
bool FileBrowser::PassesFilter(const std::string &name) const
|
||||||
auto currentNode = FindNode(root, s_CurrentPath);
|
{
|
||||||
if (currentNode) {
|
return _filter.empty() ||
|
||||||
DrawGrid(currentNode);
|
(name.find(_filter) != std::string::npos);
|
||||||
} else {
|
}
|
||||||
ImGui::Text("Path not found: %s", s_CurrentPath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::End();
|
ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace OX
|
} // namespace OX
|
||||||
|
@ -1,12 +1,65 @@
|
|||||||
|
// File: src/FileBrowser.h
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include "systems/AssetManager.h" // for ResourceTreeNode
|
||||||
|
#include "systems/assets/Texture2D.h" // for Texture2D
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace OX {
|
namespace OX {
|
||||||
|
|
||||||
class FileBrowser {
|
/// Configuration for look & feel
|
||||||
public:
|
struct FileBrowserConfig
|
||||||
static void Draw();
|
{
|
||||||
|
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
|
} // namespace OX
|
||||||
|
Loading…
Reference in New Issue
Block a user