diff --git a/.gitignore b/.gitignore index b03c5ce..d5d662a 100644 --- a/.gitignore +++ b/.gitignore @@ -468,3 +468,4 @@ FodyWeavers.xsd ### VisualStudio Patch ### # Additional files built by Visual Studio +.vscode/* \ No newline at end of file diff --git a/include/engine/imgui-objects/file-browser.hpp b/include/engine/imgui-objects/file-browser.hpp new file mode 100644 index 0000000..9451182 --- /dev/null +++ b/include/engine/imgui-objects/file-browser.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +namespace fs = std::filesystem; + +namespace Engine { + +struct FileBrowserState { + std::string root_directory; + bool has_root = false; + std::string selected_path; + + // sizing state (percentages tracked elsewhere if needed) + int mainWindowWidth = 1000; + int mainWindowHeight = 800; + float filebrowserWidth = 400.f; + float filebrowserHeight = 480.f; + bool mainwindowresize = false; +}; + +class FileBrowser { + public: + void Initialize(); + void UpdateOnWindowResize(FileBrowserState &state, int w, int h); + bool SetRootDirectory(FileBrowserState &state, const std::string &path); + + void Render(FileBrowserState &state); + + private: + void RenderDirectoryNode(FileBrowserState &state, const fs::path &dirPath, + int depth = 0); + void OpenFile(const std::string &path); +}; + +} // namespace Engine \ No newline at end of file diff --git a/include/engine/imgui-objects/top-bar.hpp b/include/engine/imgui-objects/top-bar.hpp new file mode 100644 index 0000000..fc61459 --- /dev/null +++ b/include/engine/imgui-objects/top-bar.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace Engine { +struct TopBar { + static void Render(FileBrowser &browser, FileBrowserState &state); +}; +} // namespace Engine \ No newline at end of file diff --git a/include/engine/imgui-render.hpp b/include/engine/imgui-render.hpp new file mode 100644 index 0000000..bce4ea1 --- /dev/null +++ b/include/engine/imgui-render.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace Engine { + + class ImGuiRender { + public: + static ImGuiRender& Instance(); + + void Init(SDL_Window * window, SDL_Renderer * renderer); + void ShutDown(); + + void ProcessEvent(const SDL_Event* e); + + void BeginFrame(); + void EndFrame(); + + + private: + SDL_Window* m_window = nullptr; + SDL_Renderer* m_renderer = nullptr; + bool m_initialized = false; + }; +} // namespace Engine \ No newline at end of file diff --git a/src/engine/imgui-objects/file-browser.cpp b/src/engine/imgui-objects/file-browser.cpp new file mode 100644 index 0000000..55b6df8 --- /dev/null +++ b/src/engine/imgui-objects/file-browser.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +namespace Engine { + +void FileBrowser::Initialize() { + // can be used to set directory using top bar like vscode does with a button + // in the area +} + +void FileBrowser::UpdateOnWindowResize(FileBrowserState &state, int w, int h) { + state.mainWindowWidth = w; + state.mainWindowHeight = h; + state.mainwindowresize = true; + // 40% width, 60% height + state.filebrowserWidth = 0.4f * static_cast(w); + state.filebrowserHeight = 0.6f * static_cast(h); +} +bool FileBrowser::SetRootDirectory(FileBrowserState &state, + const std::string &path) { + try { + fs::path p(path); + if (path.empty() || !fs::exists(p) || !fs::is_directory(p)) + return false; + state.root_directory = fs::canonical(p).string(); + state.has_root = true; + state.selected_path.clear(); + return true; + } catch (const fs::filesystem_error &e) { + std::cerr << "SetRootDirectory error: " << e.what() << std::endl; + return false; + } +} + +void FileBrowser::OpenFile(const std::string &path) { + std::cout << "OpenFile: " << path << std::endl; + // Opening file logic +} + +void FileBrowser::RenderDirectoryNode(FileBrowserState &state, + const fs::path &dirPath, int depth) { + // Collect entries (dirs first, then files) + std::vector entries; + try { + for (const auto &entry : fs::directory_iterator(dirPath)) { + entries.push_back(entry); + } + } catch (const fs::filesystem_error &e) { + ImGui::TextDisabled(" ", e.what()); + return; + } + + std::sort(entries.begin(), entries.end(), + [](const fs::directory_entry &a, const fs::directory_entry &b) { + bool ad = a.is_directory(); + bool bd = b.is_directory(); + if (ad != bd) + return ad > bd; // dirs first + return a.path().filename().string() < + b.path().filename().string(); + }); + + for (auto &entry : entries) { + const fs::path &p = entry.path(); + std::string name = p.filename().string(); + bool isDir = entry.is_directory(); + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth; + if (!isDir) + flags |= + ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (state.selected_path == p.string()) + flags |= ImGuiTreeNodeFlags_Selected; + + bool open = false; + if (isDir) { + open = ImGui::TreeNodeEx((name + "##" + p.string()).c_str(), flags); + } else { + ImGui::TreeNodeEx((name + "##" + p.string()).c_str(), flags); + } + + if (ImGui::IsItemClicked()) { + state.selected_path = p.string(); + if (!isDir) { + OpenFile(p.string()); + } + } + + if (open && isDir) { + // Limit depth if desired (optional) + if (depth < 64) { + RenderDirectoryNode(state, p, depth + 1); + } else { + ImGui::TextDisabled(" "); + } + ImGui::TreePop(); + } + } +} + +void FileBrowser::Render(FileBrowserState &state) { + if (state.mainwindowresize) { + ImGui::SetNextWindowSize( + ImVec2(state.filebrowserWidth, state.filebrowserHeight), + ImGuiCond_Always); + state.mainwindowresize = false; + } else { + ImGui::SetNextWindowSize( + ImVec2(state.filebrowserWidth, state.filebrowserHeight), + ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(0, 20), ImGuiCond_FirstUseEver); + } + ImGui::SetNextWindowSizeConstraints(ImVec2(120, 120), + ImVec2(FLT_MAX, FLT_MAX)); + + if (ImGui::Begin("File Browser", nullptr, ImGuiWindowFlags_NoCollapse)) { + if (!state.has_root) { + ImGui::TextWrapped( + "No root directory set.\nUse File > Open Root... in the top " + "bar to pick one."); + } else { + ImGui::TextWrapped("Root: %s", state.root_directory.c_str()); + if (!state.selected_path.empty()) { + ImGui::TextWrapped("Selected: %s", state.selected_path.c_str()); + } + ImGui::Separator(); + + // Draw tree starting at root + fs::path root(state.root_directory); + // root is first node + ImGuiTreeNodeFlags rootFlags = ImGuiTreeNodeFlags_SpanAvailWidth; + if (state.selected_path == state.root_directory) + rootFlags |= ImGuiTreeNodeFlags_Selected; + + std::string rootLabel = + (root.filename().string().empty() ? root.string() + : root.filename().string()) + + std::string("##") + state.root_directory; + bool rootOpen = ImGui::TreeNodeEx(rootLabel.c_str(), rootFlags); + + if (ImGui::IsItemClicked()) { + state.selected_path = state.root_directory; + } + + if (rootOpen) { + RenderDirectoryNode(state, root, 0); + ImGui::TreePop(); + } + } + } + ImGui::End(); +} + +} // namespace Engine \ No newline at end of file diff --git a/src/engine/imgui-objects/top-bar.cpp b/src/engine/imgui-objects/top-bar.cpp new file mode 100644 index 0000000..2616db4 --- /dev/null +++ b/src/engine/imgui-objects/top-bar.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + +namespace Engine { +void TopBar::Render(FileBrowser &browser, FileBrowserState &state) { + static bool openRootPopup = false; + static char pathBuffer[1024] = {0}; + static bool pathError = false; + + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Open Root...")) { + openRootPopup = true; + pathError = false; + if (state.has_root) + strncpy(pathBuffer, state.root_directory.c_str(), + sizeof(pathBuffer) - 1); + else + pathBuffer[0] = '\0'; + } + if (state.has_root) { + if (ImGui::MenuItem("Clear Root")) { + state.has_root = false; + state.root_directory.clear(); + state.selected_path.clear(); + } + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + + if (openRootPopup) { + ImGui::OpenPopup("Set Root Directory"); + openRootPopup = false; + } + + if (ImGui::BeginPopupModal("Set Root Directory", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::InputText("Path", pathBuffer, sizeof(pathBuffer)); + if (pathError) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), + "Invalid directory."); + } + if (ImGui::Button("Set", ImVec2(80, 0))) { + std::string candidate(pathBuffer); + if (browser.SetRootDirectory(state, candidate)) { + ImGui::CloseCurrentPopup(); + } else { + pathError = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +} // namespace Engine \ No newline at end of file diff --git a/src/engine/imgui-render.cpp b/src/engine/imgui-render.cpp new file mode 100644 index 0000000..cacaee2 --- /dev/null +++ b/src/engine/imgui-render.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include + +namespace Engine { +void ImGuiRender::Init(SDL_Window *window, SDL_Renderer *renderer) { + if (m_initialized) + return; + m_window = window; + m_renderer = renderer; + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + + ImGui_ImplSDL3_InitForSDLRenderer(m_window, m_renderer); + ImGui_ImplSDLRenderer3_Init(m_renderer); + + m_initialized = true; +} + +void ImGuiRender::ShutDown() { + if (!m_initialized) + return; + ImGui_ImplSDLRenderer3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + m_window = nullptr; + m_renderer = nullptr; + m_initialized = false; +} + +void ImGuiRender::BeginFrame() { + if (!m_initialized) return; + ImGui_ImplSDL3_NewFrame(); + ImGui_ImplSDLRenderer3_NewFrame(); + ImGui::NewFrame(); +} + +void ImGuiRender::EndFrame() { + if (!m_initialized) + return; + ImGui::Render(); + ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), m_renderer); +} + +void ImGuiRender::ProcessEvent(const SDL_Event *e) { + if (!m_initialized || !e) + return; + ImGui_ImplSDL3_ProcessEvent(e); +} +} // namespace Engine \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 08deb27..e918154 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,16 @@ #include #include #include +#include +#include +#include #include +// Initialise file browser instance and state +Engine::FileBrowser fileBrowser; +Engine::FileBrowserState fileBrowserState; +static Engine::ImGuiRender g_ImGui; + // Initialises subsystems and initialises appState to be used by all other main // functions. SDL_AppResult SDL_AppInit(void **appState, int argc, char *argv[]) { @@ -30,6 +38,10 @@ SDL_AppResult SDL_AppInit(void **appState, int argc, char *argv[]) { data.window.height); gameEngine->GetWindow().SetDimensions(data.window.width, data.window.height); + + // Update file browser state on window resize + fileBrowser.UpdateOnWindowResize( + fileBrowserState, data.window.width, data.window.height); }); gameEngine->GetEvents().RegisterCallback( Engine::EventType::KeyDown, [](Engine::EventData data) { @@ -63,6 +75,17 @@ SDL_AppResult SDL_AppInit(void **appState, int argc, char *argv[]) { data.mouse.y); }); + SPDLOG_INFO("Initializing file browser."); + fileBrowser.Initialize(); + + g_ImGui.Init(gameEngine->GetWindow().GetSDLWindow(), + gameEngine->GetRenderer().GetSDLRenderer()); + + fileBrowserState.mainWindowWidth = 1000; + fileBrowserState.mainWindowHeight = 800; + + // Populate initial directory contents + SPDLOG_INFO("Application initialized successfully."); *appState = gameEngine; return SDL_APP_CONTINUE; @@ -73,6 +96,10 @@ SDL_AppResult SDL_AppInit(void **appState, int argc, char *argv[]) { // custom event handling to be added by a user. SDL_AppResult SDL_AppEvent(void *appState, SDL_Event *event) { Engine::Engine *gameEngine = static_cast(appState); + + // Process ImGui events + g_ImGui.ProcessEvent(event); + if (event->type == SDL_EVENT_QUIT || event->type == SDL_EVENT_TERMINATING) { SPDLOG_INFO("Received SDL_EVENT_QUIT or SDL_EVENT_TERMINATING. Exiting " "application."); @@ -95,6 +122,11 @@ SDL_AppResult SDL_AppIterate(void *appState) { SPDLOG_DEBUG("Rendering all objects."); gameEngine->GetRenderManager().RenderAll(gameEngine->GetRenderer()); + g_ImGui.BeginFrame(); + Engine::TopBar::Render(fileBrowser, fileBrowserState); + fileBrowser.Render(fileBrowserState); + g_ImGui.EndFrame(); + SPDLOG_DEBUG("Presenting rendered frame."); gameEngine->GetRenderer().Present(); @@ -107,6 +139,9 @@ void SDL_AppQuit(void *appState, SDL_AppResult result) { Engine::Engine *gameEngine = static_cast(appState); if (gameEngine != nullptr) { SPDLOG_INFO("Shutting down game engine."); + + g_ImGui.ShutDown(); + gameEngine->Shutdown(); SPDLOG_INFO("Game engine shutdown completed."); } else {