#include "EditorCore.hpp" #include "App.hpp" #include "AppConfig.hpp" #include "CpuMesh.hpp" #include "EditorAccessories.hpp" #include "EditorAttachmentImpl.hpp" #include "EditorNotification.hpp" #include "EditorUtils.hpp" #include "GameObjectTags.hpp" #include "Level.hpp" #include "Macros.hpp" #include "Mesh.hpp" #include "Player.hpp" #include "ScopeGuard.hpp" #define GLFW_INCLUDE_NONE #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::literals; void EditorInspector::SelectTarget(TargetType type, void* object) { selectedItt = type; selectedItPtr = object; renaming = false; renamingScratchBuffer.clear(); } EditorContentBrowser::EditorContentBrowser(EditorInspector* inspector) : mInspector{ inspector } { } EditorContentBrowser::~EditorContentBrowser() { } void EditorContentBrowser::Show(bool* open) { ImGuiWindowFlags windowFlags; if (mDocked) { // Center window horizontally, align bottom vertically auto& viewportSize = ImGui::GetMainViewport()->Size; ImGui::SetNextWindowPos(ImVec2(viewportSize.x / 2, viewportSize.y), ImGuiCond_Always, ImVec2(0.5f, 1.0f)); ImGui::SetNextWindowSizeRelScreen(0.8f, mBrowserHeight); windowFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; } else { windowFlags = 0; } ImGui::Begin("Content Browser", open, windowFlags); ImGui::Splitter(true, kSplitterThickness, &mSplitterLeft, &mSplitterRight, kLeftPaneMinWidth, kRightPaneMinWidth); ImGui::BeginChild("LeftPane", ImVec2(mSplitterLeft - kPadding, 0.0f)); { if (ImGui::Selectable("Settings", mPane == P_Settings)) { mPane = P_Settings; } if (ImGui::Selectable("Shaders", mPane == P_Shader)) { mPane = P_Shader; } if (ImGui::Selectable("Materials", mPane == P_Material)) { mPane = P_Material; } if (ImGui::Selectable("Ires", mPane == P_Ires)) { mPane = P_Ires; } } ImGui::EndChild(); ImGui::SameLine(0.0f, kPadding + kSplitterThickness + kPadding); ImGui::BeginChild("RightPane"); // Fill remaining space switch (mPane) { case P_Settings: { ImGui::Checkbox("Docked", &mDocked); ImGui::SliderFloat("Height", &mBrowserHeight, 0.1f, 1.0f); } break; case P_Shader: { if (ImGui::Button("Refresh")) { // TODO reload shaders while keeping existing references working } ImGui::SameLine(); if (ImGui::Button("Save all")) { auto& shaders = ShaderManager::instance->GetShaders(); for (auto&& [DISCARD, shader] : shaders) { shader->SaveMetadataToFile(shader->GetDesignatedMetadataPath()); } } auto& shaders = ShaderManager::instance->GetShaders(); for (auto it = shaders.begin(); it != shaders.end(); ++it) { auto shader = it->second.Get(); auto& name = shader->GetName(); bool selected = mInspector->selectedItPtr == shader; if (ImGui::Selectable(name.c_str(), selected)) { mInspector->SelectTarget(EditorInspector::ITT_Shader, shader); } if (ImGui::BeginDragDropSource()) { // Reason: intentionally using pointer as Fpayload ImGui::SetDragDropPayload(BRUSSEL_TAG_Shader, &shader, sizeof(shader)); // NOLINT(bugprone-sizeof-expression) ImGui::Text("Shader '%s'", name.c_str()); ImGui::EndDragDropSource(); } } } break; case P_Material: { if (ImGui::Button("New")) { int n = std::rand(); auto mat = new Material("Unnamed Material " + std::to_string(n)); auto guard = GuardDeletion(mat); auto [DISCARD, inserted] = MaterialManager::instance->SaveMaterial(mat); if (inserted) { guard.Dismiss(); } else { ImGui::AddNotification(ImGuiToast(ImGuiToastType_Error, "Failed to create material.")); } } ImGui::SameLine(); if (ImGui::Button("Refresh")) { // TODO } ImGui::SameLine(); if (ImGui::Button("Save all")) { auto& mats = MaterialManager::instance->GetMaterials(); for (auto&& [DISCARD, mat] : mats) { mat->SaveToFile(mat->GetDesignatedPath()); } } auto& mats = MaterialManager::instance->GetMaterials(); for (auto it = mats.begin(); it != mats.end(); ++it) { auto mat = it->second.Get(); auto& name = mat->GetName(); bool selected = mInspector->selectedItPtr == mat; if (ImGui::Selectable(name.c_str(), selected)) { mInspector->SelectTarget(EditorInspector::ITT_Material, mat); } if (ImGui::BeginDragDropSource()) { // Reason: intentionally using pointer as payload ImGui::SetDragDropPayload(BRUSSEL_TAG_Material, &mat, sizeof(mat)); // NOLINT(bugprone-sizeof-expression) ImGui::Text("Material '%s'", name.c_str()); ImGui::EndDragDropSource(); } } } break; case P_Ires: { auto itt = mInspector->selectedItt; auto itPtr = mInspector->selectedItPtr; bool isIttIres = itt == EditorInspector::ITT_Ires; if (ImGui::Button("New")) { ImGui::OpenPopup("New Ires"); } if (ImGui::BeginPopup("New Ires")) { for (int i = 0; i < IresObject::KD_COUNT; ++i) { auto kind = static_cast(i); if (ImGui::MenuItem(IresObject::ToString(kind).data())) { auto ires = IresObject::Create(kind); auto [DISCARD, success] = IresManager::instance->Add(ires.get()); if (success) { (void)ires.release(); } } } ImGui::EndPopup(); } ImGui::SameLine(); if (ImGui::Button("Refresh list") || ImGui::IsKeyPressed(ImGuiKey_F5)) { // TODO } ImGui::SameLine(); if (ImGui::Button("Save", !isIttIres)) { auto ires = static_cast(itPtr); IresManager::instance->Save(ires); } ImGui::SameLine(); if (ImGui::Button("Reload", !isIttIres)) { auto ires = static_cast(itPtr); IresManager::instance->Reload(ires); } ImGui::SameLine(); if (ImGui::Button("Rename", !isIttIres) || (isIttIres && ImGui::IsKeyPressed(ImGuiKey_F2, false))) { auto ires = static_cast(itPtr); mInspector->renaming = true; mInspector->renamingScratchBuffer = ires->GetName(); } ImGui::SameLine(); if (ImGui::Button("Delete", !isIttIres) || (isIttIres && ImGui::IsKeyPressed(ImGuiKey_Delete, false))) { ImGui::OpenPopup("Delete Ires"); } bool openedDummy = true; if (ImGui::BeginPopupModal("Delete Ires", &openedDummy, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) { if (ImGui::Button("Confirm")) { auto ires = static_cast(itPtr); IresManager::instance->Delete(ires); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } auto& objects = IresManager::instance->GetObjects(); for (auto it = objects.begin(); it != objects.end(); ++it) { auto ires = it->second.Get(); auto& name = ires->GetName(); bool selected = itPtr == ires; ImGuiSelectableFlags flags = 0; // When renaming, disable all other entries if (mInspector->renaming && !selected) { flags |= ImGuiSelectableFlags_Disabled; } if (mInspector->renaming && selected) { // State: being renamed ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); ImGui::SetKeyboardFocusHere(); if (ImGui::InputText("##Rename", &mInspector->renamingScratchBuffer, ImGuiInputTextFlags_EnterReturnsTrue)) { // Confirm ires->SetName(std::move(mInspector->renamingScratchBuffer)); mInspector->renaming = false; } ImGui::PopStyleVar(2); if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { // Cancel mInspector->renaming = false; } } else { // State: normal if (ImGui::Selectable(name.c_str(), selected, flags)) { mInspector->SelectTarget(EditorInspector::ITT_Ires, ires); } if (ImGui::BeginDragDropSource()) { auto kindName = IresObject::ToString(ires->GetKind()); // Reason: intentionally using pointer as payload ImGui::SetDragDropPayload(kindName.data(), &ires, sizeof(ires)); // NOLINT(bugprone-sizeof-expression) ImGui::Text("%s '%s'", kindName.data(), name.c_str()); ImGui::EndDragDropSource(); } } } } break; } ImGui::EndChild(); ImGui::End(); } namespace ProjectBrussel_UNITY_ID { void PushKeyCodeRecorder(App* app, int* writeKey, bool* writeKeyStatus) { app->PushKeyCaptureCallback([=](int key, int action) { // Allow the user to cancel by pressing Esc if (key == GLFW_KEY_ESCAPE) { return true; } if (action == GLFW_PRESS) { *writeKey = key; *writeKeyStatus = writeKeyStatus; return true; } return false; }); } void ShowShaderName(const Shader* shader) { if (shader) { auto& name = shader->GetName(); bool isAnnoymous = name.empty(); if (isAnnoymous) { ImGui::Text("Shader ", (void*)shader); } else { ImGui::Text("Shader: %s", name.c_str()); } } else { ImGui::TextUnformatted("Shader: "); } } void ShowMaterialName(const Material* material) { if (material) { auto& name = material->GetName(); bool isAnnoymous = name.empty(); if (isAnnoymous) { ImGui::Text("Material: ", (void*)material); } else { ImGui::Text("Material: %s", name.c_str()); } } else { ImGui::TextUnformatted("Material: "); } } } // namespace ProjectBrussel_UNITY_ID EditorInstance::EditorInstance(App* app, GameWorld* world) : mApp{ app } , mWorld{ world } , mEdContentBrowser(&mEdInspector) {} EditorInstance::~EditorInstance() { } void EditorInstance::Show() { if (!mWorld) return; auto& io = ImGui::GetIO(); if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { mEdContentBrowserVisible = !mEdContentBrowserVisible; } ImGui::Begin("World properties"); ShowWorldProperties(); ImGui::End(); ImGui::Begin("World structure"); ShowGameObjectInTree(&mWorld->GetRoot()); ImGui::End(); ImGui::Begin("Inspector"); switch (mEdInspector.selectedItt) { case EditorInspector::ITT_GameObject: { ShowInspector(static_cast(mEdInspector.selectedItPtr)); } break; case EditorInspector::ITT_Shader: { ShowInspector(static_cast(mEdInspector.selectedItPtr)); } break; case EditorInspector::ITT_Material: { ShowInspector(static_cast(mEdInspector.selectedItPtr)); } break; case EditorInspector::ITT_Ires: { auto ires = static_cast(mEdInspector.selectedItPtr); ShowInspector(ires); } break; case EditorInspector::ITT_None: break; } ImGui::End(); if (mEdContentBrowserVisible) { mEdContentBrowser.Show(&mEdContentBrowserVisible); } ShowSpriteViewer(); } void EditorInstance::ShowWorldProperties() { } // TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism void EditorInstance::ShowInspector(Shader* shader) { using namespace Tags; using namespace ProjectBrussel_UNITY_ID; EaShader* attachment; if (auto ea = shader->GetEditorAttachment()) { attachment = static_cast(ea); } else { attachment = new EaShader(); attachment->shader = shader; shader->SetEditorAttachment(attachment); } auto& info = shader->GetInfo(); auto& name = shader->GetName(); bool isAnnoymous = name.empty(); ShowShaderName(shader); if (ImGui::Button("Reload metadata", isAnnoymous)) { shader->LoadMetadataFromFile(shader->GetDesignatedMetadataPath()); } ImGui::SameLine(); if (ImGui::Button("Save metadata", isAnnoymous)) { shader->SaveMetadataToFile(shader->GetDesignatedMetadataPath()); } ImGui::SameLine(); if (ImGui::Button("Gather info")) { shader->GatherInfoShaderIntrospection(); } if (ImGui::CollapsingHeader("Inputs")) { for (auto& input : info.inputs) { input.ShowInfo(); } } if (ImGui::CollapsingHeader("Outputs")) { for (auto& output : info.outputs) { output.ShowInfo(); } } if (ImGui::CollapsingHeader("Uniforms")) { for (auto& uniform : info.uniforms) { uniform->ShowInfo(); } } } void EditorInstance::ShowInspector(Material* material) { using namespace Tags; using namespace ProjectBrussel_UNITY_ID; EaMaterial* attachment; if (auto ea = material->GetEditorAttachment()) { attachment = static_cast(ea); } else { attachment = new EaMaterial(); material->SetEditorAttachment(attachment); } auto& name = material->GetName(); bool isAnnoymous = name.empty(); if (isAnnoymous) { ImGui::Text("", (void*)(&material)); } else { if (attachment->isEditingName) { bool save = false; save |= ImGui::InputText("##", &attachment->editingScratch, ImGuiInputTextFlags_EnterReturnsTrue); ImGui::SameLine(); save |= ImGui::Button("Save"); if (save) { bool success = MaterialManager::instance->RenameMaterial(material, attachment->editingScratch); if (success) { attachment->isEditingName = false; } } ImGui::SameLine(); if (ImGui::Button("Cancel")) { attachment->isEditingName = false; } } else { // NOTE: ReadOnly shouldn't write any data into the buffer ImGui::InputText("##", material->mName.data(), name.size() + 1, ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); if (ImGui::Button("Edit")) { attachment->editingScratch = name; // Copy attachment->isEditingName = true; } } } auto shader = material->GetShader(); ShowShaderName(shader); if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(BRUSSEL_TAG_Shader)) { auto shader = *static_cast(payload->Data); material->SetShader(shader); } ImGui::EndDragDropTarget(); } ImGui::SameLine(); if (ImGui::Button("GoTo", shader == nullptr)) { mEdInspector.SelectTarget(EditorInspector::ITT_Shader, shader); } if (!shader) return; auto& info = shader->GetInfo(); if (ImGui::Button("Reload", isAnnoymous)) { material->LoadFromFile(material->GetDesignatedPath()); } ImGui::SameLine(); if (ImGui::Button("Save", isAnnoymous)) { material->SaveToFile(material->GetDesignatedPath()); } for (auto& field : material->mBoundScalars) { auto& decl = static_cast(*info.uniforms[field.infoUniformIndex]); decl.ShowInfo(); ImGui::Indent(); switch (decl.scalarType) { case GL_FLOAT: ImGui::InputFloat("##", &field.floatValue); break; case GL_INT: ImGui::InputInt("##", &field.intValue); break; // TODO proper uint edit? case GL_UNSIGNED_INT: ImGui::InputInt("##", (int32_t*)(&field.uintValue), 0, std::numeric_limits::max()); break; default: ImGui::TextUnformatted("Unsupported scalar type"); break; } ImGui::Unindent(); } for (auto& field : material->mBoundVectors) { auto& decl = static_cast(*info.uniforms[field.infoUniformIndex]); decl.ShowInfo(); ImGui::Indent(); switch (decl.semantic) { case VES_Color1: case VES_Color2: { ImGui::ColorEdit4("##", field.value); } break; default: { ImGui::InputFloat4("##", field.value); } break; } ImGui::Unindent(); } for (auto& field : material->mBoundMatrices) { auto& decl = static_cast(*info.uniforms[field.infoUniformIndex]); decl.ShowInfo(); // TODO } for (auto& field : material->mBoundTextures) { auto& decl = static_cast(*info.uniforms[field.infoUniformIndex]); decl.ShowInfo(); // TODO } } void EditorInstance::ShowInspector(IresObject* ires) { ires->ShowEditor(*this); } void EditorInstance::ShowInspector(GameObject* object) { using namespace Tags; using namespace ProjectBrussel_UNITY_ID; auto ShowFields = [&]() { auto pos = object->GetPos(); if (ImGui::InputFloat3("Position", &pos.x)) { object->SetPos(pos); } auto quat = object->GetRotation(); if (ImGui::InputFloat4("Rotation", &quat.x)) { object->SetRotation(quat); } }; auto type = object->GetTypeTag(); switch (type) { case Tags::GOT_Player: { ShowFields(); ImGui::Separator(); auto player = static_cast(object); auto& kb = player->keybinds; ImGui::Text("Player #%d", player->GetId()); if (ImGui::Button("Load config")) { bool success = player->LoadFromFile(); if (success) { ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully loaded player config")); } } ImGui::SameLine(); if (ImGui::Button("Save config")) { bool success = player->SaveToFile(); if (success) { ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully saved player config")); } } ImGui::Text("Move left (%s)", ImGui::GetKeyNameGlfw(kb.keyLeft)); ImGui::SameLine(); if (ImGui::Button("Change##Move left")) { PushKeyCodeRecorder(mApp, &kb.keyLeft, &kb.pressedLeft); } ImGui::Text("Move right (%s)", ImGui::GetKeyNameGlfw(kb.keyRight)); ImGui::SameLine(); if (ImGui::Button("Change##Move right")) { PushKeyCodeRecorder(mApp, &kb.keyRight, &kb.pressedRight); } ImGui::Text("Jump (%s)", ImGui::GetKeyNameGlfw(kb.keyJump)); ImGui::SameLine(); if (ImGui::Button("Change##Jump")) { PushKeyCodeRecorder(mApp, &kb.keyJump, &kb.pressedJump); } ImGui::Text("Attack (%s)", ImGui::GetKeyNameGlfw(kb.keyAttack)); ImGui::SameLine(); if (ImGui::Button("Change##Attack")) { PushKeyCodeRecorder(mApp, &kb.keyAttack, &kb.pressedAttack); } } break; case Tags::GOT_LevelWrapper: { ShowFields(); ImGui::Separator(); auto lwo = static_cast(object); // TODO } break; default: { ShowFields(); } break; } } void EditorInstance::ShowGameObjectInTree(GameObject* object) { auto attachment = object->GetEditorAttachment(); if (!attachment) { attachment = EaGameObject::Create(object).release(); object->SetEditorAttachment(attachment); // NOTE: takes ownership } ImGuiTreeNodeFlags flags = 0; flags |= ImGuiTreeNodeFlags_DefaultOpen; flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick; flags |= ImGuiTreeNodeFlags_OpenOnArrow; flags |= ImGuiTreeNodeFlags_SpanAvailWidth; if (mEdInspector.selectedItPtr == object) { flags |= ImGuiTreeNodeFlags_Selected; } if (ImGui::TreeNodeEx(attachment->name.c_str(), flags)) { if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { mEdInspector.SelectTarget(EditorInspector::ITT_GameObject, object); } for (auto& child : object->GetChildren()) { ShowGameObjectInTree(child); } ImGui::TreePop(); } } void EditorInstance::OpenSpriteViewer(Sprite* sprite) { mSpriteView_Instance.Attach(sprite); mSpriteView_Frame = 0; mSpriteView_OpenNextFrame = true; } void EditorInstance::ShowSpriteViewer() { if (mSpriteView_Instance == nullptr) return; if (!mSpriteView_Instance->IsValid()) return; if (mSpriteView_OpenNextFrame) { mSpriteView_OpenNextFrame = false; ImGui::OpenPopup("Sprite Viewer"); } bool windowOpen = true; if (ImGui::BeginPopupModal("Sprite Viewer", &windowOpen)) { auto atlas = mSpriteView_Instance->GetAtlas(); auto atlasSize = atlas->GetInfo().size; auto& frames = mSpriteView_Instance->GetFrames(); int frameCount = mSpriteView_Instance->GetFrames().size(); if (ImGui::Button("<")) { --mSpriteView_Frame; } ImGui::SameLine(); ImGui::Text("%d/%d", mSpriteView_Frame + 1, frameCount); ImGui::SameLine(); if (ImGui::Button(">")) { ++mSpriteView_Frame; } // Scrolling down (negative value) should advance, so invert the sign mSpriteView_Frame += -std::round(ImGui::GetIO().MouseWheel); // Clamp mSpriteView_Frame to range [0, frameCount) if (mSpriteView_Frame < 0) { mSpriteView_Frame = frameCount - 1; } else if (mSpriteView_Frame >= frameCount) { mSpriteView_Frame = 0; } auto& currFrame = frames[mSpriteView_Frame]; auto boundingBox = mSpriteView_Instance->GetBoundingBox(); ImGui::Text("Frame size: (%d, %d)", boundingBox.x, boundingBox.y); ImGui::Text("Frame location: (%f, %f) to (%f, %f)", currFrame.u0, currFrame.v0, currFrame.u1, currFrame.v1); ImGui::Image( (ImTextureID)(uintptr_t)atlas->GetHandle(), Utils::FitImage(atlasSize), ImVec2(currFrame.u0, currFrame.v0), ImVec2(currFrame.u1, currFrame.v1)); ImGui::EndPopup(); } }