VEX Engine
Retro looking game engine made in vulkan mostly because i wanted to understand it better.
Loading...
Searching...
No Matches
ProjectSelector.cpp
1#include "ProjectSelector.hpp"
2#include <imgui.h>
3#include <nlohmann/json.hpp>
4#include <fstream>
5#include <filesystem>
6#include <cstdlib>
7
9
10#ifdef _WIN32
11 #include <windows.h>
12 #include <shlobj.h>
13#else
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17#endif
18
19namespace vex {
20
22 : Engine(title, 800, 600, GameInfo{"Vex Selector", 1, 0, 0})
23 {
25
26 std::filesystem::path docs = getUserDocumentsDir();
27 std::filesystem::path defaultSave = docs / "VexProjects";
28
29 std::string pathStr = defaultSave.string();
30 if (pathStr.length() < sizeof(m_newProjParentDir)) {
31 strcpy(m_newProjParentDir, pathStr.c_str());
32 }
33 }
34
36 if(shouldClose){
37 if(closeDelay > 0){
38 closeDelay--;
39 }else{
40 m_running = false;
41 }
42 }else{
43
46 }
47
48 if(getInputMode() != InputMode::UI){
49 setInputMode(InputMode::UI);
50 }
51
52 try {
53 SceneRenderData renderData{};
54 if (!m_interface->getRenderer().beginFrame(m_resolutionManager->getWindowResolution(), renderData)) {
55 return;
56 }
57
58 renderData.imguiTextureID = m_interface->getRenderer().getImGuiTextureID(*m_imgui);
59
60 m_imgui->beginFrame();
61 m_imgui->executeUIFunctions();
62
63 drawSelectorLayout(renderData);
67
68 m_imgui->endFrame();
69
70 m_interface->getRenderer().composeFrame(renderData, *m_imgui, true);
71 m_interface->getRenderer().endFrame(renderData);
72
73 } catch (const std::exception& e) {
74 log(LogLevel::ERROR, "Selector render failed: %s", e.what());
75 }
76 }
77 }
78
80 ImGuiViewport* viewport = ImGui::GetMainViewport();
81 ImGui::SetNextWindowPos(viewport->Pos);
82 ImGui::SetNextWindowSize(viewport->Size);
83 ImGui::SetNextWindowViewport(viewport->ID);
84
85 ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
86 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBringToFrontOnFocus |
87 ImGuiWindowFlags_NoNavFocus;
88
89 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
90 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 20.0f));
91
92 ImGui::Begin("SelectorSpace", nullptr, windowFlags);
93
94 ImGui::Text("VEX ENGINE - PROJECTS");
95 ImGui::Separator();
96
97 if (ImGui::Button("+ Create New Project", ImVec2(200, 30))) {
98 m_showCreatorModal = true;
99 }
100 ImGui::SameLine();
101 if (ImGui::Button("+ Add Existing Project", ImVec2(200, 30))) {
102 m_showAddExistingModal = true;
103 memset(m_addExistingBuffer, 0, sizeof(m_addExistingBuffer));
104 }
105
106 ImGui::Spacing();
107 ImGui::Separator();
108 ImGui::Dummy(ImVec2(0.0f, 10.0f));
109
110 float cardWidth = 220.0f;
111 int columns = static_cast<int>(viewport->Size.x / cardWidth);
112 if (columns < 1) columns = 1;
113
114 if (ImGui::BeginTable("ProjectsGrid", columns)) {
115 if(m_projects.size() < 1){
116 ImGui::TableNextColumn();
117 ImGui::Text("No projects yet.");
118 }
119 for (int i = 0; i < m_projects.size(); ++i) {
120 ImGui::TableNextColumn();
121
122 ImGui::PushID(m_projects[i].path.c_str());
123 if (ImGui::Button(m_projects[i].name.c_str(), ImVec2(200, 300))) {
124 selectProject(m_projects[i].path);
125 }
126
127 if (ImGui::BeginPopupContextItem()) {
128 ImGui::TextDisabled("%s", m_projects[i].name.c_str());
129 ImGui::Separator();
130
131 if (ImGui::MenuItem("Move Project Folder...")) {
132 m_contextMenuTargetIndex = i;
133 m_showMoveModal = true;
134
135 std::filesystem::path p(m_projects[i].path);
136 std::string parent = p.parent_path().string();
137 strcpy(m_moveProjDestBuffer, parent.c_str());
138 }
139
140 if (ImGui::MenuItem("Remove from List")) {
141 removeProject(i);
142 ImGui::EndPopup();
143 ImGui::PopID();
144 ImGui::EndTable();
145 goto EndGrid;
146 }
147
148 ImGui::EndPopup();
149 }
150
151 float wrapPosX = ImGui::GetCursorPosX() + (cardWidth - 5.f);
152 ImGui::PushTextWrapPos(wrapPosX);
153 ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", m_projects[i].path.c_str());
154 ImGui::PopTextWrapPos();
155
156 ImGui::Text("Ver: %s", m_projects[i].version.c_str());
157 ImGui::Dummy(ImVec2(0.0f, 10.0f));
158 ImGui::PopID();
159 }
160 ImGui::EndTable();
161 }
162
163 EndGrid:
164 ImGui::End();
165 ImGui::PopStyleVar(2);
166 }
167
169 if (m_showAddExistingModal) ImGui::OpenPopup("Add Existing Project");
170
171 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
172 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
173
174 if (ImGui::BeginPopupModal("Add Existing Project", &m_showAddExistingModal, ImGuiWindowFlags_AlwaysAutoResize)) {
175 ImGui::Text("Enter absolute path to project folder:");
176 #ifdef _WIN32
177 ImGui::InputTextWithHint("##pathInput", "C:/Projects/MyProject", m_addExistingBuffer, IM_ARRAYSIZE(m_addExistingBuffer));
178 #else
179 ImGui::InputTextWithHint("##pathInput", "/home/user/Projects/MyProject", m_addExistingBuffer, IM_ARRAYSIZE(m_addExistingBuffer));
180 #endif
181
182 ImGui::Dummy(ImVec2(0, 10));
183
184 if (ImGui::Button("Add", ImVec2(100, 0))) {
185 scanAndAddProject(m_addExistingBuffer);
186 m_showAddExistingModal = false;
187 ImGui::CloseCurrentPopup();
188 }
189 ImGui::SameLine();
190 if (ImGui::Button("Cancel", ImVec2(100, 0))) {
191 m_showAddExistingModal = false;
192 ImGui::CloseCurrentPopup();
193 }
194 ImGui::EndPopup();
195 }
196 }
197
199 if (m_showMoveModal) ImGui::OpenPopup("Move Project");
200
201 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
202 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
203
204 if (ImGui::BeginPopupModal("Move Project", &m_showMoveModal, ImGuiWindowFlags_AlwaysAutoResize)) {
205
206 if (m_contextMenuTargetIndex >= 0 && m_contextMenuTargetIndex < m_projects.size()) {
207 const auto& proj = m_projects[m_contextMenuTargetIndex];
208 ImGui::Text("Moving project: '%s'", proj.name.c_str());
209 ImGui::TextDisabled("Current: %s", proj.path.c_str());
210 ImGui::Separator();
211
212 ImGui::Text("New Parent Directory:");
213 ImGui::InputText("##MoveDir", m_moveProjDestBuffer, IM_ARRAYSIZE(m_moveProjDestBuffer));
214
215 ImGui::Dummy(ImVec2(0, 15));
216
217 if (ImGui::Button("Move Folder", ImVec2(120, 0))) {
218 moveProject(m_contextMenuTargetIndex, m_moveProjDestBuffer);
219 m_showMoveModal = false;
220 ImGui::CloseCurrentPopup();
221 }
222 } else {
223 m_showMoveModal = false;
224 }
225
226 ImGui::SameLine();
227 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
228 m_showMoveModal = false;
229 ImGui::CloseCurrentPopup();
230 }
231 ImGui::EndPopup();
232 }
233 }
234
236 if (index >= 0 && index < m_projects.size()) {
237 log("Removing project from list: %s", m_projects[index].name.c_str());
238 m_projects.erase(m_projects.begin() + index);
240 }
241 }
242
243 void ProjectSelector::moveProject(int index, const std::string& newParentDir) {
244 if (index < 0 || index >= m_projects.size()) return;
245
246 auto& proj = m_projects[index];
247 std::filesystem::path oldPath(proj.path);
248 std::filesystem::path newParent(newParentDir);
249 std::filesystem::path newPath = newParent / oldPath.filename();
250
251 if (!std::filesystem::exists(oldPath)) {
252 log(LogLevel::ERROR, "Source path does not exist!");
253 return;
254 }
255 if (std::filesystem::exists(newPath)) {
256 log(LogLevel::ERROR, "Destination already exists: %s", newPath.string().c_str());
257 return;
258 }
259
260 try {
261 if (!std::filesystem::exists(newParent)) std::filesystem::create_directories(newParent);
262
263 std::filesystem::rename(oldPath, newPath);
264 log("Project moved to: %s", newPath.string().c_str());
265
266 proj.path = newPath.string();
267
269
270 } catch (const std::exception& e) {
271 log(LogLevel::ERROR, "Failed to move project: %s", e.what());
272 }
273 }
274
276 if (!std::filesystem::exists(m_configPath)) return;
277
278 try {
279 std::ifstream file(m_configPath);
280 nlohmann::json j;
281 file >> j;
282
283 for (const auto& item : j) {
284 scanAndAddProject(item["path"].get<std::string>());
285 }
286 } catch (const std::exception& e) {
287 log(LogLevel::ERROR, "Failed to load projects: %s", e.what());
288 }
289 }
290
292 nlohmann::json j = nlohmann::json::array();
293 for (const auto& p : m_projects) {
294 j.push_back({
295 {"name", p.name},
296 {"path", p.path},
297 {"version", p.version}
298 });
299 }
300 std::ofstream file(m_configPath);
301 file << j.dump(4);
302 }
303
304 void ProjectSelector::scanAndAddProject(const std::string& pathStr) {
305 std::filesystem::path path(pathStr);
306 std::filesystem::path projectFile = path / "VexProject.json";
307
308 if (std::filesystem::exists(projectFile)) {
309 try {
310 std::ifstream f(projectFile);
311 nlohmann::json j;
312 f >> j;
313
314 std::string pName = j.value("project_name", "Unknown Project");
315 std::string pVer = j.value("version", "1");
316
317 for(auto& p : m_projects) {
318 if(p.path == pathStr) return;
319 }
320
321 m_projects.push_back({pName, pathStr, pVer});
323 log("Added project: %s", pName.c_str());
324
325 } catch(const std::exception& e) {
326 log(LogLevel::ERROR, "Invalid VexProject.json at %s: %s", pathStr.c_str(), e.what());
327 }
328 } else {
329 log(LogLevel::ERROR, "No VexProject.json found at %s", pathStr.c_str());
330 }
331 }
332
333 void ProjectSelector::selectProject(const std::string& projectPath) {
334 log("Project selected: %s", projectPath.c_str());
335 m_selectedProjectPath = projectPath;
336 shouldClose = true;
337 }
338
340 if (m_showCreatorModal) {
341 ImGui::OpenPopup("Create New Project");
342 }
343
344 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
345 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
346
347 if (ImGui::BeginPopupModal("Create New Project", &m_showCreatorModal, ImGuiWindowFlags_AlwaysAutoResize)) {
348
349 ImGui::Text("Project Name:");
350 ImGui::InputText("##NewProjName", m_newProjName, IM_ARRAYSIZE(m_newProjName));
351
352 ImGui::Spacing();
353
354 ImGui::Text("Location:");
355 ImGui::InputText("##NewProjDir", m_newProjParentDir, IM_ARRAYSIZE(m_newProjParentDir));
356 ImGui::SameLine();
357 ImGui::TextDisabled("(?)");
358 if (ImGui::IsItemHovered())
359 ImGui::SetTooltip("The folder where your project folder will be created.\nDefault is Documents/VexProjects.");
360
361 ImGui::Dummy(ImVec2(0, 15));
362 ImGui::Separator();
363 ImGui::Dummy(ImVec2(0, 5));
364
365 if (ImGui::Button("Create", ImVec2(120, 0))) {
367 m_showCreatorModal = false;
368 ImGui::CloseCurrentPopup();
369 }
370 ImGui::SameLine();
371 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
372 m_showCreatorModal = false;
373 ImGui::CloseCurrentPopup();
374 }
375
376 ImGui::EndPopup();
377 }
378 }
379
381 std::string name(m_newProjName);
382 std::string parent(m_newProjParentDir);
383
384 if (name.empty() || parent.empty()) return;
385
386 std::filesystem::path exeDir = GetExecutableDir();
387 std::filesystem::path templatePath = exeDir / ".." / "Assets" / "default" / "NewProject";
388
389 if (!std::filesystem::exists(templatePath)) {
390 log(LogLevel::ERROR, "Missing template: %s", templatePath.string().c_str());
391 return;
392 }
393
394 std::filesystem::path destParent(parent);
395 std::filesystem::path newProjectPath = destParent / name;
396
397 if (std::filesystem::exists(newProjectPath)) {
398 log(LogLevel::ERROR, "Project already exists at: %s", newProjectPath.string().c_str());
399 return;
400 }
401
402 try {
403 if (!std::filesystem::exists(destParent)) {
404 std::filesystem::create_directories(destParent);
405 }
406
407 std::filesystem::copy(templatePath, newProjectPath, std::filesystem::copy_options::recursive);
408
409 std::filesystem::path jsonPath = newProjectPath / "VexProject.json";
410 if (std::filesystem::exists(jsonPath)) {
411 std::ifstream i(jsonPath);
412 nlohmann::json j;
413 i >> j;
414
415 j["project_name"] = name;
416
417 std::ofstream o(jsonPath);
418 o << j.dump(4);
419 }
420
421 scanAndAddProject(newProjectPath.string());
422 log("Created project: %s", name.c_str());
423
424 } catch (const std::exception& e) {
425 log(LogLevel::ERROR, "Creation failed: %s", e.what());
426 }
427 }
428
429 std::filesystem::path ProjectSelector::getUserDocumentsDir() {
430 #ifdef _WIN32
431 CHAR path[MAX_PATH];
432 HRESULT result = SHGetFolderPathA(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, path);
433 if (SUCCEEDED(result)) {
434 return std::filesystem::path(path);
435 }
436 #else
437 const char* homeDir = getenv("HOME");
438 if (homeDir) {
439 return std::filesystem::path(homeDir) / "Documents";
440 }
441 #endif
442 return std::filesystem::current_path();
443 }
444}
This file defines PhysicsSystem class.
Specialized Engine class for the initial project selection screen.
void setInputMode(InputMode mode)
Function allowing for changing input mode at runtime.
Definition Engine.hpp:126
InputMode getInputMode() const
Returns the current input mode.
Definition Engine.hpp:130
void setResolutionMode(ResolutionMode mode)
Sets the resolution mode and updates the resolution manager.
Definition Engine.cpp:238
ResolutionMode getResolutionMode() const
Returns the current resolution mode.
Definition Engine.hpp:118
Engine(const char *title, int width, int height, GameInfo gInfo)
Constructor for the Engine class.
Definition Engine.cpp:46
void drawAddExistingModal()
Draws the modal for adding an existing project.
void drawMoveProjectModal()
Draws the modal for moving an existing project.
void drawSelectorLayout(const SceneRenderData &data)
Internal helper to draw the ImGUI layout for the project selector.
void render() override
Overrides the base render function to draw the project selection UI.
std::filesystem::path getUserDocumentsDir()
Gets the path to the user's documents directory.
void removeProject(int index)
Removes a project from the list.
void saveKnownProjects()
Saves the current list of known projects to the configuration file.
void drawCreatorModal()
Draws the modal for creating a new project.
void createNewProject()
Creates a new project with the given name and path.
void loadKnownProjects()
Loads the list of recently known projects from the configuration file.
void moveProject(int index, const std::string &newParentDir)
Moves a project to a new parent directory.
void selectProject(const std::string &projectPath)
Sets the path of the project that the user selected and triggers the closure of the selector.
ProjectSelector(const char *title)
Constructs the ProjectSelector window.
void scanAndAddProject(const std::string &path)
Scans a directory path for a valid VEX project file and adds it to the list.
std::filesystem::path VEX_EXPORT GetExecutableDir()
Retrieves the directory containing the current executable.
Definition PathUtils.cpp:37
@ NATIVE
Use window resolution.
void VEX_EXPORT log(const char *fmt,...)
Logs a formatted message.
this struct contains information about the game. like project name and version.
Definition GameInfo.hpp:15
Data structure to pass state between render stages.
Definition Renderer.hpp:57