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 "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
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user