Added scriptable window support to Modularity, Yey!
This commit is contained in:
54
Scripts/EditorWindowSample.cpp
Normal file
54
Scripts/EditorWindowSample.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
// Minimal sample showing how to expose a custom editor tab from a script binary.
|
||||
// Build via the engine’s “Compile Script” action. If compiling manually:
|
||||
// Linux: g++ -std=c++20 -fPIC -O2 -I../src -I../include -c EditorWindowSample.cpp -o ../Cache/ScriptBin/EditorWindowSample.o
|
||||
// g++ -shared ../Cache/ScriptBin/EditorWindowSample.o -o ../Cache/ScriptBin/EditorWindowSample.so -ldl -lpthread
|
||||
// Windows: cl /nologo /std:c++20 /EHsc /MD /O2 /I ..\src /I ..\include /c EditorWindowSample.cpp /Fo ..\Cache\ScriptBin\EditorWindowSample.obj
|
||||
// link /nologo /DLL ..\Cache\ScriptBin\EditorWindowSample.obj /OUT:..\Cache\ScriptBin\EditorWindowSample.dll User32.lib Advapi32.lib
|
||||
|
||||
#include "ScriptRuntime.h"
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
bool toggle = false;
|
||||
float sliderValue = 0.5f;
|
||||
char note[128] = "Hello from script!";
|
||||
|
||||
void drawContent(ScriptContext& ctx) {
|
||||
ImGui::TextUnformatted("EditorWindowSample");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Checkbox("Toggle", &toggle);
|
||||
ImGui::SliderFloat("Value", &sliderValue, 0.0f, 1.0f, "%.2f");
|
||||
ImGui::InputText("Note", note, sizeof(note));
|
||||
|
||||
if (ImGui::Button("Log Message")) {
|
||||
ctx.AddConsoleMessage(std::string("Script tab says: ") + note);
|
||||
}
|
||||
|
||||
if (ctx.object) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Selected object: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
|
||||
if (ImGui::Button("Nudge +Y")) {
|
||||
auto pos = ctx.object->position;
|
||||
pos.y += 0.25f;
|
||||
ctx.SetPosition(pos);
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Select an object to enable actions");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
|
||||
// Called every frame while the scripted window is open. Use ImGui freely here.
|
||||
drawContent(ctx);
|
||||
}
|
||||
|
||||
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
|
||||
// Called once when the user closes the tab from View -> Scripted Windows.
|
||||
// Good place to persist settings or emit a final log.
|
||||
(void)ctx;
|
||||
}
|
||||
98
src/EditorWindows/EnvironmentWindows.cpp
Normal file
98
src/EditorWindows/EnvironmentWindows.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "Engine.h"
|
||||
#include "ModelLoader.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
void Engine::renderEnvironmentWindow() {
|
||||
if (!showEnvironmentWindow) return;
|
||||
ImGui::Begin("Environment", &showEnvironmentWindow);
|
||||
|
||||
Skybox* skybox = renderer.getSkybox();
|
||||
if (skybox) {
|
||||
float tod = skybox->getTimeOfDay();
|
||||
ImGui::TextDisabled("Day / Night Cycle");
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::SliderFloat("##EnvDayNight", &tod, 0.0f, 1.0f, "%.2f")) {
|
||||
skybox->setTimeOfDay(tod);
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
static char skyVertBuf[256] = {};
|
||||
static char skyFragBuf[256] = {};
|
||||
if (skyVertBuf[0] == '\0') std::snprintf(skyVertBuf, sizeof(skyVertBuf), "%s", skybox->getVertPath().c_str());
|
||||
if (skyFragBuf[0] == '\0') std::snprintf(skyFragBuf, sizeof(skyFragBuf), "%s", skybox->getFragPath().c_str());
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Skybox Shader");
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputText("##SkyVert", skyVertBuf, sizeof(skyVertBuf))) {}
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputText("##SkyFrag", skyFragBuf, sizeof(skyFragBuf))) {}
|
||||
|
||||
bool selectionIsShader = false;
|
||||
if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) {
|
||||
selectionIsShader = fileBrowser.getFileCategory(fs::directory_entry(fileBrowser.selectedFile)) == FileCategory::Shader;
|
||||
}
|
||||
ImGui::BeginDisabled(!selectionIsShader);
|
||||
if (ImGui::Button("Use Selection as Vert")) {
|
||||
std::snprintf(skyVertBuf, sizeof(skyVertBuf), "%s", fileBrowser.selectedFile.string().c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Use Selection as Frag")) {
|
||||
std::snprintf(skyFragBuf, sizeof(skyFragBuf), "%s", fileBrowser.selectedFile.string().c_str());
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::Button("Reload Skybox Shader")) {
|
||||
skybox->setShaderPaths(skyVertBuf, skyFragBuf);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Skybox not available");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Global Ambient");
|
||||
glm::vec3 ambient = renderer.getAmbientColor();
|
||||
if (ImGui::ColorEdit3("##AmbientColor", &ambient.x, ImGuiColorEditFlags_DisplayRGB)) {
|
||||
renderer.setAmbientColor(ambient);
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void Engine::renderCameraWindow() {
|
||||
if (!showCameraWindow) return;
|
||||
ImGui::Begin("Camera", &showCameraWindow);
|
||||
|
||||
ImGui::TextDisabled("Movement");
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::DragFloat("Base Speed", &camera.moveSpeed, 0.1f, 0.1f, 100.0f, "%.2f")) {
|
||||
camera.moveSpeed = std::max(0.01f, camera.moveSpeed);
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::DragFloat("Sprint Speed", &camera.sprintSpeed, 0.1f, 0.1f, 200.0f, "%.2f")) {
|
||||
camera.sprintSpeed = std::max(camera.moveSpeed, camera.sprintSpeed);
|
||||
}
|
||||
ImGui::Checkbox("Smooth Movement", &camera.smoothMovement);
|
||||
ImGui::BeginDisabled(!camera.smoothMovement);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::DragFloat("Acceleration", &camera.acceleration, 0.1f, 0.1f, 100.0f, "%.2f");
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
788
src/EditorWindows/FileBrowserWindow.cpp
Normal file
788
src/EditorWindows/FileBrowserWindow.cpp
Normal file
@@ -0,0 +1,788 @@
|
||||
#include "Engine.h"
|
||||
#include "ModelLoader.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
namespace FileIcons {
|
||||
// Draw a folder icon
|
||||
void DrawFolderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float w = size;
|
||||
float h = size * 0.75f;
|
||||
float tabW = w * 0.4f;
|
||||
float tabH = h * 0.15f;
|
||||
|
||||
// Folder body
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(pos.x, pos.y + tabH),
|
||||
ImVec2(pos.x + w, pos.y + h),
|
||||
color, 3.0f
|
||||
);
|
||||
// Folder tab
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(pos.x, pos.y),
|
||||
ImVec2(pos.x + tabW, pos.y + tabH + 2),
|
||||
color, 2.0f
|
||||
);
|
||||
}
|
||||
|
||||
// Draw a scene/document icon
|
||||
void DrawSceneIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float w = size * 0.8f;
|
||||
float h = size;
|
||||
float cornerSize = w * 0.25f;
|
||||
|
||||
// Main document body
|
||||
ImVec2 p1 = ImVec2(pos.x, pos.y);
|
||||
ImVec2 p2 = ImVec2(pos.x + w - cornerSize, pos.y);
|
||||
ImVec2 p3 = ImVec2(pos.x + w, pos.y + cornerSize);
|
||||
ImVec2 p4 = ImVec2(pos.x + w, pos.y + h);
|
||||
ImVec2 p5 = ImVec2(pos.x, pos.y + h);
|
||||
|
||||
drawList->AddQuadFilled(p1, ImVec2(pos.x + w - cornerSize, pos.y), ImVec2(pos.x + w - cornerSize, pos.y + h), p5, color);
|
||||
drawList->AddTriangleFilled(p2, p3, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), color);
|
||||
drawList->AddRectFilled(ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p4, color);
|
||||
|
||||
// Corner fold
|
||||
drawList->AddTriangleFilled(p2, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p3,
|
||||
IM_COL32(255, 255, 255, 60));
|
||||
|
||||
// Scene icon indicator (play triangle)
|
||||
float cx = pos.x + w * 0.5f;
|
||||
float cy = pos.y + h * 0.55f;
|
||||
float triSize = size * 0.25f;
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(cx - triSize * 0.4f, cy - triSize * 0.5f),
|
||||
ImVec2(cx - triSize * 0.4f, cy + triSize * 0.5f),
|
||||
ImVec2(cx + triSize * 0.5f, cy),
|
||||
IM_COL32(255, 255, 255, 180)
|
||||
);
|
||||
}
|
||||
|
||||
// Draw a 3D model icon (cube wireframe)
|
||||
void DrawModelIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float s = size * 0.8f;
|
||||
float offset = size * 0.1f;
|
||||
float depth = s * 0.3f;
|
||||
|
||||
// Front face
|
||||
ImVec2 f1 = ImVec2(pos.x + offset, pos.y + offset + depth);
|
||||
ImVec2 f2 = ImVec2(pos.x + offset + s, pos.y + offset + depth);
|
||||
ImVec2 f3 = ImVec2(pos.x + offset + s, pos.y + offset + s);
|
||||
ImVec2 f4 = ImVec2(pos.x + offset, pos.y + offset + s);
|
||||
|
||||
// Back face
|
||||
ImVec2 b1 = ImVec2(f1.x + depth, f1.y - depth);
|
||||
ImVec2 b2 = ImVec2(f2.x + depth, f2.y - depth);
|
||||
ImVec2 b3 = ImVec2(f3.x + depth, f3.y - depth);
|
||||
|
||||
// Fill front face
|
||||
drawList->AddQuadFilled(f1, f2, f3, f4, color);
|
||||
|
||||
// Fill top face
|
||||
drawList->AddQuadFilled(f1, f2, b2, b1, IM_COL32(
|
||||
(color & 0xFF) * 0.7f,
|
||||
((color >> 8) & 0xFF) * 0.7f,
|
||||
((color >> 16) & 0xFF) * 0.7f,
|
||||
(color >> 24) & 0xFF
|
||||
));
|
||||
|
||||
// Fill right face
|
||||
drawList->AddQuadFilled(f2, b2, b3, f3, IM_COL32(
|
||||
(color & 0xFF) * 0.5f,
|
||||
((color >> 8) & 0xFF) * 0.5f,
|
||||
((color >> 16) & 0xFF) * 0.5f,
|
||||
(color >> 24) & 0xFF
|
||||
));
|
||||
|
||||
// Edges
|
||||
ImU32 edgeColor = IM_COL32(255, 255, 255, 100);
|
||||
drawList->AddLine(f1, f2, edgeColor, 1.0f);
|
||||
drawList->AddLine(f2, f3, edgeColor, 1.0f);
|
||||
drawList->AddLine(f3, f4, edgeColor, 1.0f);
|
||||
drawList->AddLine(f4, f1, edgeColor, 1.0f);
|
||||
drawList->AddLine(f1, b1, edgeColor, 1.0f);
|
||||
drawList->AddLine(f2, b2, edgeColor, 1.0f);
|
||||
drawList->AddLine(b1, b2, edgeColor, 1.0f);
|
||||
drawList->AddLine(f3, b3, edgeColor, 1.0f);
|
||||
drawList->AddLine(b2, b3, edgeColor, 1.0f);
|
||||
}
|
||||
|
||||
// Draw a texture/image icon
|
||||
void DrawTextureIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float padding = size * 0.1f;
|
||||
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
|
||||
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
|
||||
|
||||
// Frame
|
||||
drawList->AddRectFilled(tl, br, color, 2.0f);
|
||||
|
||||
// Mountain landscape
|
||||
float midY = pos.y + size * 0.6f;
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(pos.x + size * 0.2f, br.y - padding),
|
||||
ImVec2(pos.x + size * 0.45f, midY),
|
||||
ImVec2(pos.x + size * 0.7f, br.y - padding),
|
||||
IM_COL32(60, 60, 60, 255)
|
||||
);
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(pos.x + size * 0.5f, br.y - padding),
|
||||
ImVec2(pos.x + size * 0.7f, midY + size * 0.1f),
|
||||
ImVec2(pos.x + size * 0.9f, br.y - padding),
|
||||
IM_COL32(80, 80, 80, 255)
|
||||
);
|
||||
|
||||
// Sun
|
||||
float sunR = size * 0.1f;
|
||||
drawList->AddCircleFilled(ImVec2(pos.x + size * 0.75f, pos.y + size * 0.35f), sunR, IM_COL32(255, 220, 100, 255));
|
||||
}
|
||||
|
||||
// Draw a shader icon (code brackets)
|
||||
void DrawShaderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float padding = size * 0.15f;
|
||||
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
|
||||
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
|
||||
|
||||
// Background
|
||||
drawList->AddRectFilled(tl, br, color, 3.0f);
|
||||
|
||||
// Code lines
|
||||
ImU32 lineColor = IM_COL32(255, 255, 255, 180);
|
||||
float lineY = pos.y + size * 0.35f;
|
||||
float lineH = size * 0.08f;
|
||||
float lineSpacing = size * 0.15f;
|
||||
|
||||
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.7f, lineY + lineH), lineColor);
|
||||
lineY += lineSpacing;
|
||||
drawList->AddRectFilled(ImVec2(pos.x + size * 0.3f, lineY), ImVec2(pos.x + size * 0.8f, lineY + lineH), lineColor);
|
||||
lineY += lineSpacing;
|
||||
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.55f, lineY + lineH), lineColor);
|
||||
}
|
||||
|
||||
// Draw an audio icon (speaker/waveform)
|
||||
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
// Speaker body
|
||||
float spkW = size * 0.25f;
|
||||
float spkH = size * 0.3f;
|
||||
float cx = pos.x + size * 0.35f;
|
||||
float cy = pos.y + size * 0.5f;
|
||||
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(cx - spkW * 0.5f, cy - spkH * 0.5f),
|
||||
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
|
||||
color
|
||||
);
|
||||
|
||||
// Speaker cone
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
|
||||
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
|
||||
ImVec2(cx + spkW * 1.2f, cy + spkH),
|
||||
color
|
||||
);
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
|
||||
ImVec2(cx + spkW * 1.2f, cy - spkH),
|
||||
ImVec2(cx + spkW * 1.2f, cy + spkH),
|
||||
color
|
||||
);
|
||||
|
||||
// Sound waves
|
||||
ImU32 waveColor = IM_COL32(255, 255, 255, 150);
|
||||
float waveX = cx + spkW * 1.5f;
|
||||
drawList->AddBezierQuadratic(
|
||||
ImVec2(waveX, cy - size * 0.15f),
|
||||
ImVec2(waveX + size * 0.1f, cy),
|
||||
ImVec2(waveX, cy + size * 0.15f),
|
||||
waveColor, 2.0f
|
||||
);
|
||||
waveX += size * 0.12f;
|
||||
drawList->AddBezierQuadratic(
|
||||
ImVec2(waveX, cy - size * 0.22f),
|
||||
ImVec2(waveX + size * 0.12f, cy),
|
||||
ImVec2(waveX, cy + size * 0.22f),
|
||||
waveColor, 2.0f
|
||||
);
|
||||
}
|
||||
|
||||
// Draw a generic file icon
|
||||
void DrawFileIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float w = size * 0.7f;
|
||||
float h = size * 0.9f;
|
||||
float offsetX = (size - w) * 0.5f;
|
||||
float offsetY = (size - h) * 0.5f;
|
||||
float cornerSize = w * 0.25f;
|
||||
|
||||
ImVec2 p1 = ImVec2(pos.x + offsetX, pos.y + offsetY);
|
||||
ImVec2 p2 = ImVec2(pos.x + offsetX + w - cornerSize, pos.y + offsetY);
|
||||
ImVec2 p3 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + cornerSize);
|
||||
ImVec2 p4 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h);
|
||||
ImVec2 p5 = ImVec2(pos.x + offsetX, pos.y + offsetY + h);
|
||||
|
||||
// Main body
|
||||
drawList->AddQuadFilled(p1, p2, ImVec2(p2.x, p4.y), p5, color);
|
||||
drawList->AddTriangleFilled(p2, p3, ImVec2(p2.x, p3.y), color);
|
||||
drawList->AddRectFilled(ImVec2(p2.x, p3.y), p4, color);
|
||||
|
||||
// Corner fold
|
||||
drawList->AddTriangleFilled(p2, ImVec2(p2.x, p3.y), p3, IM_COL32(255, 255, 255, 50));
|
||||
}
|
||||
|
||||
// Draw a script/code icon
|
||||
void DrawScriptIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
float padding = size * 0.12f;
|
||||
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
|
||||
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
|
||||
|
||||
// Background
|
||||
drawList->AddRectFilled(tl, br, color, 3.0f);
|
||||
|
||||
// Brackets < >
|
||||
ImU32 bracketColor = IM_COL32(255, 255, 255, 200);
|
||||
float cx = pos.x + size * 0.5f;
|
||||
float cy = pos.y + size * 0.5f;
|
||||
float bSize = size * 0.2f;
|
||||
|
||||
// Left bracket <
|
||||
drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.5f, cy), bracketColor, 2.5f);
|
||||
drawList->AddLine(ImVec2(cx - bSize * 1.5f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
|
||||
|
||||
// Right bracket >
|
||||
drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.5f, cy), bracketColor, 2.5f);
|
||||
drawList->AddLine(ImVec2(cx + bSize * 1.5f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
|
||||
}
|
||||
|
||||
// Draw a text icon
|
||||
void DrawTextIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
|
||||
DrawFileIcon(drawList, pos, size, color);
|
||||
|
||||
// Text lines
|
||||
ImU32 lineColor = IM_COL32(255, 255, 255, 150);
|
||||
float startX = pos.x + size * 0.25f;
|
||||
float endX = pos.x + size * 0.65f;
|
||||
float lineY = pos.y + size * 0.4f;
|
||||
float lineH = size * 0.06f;
|
||||
float spacing = size * 0.12f;
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float w = (i == 1) ? (endX - startX) * 0.7f : (endX - startX);
|
||||
drawList->AddRectFilled(ImVec2(startX, lineY), ImVec2(startX + w, lineY + lineH), lineColor);
|
||||
lineY += spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawIcon(ImDrawList* drawList, FileCategory category, ImVec2 pos, float size, ImU32 color) {
|
||||
switch (category) {
|
||||
case FileCategory::Folder: DrawFolderIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Scene: DrawSceneIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Model: DrawModelIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Material:DrawShaderIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Texture: DrawTextureIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Shader: DrawShaderIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Script: DrawScriptIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Audio: DrawAudioIcon(drawList, pos, size, color); break;
|
||||
case FileCategory::Text: DrawTextIcon(drawList, pos, size, color); break;
|
||||
default: DrawFileIcon(drawList, pos, size, color); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Engine::renderFileBrowserPanel() {
|
||||
ImGui::Begin("Project", &showFileBrowser);
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
|
||||
toolbarBg.x = std::min(toolbarBg.x + 0.02f, 1.0f);
|
||||
toolbarBg.y = std::min(toolbarBg.y + 0.02f, 1.0f);
|
||||
toolbarBg.z = std::min(toolbarBg.z + 0.02f, 1.0f);
|
||||
|
||||
if (fileBrowser.needsRefresh) {
|
||||
fileBrowser.refresh();
|
||||
}
|
||||
|
||||
// Get colors for categories
|
||||
auto getCategoryColor = [](FileCategory cat) -> ImU32 {
|
||||
switch (cat) {
|
||||
case FileCategory::Folder: return IM_COL32(255, 200, 80, 255); // Yellow/orange
|
||||
case FileCategory::Scene: return IM_COL32(100, 180, 255, 255); // Blue
|
||||
case FileCategory::Model: return IM_COL32(100, 220, 140, 255); // Green
|
||||
case FileCategory::Material:return IM_COL32(220, 200, 120, 255); // Gold
|
||||
case FileCategory::Texture: return IM_COL32(220, 130, 220, 255); // Purple/pink
|
||||
case FileCategory::Shader: return IM_COL32(255, 140, 90, 255); // Orange
|
||||
case FileCategory::Script: return IM_COL32(130, 200, 255, 255); // Light blue
|
||||
case FileCategory::Audio: return IM_COL32(255, 180, 100, 255); // Warm orange
|
||||
case FileCategory::Text: return IM_COL32(180, 180, 180, 255); // Gray
|
||||
default: return IM_COL32(150, 150, 150, 255); // Dark gray
|
||||
}
|
||||
};
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, toolbarBg);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 3.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 3.0f));
|
||||
ImGui::BeginChild("ProjectToolbar", ImVec2(0, 44), true, ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 3.0f));
|
||||
bool canGoBack = fileBrowser.historyIndex > 0;
|
||||
bool canGoForward = fileBrowser.historyIndex < (int)fileBrowser.pathHistory.size() - 1;
|
||||
bool canGoUp = fileBrowser.currentPath != fileBrowser.projectRoot &&
|
||||
fileBrowser.currentPath.has_parent_path();
|
||||
|
||||
ImGui::BeginDisabled(!canGoBack);
|
||||
ImGui::Button("<##Back", ImVec2(26, 0));
|
||||
if (ImGui::IsItemActivated()) fileBrowser.navigateBack();
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Back");
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!canGoForward);
|
||||
ImGui::Button(">##Forward", ImVec2(26, 0));
|
||||
if (ImGui::IsItemActivated()) fileBrowser.navigateForward();
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Forward");
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!canGoUp);
|
||||
ImGui::Button("^##Up", ImVec2(26, 0));
|
||||
if (ImGui::IsItemActivated()) fileBrowser.navigateUp();
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Up one folder");
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.5f));
|
||||
|
||||
fs::path relativePath;
|
||||
if (fileBrowser.projectRoot.empty()) {
|
||||
relativePath = fileBrowser.currentPath.filename();
|
||||
} else {
|
||||
try {
|
||||
relativePath = fs::relative(fileBrowser.currentPath, fileBrowser.projectRoot);
|
||||
} catch (...) {
|
||||
relativePath = fileBrowser.currentPath.filename();
|
||||
}
|
||||
}
|
||||
std::vector<fs::path> pathParts;
|
||||
fs::path accumulated = fileBrowser.projectRoot;
|
||||
|
||||
pathParts.push_back(fileBrowser.projectRoot);
|
||||
for (const auto& part : relativePath) {
|
||||
if (part != ".") {
|
||||
accumulated /= part;
|
||||
pathParts.push_back(accumulated);
|
||||
}
|
||||
}
|
||||
|
||||
struct Breadcrumb {
|
||||
std::string label;
|
||||
fs::path target;
|
||||
};
|
||||
std::vector<Breadcrumb> crumbs;
|
||||
if (pathParts.size() <= 4) {
|
||||
for (size_t i = 0; i < pathParts.size(); ++i) {
|
||||
std::string name = (i == 0) ? "Project" : pathParts[i].filename().string();
|
||||
crumbs.push_back({name, pathParts[i]});
|
||||
}
|
||||
} else {
|
||||
crumbs.push_back({"Project", pathParts.front()});
|
||||
crumbs.push_back({"..", pathParts[pathParts.size() - 3]});
|
||||
crumbs.push_back({pathParts[pathParts.size() - 2].filename().string(), pathParts[pathParts.size() - 2]});
|
||||
crumbs.push_back({pathParts.back().filename().string(), pathParts.back()});
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < crumbs.size(); i++) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::SmallButton(crumbs[i].label.c_str())) {
|
||||
fileBrowser.navigateTo(crumbs[i].target);
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (i < crumbs.size() - 1) {
|
||||
ImGui::SameLine(0, 2);
|
||||
ImGui::TextDisabled("/");
|
||||
ImGui::SameLine(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140);
|
||||
if (ImGui::InputTextWithHint("##Search", "Search...", fileBrowserSearch, sizeof(fileBrowserSearch))) {
|
||||
fileBrowser.searchFilter = fileBrowserSearch;
|
||||
fileBrowser.needsRefresh = true;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
bool isGridMode = fileBrowser.viewMode == FileBrowserViewMode::Grid;
|
||||
if (isGridMode) {
|
||||
ImGui::TextDisabled("Size");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(90);
|
||||
ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
|
||||
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh", ImVec2(68, 0))) {
|
||||
fileBrowser.needsRefresh = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("New Mat", ImVec2(78, 0))) {
|
||||
fs::path target = fileBrowser.currentPath / "NewMaterial.mat";
|
||||
int counter = 1;
|
||||
while (fs::exists(target)) {
|
||||
target = fileBrowser.currentPath / ("NewMaterial" + std::to_string(counter++) + ".mat");
|
||||
}
|
||||
SceneObject temp("Material", ObjectType::Cube, -1);
|
||||
temp.materialPath = target.string();
|
||||
saveMaterialToFile(temp);
|
||||
fileBrowser.needsRefresh = true;
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// === FILE CONTENT AREA ===
|
||||
ImVec4 contentBg = style.Colors[ImGuiCol_WindowBg];
|
||||
contentBg.x = std::min(contentBg.x + 0.01f, 1.0f);
|
||||
contentBg.y = std::min(contentBg.y + 0.01f, 1.0f);
|
||||
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
|
||||
ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
if (fileBrowser.viewMode == FileBrowserViewMode::Grid) {
|
||||
float baseIconSize = 64.0f;
|
||||
float iconSize = baseIconSize * fileBrowserIconScale;
|
||||
float padding = 8.0f * fileBrowserIconScale;
|
||||
float textHeight = 32.0f; // Space for filename text
|
||||
float cellWidth = iconSize + padding * 2;
|
||||
float cellHeight = iconSize + padding * 2 + textHeight;
|
||||
|
||||
float windowWidth = ImGui::GetContentRegionAvail().x;
|
||||
int columns = std::max(1, (int)((windowWidth + padding) / (cellWidth + padding)));
|
||||
|
||||
// Use a table for consistent grid layout
|
||||
if (ImGui::BeginTable("FileGrid", columns, ImGuiTableFlags_NoPadInnerX)) {
|
||||
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
|
||||
const auto& entry = fileBrowser.entries[i];
|
||||
std::string filename = entry.path().filename().string();
|
||||
FileCategory category = fileBrowser.getFileCategory(entry);
|
||||
bool isSelected = fileBrowser.selectedFile == entry.path();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushID(i);
|
||||
|
||||
// Cell content area
|
||||
ImVec2 cellStart = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cellEnd(cellStart.x + cellWidth, cellStart.y + cellHeight);
|
||||
|
||||
// Invisible button for the entire cell
|
||||
if (ImGui::InvisibleButton("##cell", ImVec2(cellWidth, cellHeight))) {
|
||||
fileBrowser.selectedFile = entry.path();
|
||||
}
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(0);
|
||||
|
||||
// Draw background
|
||||
ImU32 bgColor = isSelected ? IM_COL32(70, 110, 160, 200) :
|
||||
(hovered ? IM_COL32(60, 65, 75, 180) : IM_COL32(0, 0, 0, 0));
|
||||
if (bgColor != IM_COL32(0, 0, 0, 0)) {
|
||||
drawList->AddRectFilled(cellStart, cellEnd, bgColor, 6.0f);
|
||||
}
|
||||
|
||||
// Draw border on selection
|
||||
if (isSelected) {
|
||||
drawList->AddRect(cellStart, cellEnd, IM_COL32(100, 150, 220, 255), 6.0f, 0, 2.0f);
|
||||
}
|
||||
|
||||
// Draw icon centered in cell
|
||||
ImVec2 iconPos(
|
||||
cellStart.x + (cellWidth - iconSize) * 0.5f,
|
||||
cellStart.y + padding
|
||||
);
|
||||
FileIcons::DrawIcon(drawList, category, iconPos, iconSize, getCategoryColor(category));
|
||||
|
||||
// Draw filename below icon (centered, with wrapping)
|
||||
std::string displayName = filename;
|
||||
float maxTextWidth = cellWidth - 4;
|
||||
|
||||
// Truncate if too long
|
||||
ImVec2 textSize = ImGui::CalcTextSize(displayName.c_str());
|
||||
if (textSize.x > maxTextWidth) {
|
||||
while (displayName.length() > 3) {
|
||||
displayName.pop_back();
|
||||
if (ImGui::CalcTextSize((displayName + "...").c_str()).x <= maxTextWidth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
displayName += "...";
|
||||
textSize = ImGui::CalcTextSize(displayName.c_str());
|
||||
}
|
||||
|
||||
ImVec2 textPos(
|
||||
cellStart.x + (cellWidth - textSize.x) * 0.5f,
|
||||
cellStart.y + padding + iconSize + 4
|
||||
);
|
||||
|
||||
// Text with subtle shadow for readability
|
||||
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 100), displayName.c_str());
|
||||
drawList->AddText(textPos, IM_COL32(230, 230, 230, 255), displayName.c_str());
|
||||
|
||||
// Handle double click
|
||||
if (doubleClicked) {
|
||||
if (entry.is_directory()) {
|
||||
fileBrowser.navigateTo(entry.path());
|
||||
} else if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
std::string defaultName = entry.path().stem().string();
|
||||
if (isObj) {
|
||||
pendingOBJPath = entry.path().string();
|
||||
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
||||
showImportOBJDialog = true;
|
||||
} else {
|
||||
pendingModelPath = entry.path().string();
|
||||
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
|
||||
showImportModelDialog = true;
|
||||
}
|
||||
} else if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||
if (SceneObject* sel = getSelectedObject()) {
|
||||
sel->materialPath = entry.path().string();
|
||||
loadMaterialFromFile(*sel);
|
||||
}
|
||||
} else if (fileBrowser.isSceneFile(entry)) {
|
||||
std::string sceneName = entry.path().stem().string();
|
||||
loadScene(sceneName);
|
||||
logToConsole("Loaded scene: " + sceneName);
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu
|
||||
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
|
||||
if (ImGui::MenuItem("Open")) {
|
||||
if (entry.is_directory()) {
|
||||
fileBrowser.navigateTo(entry.path());
|
||||
} else if (fileBrowser.isSceneFile(entry)) {
|
||||
std::string sceneName = entry.path().stem().string();
|
||||
loadScene(sceneName);
|
||||
}
|
||||
}
|
||||
if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
if (ImGui::MenuItem("Import to Scene")) {
|
||||
std::string defaultName = entry.path().stem().string();
|
||||
if (isObj) {
|
||||
pendingOBJPath = entry.path().string();
|
||||
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
||||
showImportOBJDialog = true;
|
||||
} else {
|
||||
pendingModelPath = entry.path().string();
|
||||
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
|
||||
showImportModelDialog = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Quick Import")) {
|
||||
if (isObj) {
|
||||
importOBJToScene(entry.path().string(), "");
|
||||
} else {
|
||||
importModelToScene(entry.path().string(), "");
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Convert to Raw Mesh")) {
|
||||
convertModelToRawMesh(entry.path().string());
|
||||
}
|
||||
}
|
||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||
if (ImGui::MenuItem("Apply to Selected")) {
|
||||
if (SceneObject* sel = getSelectedObject()) {
|
||||
sel->materialPath = entry.path().string();
|
||||
loadMaterialFromFile(*sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
|
||||
if (ImGui::MenuItem("Compile Script")) {
|
||||
compileScriptFile(entry.path());
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Show in Explorer")) {
|
||||
#ifdef _WIN32
|
||||
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
|
||||
system(cmd.c_str());
|
||||
#elif __linux__
|
||||
std::string cmd = "xdg-open \"" + entry.path().parent_path().string() + "\"";
|
||||
system(cmd.c_str());
|
||||
#endif
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
} else {
|
||||
// List View
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2));
|
||||
|
||||
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
|
||||
const auto& entry = fileBrowser.entries[i];
|
||||
std::string filename = entry.path().filename().string();
|
||||
FileCategory category = fileBrowser.getFileCategory(entry);
|
||||
bool isSelected = fileBrowser.selectedFile == entry.path();
|
||||
|
||||
ImGui::PushID(i);
|
||||
|
||||
// Selectable row
|
||||
if (ImGui::Selectable("##row", isSelected, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 20))) {
|
||||
fileBrowser.selectedFile = entry.path();
|
||||
|
||||
if (ImGui::IsMouseDoubleClicked(0)) {
|
||||
if (entry.is_directory()) {
|
||||
fileBrowser.navigateTo(entry.path());
|
||||
} else if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
std::string defaultName = entry.path().stem().string();
|
||||
if (isObj) {
|
||||
pendingOBJPath = entry.path().string();
|
||||
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
||||
showImportOBJDialog = true;
|
||||
} else {
|
||||
pendingModelPath = entry.path().string();
|
||||
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
|
||||
showImportModelDialog = true;
|
||||
}
|
||||
} else if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||
if (SceneObject* sel = getSelectedObject()) {
|
||||
sel->materialPath = entry.path().string();
|
||||
loadMaterialFromFile(*sel);
|
||||
}
|
||||
} else if (fileBrowser.isSceneFile(entry)) {
|
||||
std::string sceneName = entry.path().stem().string();
|
||||
loadScene(sceneName);
|
||||
logToConsole("Loaded scene: " + sceneName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu
|
||||
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
|
||||
if (ImGui::MenuItem("Open")) {
|
||||
if (entry.is_directory()) {
|
||||
fileBrowser.navigateTo(entry.path());
|
||||
} else if (fileBrowser.isSceneFile(entry)) {
|
||||
std::string sceneName = entry.path().stem().string();
|
||||
loadScene(sceneName);
|
||||
}
|
||||
}
|
||||
if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
bool isRaw = ext == ".rmesh";
|
||||
if (ImGui::MenuItem("Import to Scene")) {
|
||||
std::string defaultName = entry.path().stem().string();
|
||||
if (isObj) {
|
||||
pendingOBJPath = entry.path().string();
|
||||
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
||||
showImportOBJDialog = true;
|
||||
} else {
|
||||
pendingModelPath = entry.path().string();
|
||||
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
|
||||
showImportModelDialog = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Quick Import")) {
|
||||
if (isObj) {
|
||||
importOBJToScene(entry.path().string(), "");
|
||||
} else {
|
||||
importModelToScene(entry.path().string(), "");
|
||||
}
|
||||
}
|
||||
if (!isRaw && ImGui::MenuItem("Convert to Raw Mesh")) {
|
||||
convertModelToRawMesh(entry.path().string());
|
||||
}
|
||||
}
|
||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
|
||||
if (ImGui::MenuItem("Apply to Selected")) {
|
||||
if (SceneObject* sel = getSelectedObject()) {
|
||||
sel->materialPath = entry.path().string();
|
||||
loadMaterialFromFile(*sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
|
||||
if (ImGui::MenuItem("Compile Script")) {
|
||||
compileScriptFile(entry.path());
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Show in Explorer")) {
|
||||
#ifdef _WIN32
|
||||
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
|
||||
system(cmd.c_str());
|
||||
#elif __linux__
|
||||
std::string cmd = "xdg-open \"" + entry.path().parent_path().string() + "\"";
|
||||
system(cmd.c_str());
|
||||
#endif
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Draw icon inline
|
||||
ImGui::SameLine(4);
|
||||
ImVec2 iconPos = ImGui::GetCursorScreenPos();
|
||||
iconPos.y -= 2;
|
||||
FileIcons::DrawIcon(drawList, category, iconPos, 16, getCategoryColor(category));
|
||||
|
||||
ImGui::SameLine(26);
|
||||
|
||||
// Color-coded filename
|
||||
ImVec4 textColor;
|
||||
switch (category) {
|
||||
case FileCategory::Folder: textColor = ImVec4(1.0f, 0.85f, 0.4f, 1.0f); break;
|
||||
case FileCategory::Scene: textColor = ImVec4(0.5f, 0.75f, 1.0f, 1.0f); break;
|
||||
case FileCategory::Model: textColor = ImVec4(0.5f, 0.9f, 0.6f, 1.0f); break;
|
||||
case FileCategory::Material:textColor = ImVec4(0.95f, 0.8f, 0.45f, 1.0f); break;
|
||||
case FileCategory::Texture: textColor = ImVec4(0.9f, 0.6f, 0.9f, 1.0f); break;
|
||||
default: textColor = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); break;
|
||||
}
|
||||
ImGui::TextColored(textColor, "%s", filename.c_str());
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
741
src/EditorWindows/ProjectManagerWindow.cpp
Normal file
741
src/EditorWindows/ProjectManagerWindow.cpp
Normal file
@@ -0,0 +1,741 @@
|
||||
#include "Engine.h"
|
||||
#include "ModelLoader.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
namespace ImGui {
|
||||
|
||||
// Animated progress bar that keeps circles moving while work happens in the background.
|
||||
bool BufferingBar(const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col) {
|
||||
ImGuiWindow* window = GetCurrentWindow();
|
||||
if (window->SkipItems)
|
||||
return false;
|
||||
|
||||
ImGuiContext& g = *GImGui;
|
||||
const ImGuiStyle& style = g.Style;
|
||||
const ImGuiID id = window->GetID(label);
|
||||
|
||||
ImVec2 pos = window->DC.CursorPos;
|
||||
ImVec2 size = size_arg;
|
||||
size.x -= style.FramePadding.x * 2;
|
||||
|
||||
const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
|
||||
ItemSize(bb, style.FramePadding.y);
|
||||
if (!ItemAdd(bb, id))
|
||||
return false;
|
||||
|
||||
const float circleStart = size.x * 0.7f;
|
||||
const float circleEnd = size.x;
|
||||
const float circleWidth = circleEnd - circleStart;
|
||||
|
||||
window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart, bb.Max.y), bg_col);
|
||||
window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart * value, bb.Max.y), fg_col);
|
||||
|
||||
const float t = g.Time;
|
||||
const float r = size.y / 2;
|
||||
const float speed = 1.5f;
|
||||
|
||||
const float a = speed * 0;
|
||||
const float b = speed * 0.333f;
|
||||
const float c = speed * 0.666f;
|
||||
|
||||
const float o1 = (circleWidth + r) * (t + a - speed * (int)((t + a) / speed)) / speed;
|
||||
const float o2 = (circleWidth + r) * (t + b - speed * (int)((t + b) / speed)) / speed;
|
||||
const float o3 = (circleWidth + r) * (t + c - speed * (int)((t + c) / speed)) / speed;
|
||||
|
||||
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o1, bb.Min.y + r), r, bg_col);
|
||||
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o2, bb.Min.y + r), r, bg_col);
|
||||
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o3, bb.Min.y + r), r, bg_col);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Simple loading spinner for indeterminate tasks.
|
||||
bool Spinner(const char* label, float radius, int thickness, const ImU32& color) {
|
||||
ImGuiWindow* window = GetCurrentWindow();
|
||||
if (window->SkipItems)
|
||||
return false;
|
||||
|
||||
ImGuiContext& g = *GImGui;
|
||||
const ImGuiStyle& style = g.Style;
|
||||
const ImGuiID id = window->GetID(label);
|
||||
|
||||
ImVec2 pos = window->DC.CursorPos;
|
||||
ImVec2 size((radius) * 2, (radius + style.FramePadding.y) * 2);
|
||||
|
||||
const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
|
||||
ItemSize(bb, style.FramePadding.y);
|
||||
if (!ItemAdd(bb, id))
|
||||
return false;
|
||||
|
||||
window->DrawList->PathClear();
|
||||
|
||||
int num_segments = 30;
|
||||
int start = abs(ImSin(g.Time * 1.8f) * (num_segments - 5));
|
||||
|
||||
const float a_min = IM_PI * 2.0f * ((float)start) / (float)num_segments;
|
||||
const float a_max = IM_PI * 2.0f * ((float)num_segments - 3) / (float)num_segments;
|
||||
|
||||
const ImVec2 centre = ImVec2(pos.x + radius, pos.y + radius + style.FramePadding.y);
|
||||
|
||||
for (int i = 0; i < num_segments; i++) {
|
||||
const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min);
|
||||
window->DrawList->PathLineTo(ImVec2(centre.x + ImCos(a + g.Time * 8) * radius,
|
||||
centre.y + ImSin(a + g.Time * 8) * radius));
|
||||
}
|
||||
|
||||
window->DrawList->PathStroke(color, false, thickness);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ImGui
|
||||
|
||||
namespace {
|
||||
struct PackageTaskResult {
|
||||
bool success = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct PackageTaskState {
|
||||
bool active = false;
|
||||
float startTime = 0.0f;
|
||||
std::string label;
|
||||
std::future<PackageTaskResult> future;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
void Engine::renderLauncher() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 displaySize = io.DisplaySize;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.09f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowSize(displaySize);
|
||||
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
||||
|
||||
if (ImGui::Begin("Launcher", nullptr, flags))
|
||||
{
|
||||
float leftPanelWidth = 280.0f;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.06f, 0.06f, 0.07f, 1.0f));
|
||||
ImGui::BeginChild("LauncherLeft", ImVec2(leftPanelWidth, 0), true);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.45f, 0.72f, 0.95f, 1.0f), "MODULARITY");
|
||||
ImGui::TextDisabled("Game Engine");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "GET STARTED");
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.38f, 0.55f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.48f, 0.68f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.42f, 0.60f, 1.0f));
|
||||
|
||||
if (ImGui::Button("New Project", ImVec2(-1, 36.0f)))
|
||||
{
|
||||
projectManager.showNewProjectDialog = true;
|
||||
projectManager.errorMessage.clear();
|
||||
std::memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
|
||||
|
||||
#ifdef _WIN32
|
||||
char documentsPath[MAX_PATH];
|
||||
SHGetFolderPathA(NULL, CSIDL_MYDOCUMENTS, NULL, 0, documentsPath);
|
||||
std::strcpy(projectManager.newProjectLocation, documentsPath);
|
||||
std::strcat(projectManager.newProjectLocation, "\\ModularityProjects");
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home)
|
||||
{
|
||||
std::strcpy(projectManager.newProjectLocation, home);
|
||||
std::strcat(projectManager.newProjectLocation, "/ModularityProjects");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Open Project", ImVec2(-1, 36.0f)))
|
||||
{
|
||||
projectManager.showOpenProjectDialog = true;
|
||||
projectManager.errorMessage.clear();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "QUICK ACTIONS");
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Documentation", ImVec2(-1, 30.0f)))
|
||||
{
|
||||
#ifdef _WIN32
|
||||
system("start https://github.com");
|
||||
#else
|
||||
system("xdg-open https://github.com &");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (ImGui::Button("Exit", ImVec2(-1, 30.0f)))
|
||||
{
|
||||
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.11f, 1.0f));
|
||||
ImGui::BeginChild("LauncherRight", ImVec2(0, 0), true);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "RECENT PROJECTS");
|
||||
ImGui::Spacing();
|
||||
|
||||
if (projectManager.recentProjects.empty())
|
||||
{
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("No recent projects");
|
||||
ImGui::TextDisabled("Create a new project to get started!");
|
||||
}
|
||||
else
|
||||
{
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
for (size_t i = 0; i < projectManager.recentProjects.size(); ++i)
|
||||
{
|
||||
const auto& rp = projectManager.recentProjects[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
char label[512];
|
||||
std::snprintf(label, sizeof(label), "%s\n%s",
|
||||
rp.name.c_str(), rp.path.c_str());
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f));
|
||||
|
||||
bool selected = ImGui::Selectable(
|
||||
label,
|
||||
false,
|
||||
ImGuiSelectableFlags_AllowDoubleClick,
|
||||
ImVec2(availWidth, 48.0f)
|
||||
);
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
// Dummy to extend window bounds properly
|
||||
ImGui::Dummy(ImVec2(0, 0));
|
||||
|
||||
if (selected || ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
OpenProjectPath(rp.path);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem("RecentProjectContext"))
|
||||
{
|
||||
if (ImGui::MenuItem("Open"))
|
||||
{
|
||||
OpenProjectPath(rp.path);
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("Remove from Recent"))
|
||||
{
|
||||
projectManager.recentProjects.erase(
|
||||
projectManager.recentProjects.begin() + i
|
||||
);
|
||||
projectManager.saveRecentProjects();
|
||||
ImGui::EndPopup();
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextDisabled("Modularity Engine - Version 0.6.8");
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
if (projectManager.showNewProjectDialog)
|
||||
renderNewProjectDialog();
|
||||
if (projectManager.showOpenProjectDialog)
|
||||
renderOpenProjectDialog();
|
||||
}
|
||||
|
||||
void Engine::renderNewProjectDialog() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
||||
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 250), ImGuiCond_Appearing);
|
||||
|
||||
if (ImGui::Begin("New Project", &projectManager.showNewProjectDialog,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
||||
|
||||
ImGui::Text("Project Name:");
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##ProjectName", projectManager.newProjectName,
|
||||
sizeof(projectManager.newProjectName));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Choose Pipeline Mode")) {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Location:");
|
||||
ImGui::SetNextItemWidth(-70);
|
||||
ImGui::InputText("##Location", projectManager.newProjectLocation,
|
||||
sizeof(projectManager.newProjectLocation));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse")) {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (strlen(projectManager.newProjectName) > 0) {
|
||||
fs::path previewPath = fs::path(projectManager.newProjectLocation) /
|
||||
projectManager.newProjectName;
|
||||
ImGui::TextDisabled("Project will be created at:");
|
||||
ImGui::TextWrapped("%s", previewPath.string().c_str());
|
||||
}
|
||||
|
||||
if (!projectManager.errorMessage.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
|
||||
projectManager.errorMessage.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float buttonWidth = 100;
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
||||
|
||||
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
||||
projectManager.showNewProjectDialog = false;
|
||||
memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.4f, 1.0f));
|
||||
if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) {
|
||||
if (strlen(projectManager.newProjectName) == 0) {
|
||||
projectManager.errorMessage = "Please enter a project name";
|
||||
} else if (strlen(projectManager.newProjectLocation) == 0) {
|
||||
projectManager.errorMessage = "Please specify a location";
|
||||
} else {
|
||||
createNewProject(projectManager.newProjectName,
|
||||
projectManager.newProjectLocation);
|
||||
projectManager.showNewProjectDialog = false;
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void Engine::renderOpenProjectDialog() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
||||
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Appearing);
|
||||
|
||||
if (ImGui::Begin("Open Project", &projectManager.showOpenProjectDialog,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
||||
|
||||
ImGui::Text("Project File Path (.modu):");
|
||||
ImGui::SetNextItemWidth(-70);
|
||||
ImGui::InputText("##OpenPath", projectManager.openProjectPath,
|
||||
sizeof(projectManager.openProjectPath));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse")) {
|
||||
}
|
||||
|
||||
ImGui::TextDisabled("Select a project.modu file");
|
||||
|
||||
if (!projectManager.errorMessage.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
|
||||
projectManager.errorMessage.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float buttonWidth = 100;
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
||||
|
||||
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
||||
projectManager.showOpenProjectDialog = false;
|
||||
memset(projectManager.openProjectPath, 0, sizeof(projectManager.openProjectPath));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Open", ImVec2(buttonWidth, 0))) {
|
||||
if (strlen(projectManager.openProjectPath) == 0) {
|
||||
projectManager.errorMessage = "Please enter a project path";
|
||||
} else {
|
||||
OpenProjectPath(projectManager.openProjectPath);
|
||||
if (!projectManager.errorMessage.empty()) {
|
||||
// Error handled in OpenProjectPath
|
||||
} else {
|
||||
projectManager.showOpenProjectDialog = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void Engine::renderProjectBrowserPanel() {
|
||||
ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f);
|
||||
ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, headerCol);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, headerColActive);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, headerColActive);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
|
||||
|
||||
ImGui::Begin("Project Settings", &showProjectBrowser);
|
||||
|
||||
if (!projectManager.currentProject.isLoaded) {
|
||||
ImGui::TextDisabled("No project loaded");
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(3);
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "%s", projectManager.currentProject.name.c_str());
|
||||
if (projectManager.currentProject.hasUnsavedChanges) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "*");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
static int selectedTab = 0;
|
||||
const char* tabs[] = { "Scenes", "Packages", "Assets" };
|
||||
|
||||
ImGui::BeginChild("SettingsNav", ImVec2(180, 0), true);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (ImGui::Selectable(tabs[i], selectedTab == i, 0, ImVec2(0, 32))) {
|
||||
selectedTab = i;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("SettingsBody", ImVec2(0, 0), false);
|
||||
|
||||
if (selectedTab == 0) {
|
||||
if (ImGui::Button("+ New Scene")) {
|
||||
showNewSceneDialog = true;
|
||||
memset(newSceneName, 0, sizeof(newSceneName));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
auto scenes = projectManager.currentProject.getSceneList();
|
||||
for (const auto& scene : scenes) {
|
||||
bool isCurrentScene = (scene == projectManager.currentProject.currentSceneName);
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
if (isCurrentScene) flags |= ImGuiTreeNodeFlags_Selected;
|
||||
|
||||
ImGui::TreeNodeEx(scene.c_str(), flags, "[S] %s", scene.c_str());
|
||||
|
||||
if (ImGui::IsItemClicked() && !isCurrentScene) {
|
||||
loadScene(scene);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Load") && !isCurrentScene) {
|
||||
loadScene(scene);
|
||||
}
|
||||
if (ImGui::MenuItem("Duplicate")) {
|
||||
addConsoleMessage("Scene duplication not yet implemented.", ConsoleMessageType::Info);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Delete") && !isCurrentScene) {
|
||||
fs::remove(projectManager.currentProject.getSceneFilePath(scene));
|
||||
addConsoleMessage("Deleted scene: " + scene, ConsoleMessageType::Info);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
if (scenes.empty()) {
|
||||
ImGui::TextDisabled("No scenes yet");
|
||||
}
|
||||
} else if (selectedTab == 1) {
|
||||
static PackageTaskState packageTask;
|
||||
static std::string packageStatus;
|
||||
static char gitUrlBuf[256] = "";
|
||||
static char gitNameBuf[128] = "";
|
||||
static char gitIncludeBuf[128] = "include";
|
||||
|
||||
auto pollPackageTask = [&]() {
|
||||
if (!packageTask.active) return;
|
||||
if (!packageTask.future.valid()) {
|
||||
packageTask.active = false;
|
||||
return;
|
||||
}
|
||||
auto state = packageTask.future.wait_for(std::chrono::milliseconds(0));
|
||||
if (state == std::future_status::ready) {
|
||||
PackageTaskResult result = packageTask.future.get();
|
||||
packageStatus = result.message;
|
||||
packageTask.active = false;
|
||||
packageTask.future = std::future<PackageTaskResult>();
|
||||
}
|
||||
};
|
||||
|
||||
auto startPackageTask = [&](const char* label, std::function<PackageTaskResult()> fn) {
|
||||
if (packageTask.active) return;
|
||||
packageTask.active = true;
|
||||
packageTask.label = label;
|
||||
packageTask.startTime = static_cast<float>(ImGui::GetTime());
|
||||
packageTask.future = std::async(std::launch::async, std::move(fn));
|
||||
};
|
||||
|
||||
pollPackageTask();
|
||||
|
||||
if (!packageStatus.empty()) {
|
||||
ImGui::TextWrapped("%s", packageStatus.c_str());
|
||||
}
|
||||
|
||||
if (packageTask.active) {
|
||||
const ImU32 col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
|
||||
const ImU32 bg = ImGui::GetColorU32(ImGuiCol_Button);
|
||||
float elapsed = static_cast<float>(ImGui::GetTime()) - packageTask.startTime;
|
||||
float phase = std::fmod(elapsed * 0.25f, 1.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("%s", packageTask.label.c_str());
|
||||
ImGui::BufferingBar("##pkg_buffer", phase, ImVec2(ImGui::GetContentRegionAvail().x, 6.0f), bg, col);
|
||||
ImGui::Spinner("##pkg_spinner", 10.0f, 4, col);
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(packageTask.active);
|
||||
ImGui::TextDisabled("Add package from Git");
|
||||
ImGui::InputTextWithHint("URL", "https://github.com/user/repo.git", gitUrlBuf, sizeof(gitUrlBuf));
|
||||
ImGui::InputTextWithHint("Name (optional)", "use repo name", gitNameBuf, sizeof(gitNameBuf));
|
||||
ImGui::InputTextWithHint("Include dir", "include", gitIncludeBuf, sizeof(gitIncludeBuf));
|
||||
if (ImGui::Button("Add as submodule")) {
|
||||
std::string url = gitUrlBuf;
|
||||
std::string name = gitNameBuf;
|
||||
std::string include = gitIncludeBuf;
|
||||
startPackageTask("Installing package...", [this, url, name, include]() {
|
||||
PackageTaskResult result;
|
||||
std::string newId;
|
||||
if (packageManager.installGitPackage(url, name, include, newId)) {
|
||||
result.success = true;
|
||||
result.message = "Installed package: " + newId;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Installed packages");
|
||||
if (packageTask.active) {
|
||||
ImGui::TextDisabled("Working...");
|
||||
} else {
|
||||
const auto& registry = packageManager.getRegistry();
|
||||
const auto& installedIds = packageManager.getInstalled();
|
||||
if (installedIds.empty()) {
|
||||
ImGui::TextDisabled("None installed");
|
||||
} else {
|
||||
for (const auto& id : installedIds) {
|
||||
const PackageInfo* pkg = nullptr;
|
||||
for (const auto& p : registry) {
|
||||
if (p.id == id) { pkg = &p; break; }
|
||||
}
|
||||
if (!pkg) continue;
|
||||
|
||||
ImGui::PushID(pkg->id.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("%s", pkg->name.c_str());
|
||||
ImGui::TextDisabled("%s", pkg->description.c_str());
|
||||
if (!pkg->external) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "[bundled]");
|
||||
ImGui::PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::TextDisabled("Path: %s", pkg->localPath.string().c_str());
|
||||
ImGui::TextDisabled("Git: %s", pkg->gitUrl.c_str());
|
||||
|
||||
if (ImGui::Button("Check updates")) {
|
||||
std::string id = pkg->id;
|
||||
startPackageTask("Checking package status...", [this, id]() {
|
||||
PackageTaskResult result;
|
||||
std::string status;
|
||||
if (packageManager.checkGitStatus(id, status)) {
|
||||
result.success = true;
|
||||
result.message = status;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update")) {
|
||||
std::string id = pkg->id;
|
||||
std::string name = pkg->name;
|
||||
startPackageTask("Updating package...", [this, id, name]() {
|
||||
PackageTaskResult result;
|
||||
std::string log;
|
||||
if (packageManager.updateGitPackage(id, log)) {
|
||||
result.success = true;
|
||||
result.message = "Updated " + name + "\n" + log;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Uninstall")) {
|
||||
std::string id = pkg->id;
|
||||
std::string name = pkg->name;
|
||||
startPackageTask("Removing package...", [this, id, name]() {
|
||||
PackageTaskResult result;
|
||||
if (packageManager.remove(id)) {
|
||||
result.success = true;
|
||||
result.message = "Removed " + name;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (selectedTab == 2) {
|
||||
ImGui::TextDisabled("Loaded OBJ Meshes");
|
||||
const auto& meshesObj = g_objLoader.getAllMeshes();
|
||||
if (meshesObj.empty()) {
|
||||
ImGui::TextDisabled("No meshes loaded");
|
||||
ImGui::TextDisabled("Import .obj files from File Browser");
|
||||
} else {
|
||||
for (size_t i = 0; i < meshesObj.size(); i++) {
|
||||
const auto& mesh = meshesObj[i];
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
|
||||
ImGui::TreeNodeEx((void*)(intptr_t)i, flags, "[M] %s", mesh.name.c_str());
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Vertices: %d", mesh.vertexCount);
|
||||
ImGui::Text("Faces: %d", mesh.faceCount);
|
||||
ImGui::Text("Has Normals: %s", mesh.hasNormals ? "Yes" : "No");
|
||||
ImGui::Text("Has UVs: %s", mesh.hasTexCoords ? "Yes" : "No");
|
||||
ImGui::TextDisabled("%s", mesh.path.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Loaded Models (Assimp)");
|
||||
const auto& meshesAssimp = getModelLoader().getAllMeshes();
|
||||
if (meshesAssimp.empty()) {
|
||||
ImGui::TextDisabled("No models loaded");
|
||||
ImGui::TextDisabled("Import FBX/GLTF/other supported models from File Browser");
|
||||
} else {
|
||||
for (size_t i = 0; i < meshesAssimp.size(); i++) {
|
||||
const auto& mesh = meshesAssimp[i];
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
|
||||
ImGui::TreeNodeEx((void*)(intptr_t)(10000 + i), flags, "[A] %s", mesh.name.c_str());
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Vertices: %d", mesh.vertexCount);
|
||||
ImGui::Text("Faces: %d", mesh.faceCount);
|
||||
ImGui::Text("Has Normals: %s", mesh.hasNormals ? "Yes" : "No");
|
||||
ImGui::Text("Has UVs: %s", mesh.hasTexCoords ? "Yes" : "No");
|
||||
ImGui::TextDisabled("%s", mesh.path.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
2153
src/EditorWindows/SceneWindows.cpp
Normal file
2153
src/EditorWindows/SceneWindows.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1757
src/EditorWindows/ViewportWindows.cpp
Normal file
1757
src/EditorWindows/ViewportWindows.cpp
Normal file
File diff suppressed because it is too large
Load Diff
120
src/Engine.cpp
120
src/Engine.cpp
@@ -2,6 +2,8 @@
|
||||
#include "ModelLoader.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
struct MaterialFileData {
|
||||
@@ -394,6 +396,7 @@ void Engine::run() {
|
||||
if (showProjectBrowser) renderProjectBrowserPanel();
|
||||
}
|
||||
|
||||
renderScriptEditorWindows();
|
||||
renderViewport();
|
||||
if (showGameViewport) renderGameViewportWindow();
|
||||
renderDialogs();
|
||||
@@ -926,6 +929,8 @@ void Engine::OpenProjectPath(const std::string& path) {
|
||||
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
|
||||
fileBrowser.currentPath = projectManager.currentProject.projectPath;
|
||||
fileBrowser.needsRefresh = true;
|
||||
scriptEditorWindowsDirty = true;
|
||||
scriptEditorWindows.clear();
|
||||
showLauncher = false;
|
||||
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
|
||||
} else {
|
||||
@@ -970,6 +975,8 @@ void Engine::createNewProject(const char* name, const char* location) {
|
||||
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
|
||||
fileBrowser.currentPath = projectManager.currentProject.projectPath;
|
||||
fileBrowser.needsRefresh = true;
|
||||
scriptEditorWindowsDirty = true;
|
||||
scriptEditorWindows.clear();
|
||||
|
||||
showLauncher = false;
|
||||
firstFrame = true;
|
||||
@@ -1362,6 +1369,119 @@ void Engine::compileScriptFile(const fs::path& scriptPath) {
|
||||
addConsoleMessage("Compiled script -> " + commands.binaryPath.string(), ConsoleMessageType::Success);
|
||||
if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info);
|
||||
if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info);
|
||||
|
||||
// Update any ScriptComponents that point to this source with the freshly built binary path.
|
||||
std::string compiledSource = fs::absolute(scriptPath).lexically_normal().string();
|
||||
for (auto& obj : sceneObjects) {
|
||||
for (auto& sc : obj.scripts) {
|
||||
std::error_code ec;
|
||||
fs::path scAbs = fs::absolute(sc.path, ec);
|
||||
std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string();
|
||||
if (scPathNorm == compiledSource) {
|
||||
sc.lastBinaryPath = commands.binaryPath.string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh scripted editor window registry in case new tabs were added by this build.
|
||||
scriptEditorWindowsDirty = true;
|
||||
refreshScriptEditorWindows();
|
||||
}
|
||||
|
||||
void Engine::refreshScriptEditorWindows() {
|
||||
if (!scriptEditorWindowsDirty) return;
|
||||
scriptEditorWindowsDirty = false;
|
||||
|
||||
if (!projectManager.currentProject.isLoaded) {
|
||||
scriptEditorWindows.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, bool> previousState;
|
||||
for (const auto& entry : scriptEditorWindows) {
|
||||
previousState[entry.binaryPath.lexically_normal().string()] = entry.open;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
std::vector<ScriptEditorWindowEntry> updated;
|
||||
|
||||
auto tryAddEntry = [&](const fs::path& binaryPath) {
|
||||
if (binaryPath.empty() || !fs::exists(binaryPath)) return;
|
||||
std::string key = binaryPath.lexically_normal().string();
|
||||
if (!seen.insert(key).second) return;
|
||||
if (!scriptRuntime.hasEditorWindow(binaryPath)) return;
|
||||
|
||||
ScriptEditorWindowEntry entry;
|
||||
entry.binaryPath = binaryPath;
|
||||
entry.label = binaryPath.stem().string();
|
||||
if (entry.label.empty()) entry.label = "ScriptWindow";
|
||||
auto it = previousState.find(key);
|
||||
entry.open = (it != previousState.end()) ? it->second : false;
|
||||
updated.push_back(std::move(entry));
|
||||
};
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
for (const auto& sc : obj.scripts) {
|
||||
fs::path binaryPath;
|
||||
if (!sc.lastBinaryPath.empty()) {
|
||||
binaryPath = fs::path(sc.lastBinaryPath);
|
||||
}
|
||||
if (binaryPath.empty() || !fs::exists(binaryPath)) {
|
||||
binaryPath = resolveScriptBinary(sc.path);
|
||||
}
|
||||
tryAddEntry(binaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan the configured script output directory for standalone editor tabs.
|
||||
fs::path configPath = projectManager.currentProject.scriptsConfigPath;
|
||||
if (configPath.empty()) {
|
||||
configPath = projectManager.currentProject.projectPath / "Scripts.modu";
|
||||
}
|
||||
ScriptBuildConfig config;
|
||||
std::string error;
|
||||
if (scriptCompiler.loadConfig(configPath, config, error)) {
|
||||
fs::path outDir = config.outDir;
|
||||
if (!outDir.is_absolute()) {
|
||||
outDir = projectManager.currentProject.projectPath / outDir;
|
||||
}
|
||||
std::error_code ec;
|
||||
if (fs::exists(outDir, ec)) {
|
||||
for (auto it = fs::recursive_directory_iterator(outDir, ec);
|
||||
it != fs::recursive_directory_iterator(); ++it) {
|
||||
if (it->is_directory()) continue;
|
||||
auto ext = it->path().extension().string();
|
||||
if (ext == ".so" || ext == ".dll" || ext == ".dylib") {
|
||||
tryAddEntry(it->path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scriptEditorWindows.swap(updated);
|
||||
}
|
||||
|
||||
void Engine::renderScriptEditorWindows() {
|
||||
if (scriptEditorWindows.empty()) return;
|
||||
|
||||
ScriptContext ctx;
|
||||
ctx.engine = this;
|
||||
ctx.object = getSelectedObject();
|
||||
ctx.script = nullptr;
|
||||
|
||||
for (auto& entry : scriptEditorWindows) {
|
||||
if (!entry.open) continue;
|
||||
|
||||
std::string title = entry.label + "###" + entry.binaryPath.string();
|
||||
if (ImGui::Begin(title.c_str(), &entry.open)) {
|
||||
scriptRuntime.callEditorWindow(entry.binaryPath, ctx);
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!entry.open) {
|
||||
scriptRuntime.callExitEditorWindow(entry.binaryPath, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::setupImGui() {
|
||||
|
||||
@@ -78,6 +78,13 @@ private:
|
||||
bool showSaveSceneAsDialog = false;
|
||||
char newSceneName[128] = "";
|
||||
char saveSceneAsName[128] = "";
|
||||
struct ScriptEditorWindowEntry {
|
||||
fs::path binaryPath;
|
||||
std::string label;
|
||||
bool open = false;
|
||||
};
|
||||
std::vector<ScriptEditorWindowEntry> scriptEditorWindows;
|
||||
bool scriptEditorWindowsDirty = true;
|
||||
bool rendererInitialized = false;
|
||||
|
||||
bool showImportOBJDialog = false;
|
||||
@@ -159,6 +166,8 @@ private:
|
||||
void renderGameViewportWindow();
|
||||
void renderDialogs();
|
||||
void renderProjectBrowserPanel();
|
||||
void renderScriptEditorWindows();
|
||||
void refreshScriptEditorWindows();
|
||||
Camera makeCameraFromObject(const SceneObject& obj) const;
|
||||
void compileScriptFile(const fs::path& scriptPath);
|
||||
void updateScripts(float delta);
|
||||
|
||||
5445
src/EnginePanels.cpp
5445
src/EnginePanels.cpp
File diff suppressed because it is too large
Load Diff
@@ -385,6 +385,8 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
|
||||
mod.testEditor = reinterpret_cast<TestEditorFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TestEditor"));
|
||||
mod.update = reinterpret_cast<UpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_Update"));
|
||||
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TickUpdate"));
|
||||
mod.editorRender = reinterpret_cast<EditorRenderFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "RenderEditorWindow"));
|
||||
mod.editorExit = reinterpret_cast<EditorExitFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "ExitRenderEditorWindow"));
|
||||
#else
|
||||
mod.handle = dlopen(binaryPath.string().c_str(), RTLD_NOW);
|
||||
if (!mod.handle) {
|
||||
@@ -398,11 +400,13 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
|
||||
mod.testEditor = reinterpret_cast<TestEditorFn>(dlsym(mod.handle, "Script_TestEditor"));
|
||||
mod.update = reinterpret_cast<UpdateFn>(dlsym(mod.handle, "Script_Update"));
|
||||
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(dlsym(mod.handle, "Script_TickUpdate"));
|
||||
mod.editorRender = reinterpret_cast<EditorRenderFn>(dlsym(mod.handle, "RenderEditorWindow"));
|
||||
mod.editorExit = reinterpret_cast<EditorExitFn>(dlsym(mod.handle, "ExitRenderEditorWindow"));
|
||||
#if !defined(_WIN32)
|
||||
{
|
||||
const char* err = dlerror();
|
||||
if (err && !mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
|
||||
&& !mod.update && !mod.tickUpdate) {
|
||||
&& !mod.update && !mod.tickUpdate && !mod.editorRender && !mod.editorExit) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
@@ -410,7 +414,7 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
|
||||
#endif
|
||||
|
||||
if (!mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
|
||||
&& !mod.update && !mod.tickUpdate) {
|
||||
&& !mod.update && !mod.tickUpdate && !mod.editorRender && !mod.editorExit) {
|
||||
#if defined(_WIN32)
|
||||
FreeLibrary(static_cast<HMODULE>(mod.handle));
|
||||
#else
|
||||
@@ -469,3 +473,22 @@ void ScriptRuntime::unloadAll() {
|
||||
}
|
||||
loaded.clear();
|
||||
}
|
||||
|
||||
bool ScriptRuntime::hasEditorWindow(const fs::path& binaryPath) {
|
||||
Module* mod = getModule(binaryPath);
|
||||
return mod && mod->editorRender;
|
||||
}
|
||||
|
||||
void ScriptRuntime::callEditorWindow(const fs::path& binaryPath, ScriptContext& ctx) {
|
||||
Module* mod = getModule(binaryPath);
|
||||
if (mod && mod->editorRender) {
|
||||
mod->editorRender(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptRuntime::callExitEditorWindow(const fs::path& binaryPath, ScriptContext& ctx) {
|
||||
Module* mod = getModule(binaryPath);
|
||||
if (mod && mod->editorExit) {
|
||||
mod->editorExit(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ public:
|
||||
using UpdateFn = void(*)(ScriptContext&, float);
|
||||
using TickUpdateFn = void(*)(ScriptContext&, float);
|
||||
using InspectorFn = void(*)(ScriptContext&);
|
||||
using EditorRenderFn = void(*)(ScriptContext&);
|
||||
using EditorExitFn = void(*)(ScriptContext&);
|
||||
using IEnumFn = void(*)(ScriptContext&, float);
|
||||
|
||||
InspectorFn getInspector(const fs::path& binaryPath);
|
||||
@@ -85,6 +87,10 @@ public:
|
||||
bool runSpec, bool runTest);
|
||||
void unloadAll();
|
||||
const std::string& getLastError() const { return lastError; }
|
||||
// Editor extension hooks: load RenderEditorWindow/ExitRenderEditorWindow from a script binary.
|
||||
bool hasEditorWindow(const fs::path& binaryPath);
|
||||
void callEditorWindow(const fs::path& binaryPath, ScriptContext& ctx);
|
||||
void callExitEditorWindow(const fs::path& binaryPath, ScriptContext& ctx);
|
||||
|
||||
private:
|
||||
struct Module {
|
||||
@@ -95,6 +101,8 @@ private:
|
||||
TestEditorFn testEditor = nullptr;
|
||||
UpdateFn update = nullptr;
|
||||
TickUpdateFn tickUpdate = nullptr;
|
||||
EditorRenderFn editorRender = nullptr;
|
||||
EditorExitFn editorExit = nullptr;
|
||||
std::unordered_set<int> beginCalledObjects;
|
||||
};
|
||||
Module* getModule(const fs::path& binaryPath);
|
||||
|
||||
Reference in New Issue
Block a user