#include "EditorCore.hpp" #include "App.hpp" #include "AppConfig.hpp" #include "EditorAccessories.hpp" #include "EditorAttachmentImpl.hpp" #include "EditorCommandPalette.hpp" #include "EditorNotification.hpp" #include "EditorUtils.hpp" #include "GameObject.hpp" #include "Level.hpp" #include "Macros.hpp" #include "Mesh.hpp" #include "Player.hpp" #include "SceneThings.hpp" #include "ScopeGuard.hpp" #include "VertexIndex.hpp" #include "YCombinator.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("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_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_AutoSelectAll | 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; }); } struct GobjTreeNodeShowInfo { EditorInstance* in_editor; GameObject* out_openPopup = nullptr; }; void GobjTreeNode(GobjTreeNodeShowInfo& showInfo, GameObject* object) { auto& inspector = showInfo.in_editor->GetInspector(); auto attachment = object->GetEditorAttachment(); if (!attachment) { attachment = EaGameObject::Create(object).release(); object->SetEditorAttachment(attachment); // NOTE: takes ownership } ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_NoTreePushOnOpen; if (inspector.selectedItPtr == object) { flags |= ImGuiTreeNodeFlags_Selected; } ImGui::PushID(reinterpret_cast(object)); // BEGIN tree node bool opened = ImGui::TreeNodeEx(attachment->name.c_str(), flags); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { inspector.SelectTarget(EditorInspector::ITT_GameObject, object); } if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) { showInfo.out_openPopup = object; } if (opened) { ImGui::Indent(); for (auto& child : object->GetChildren()) { GobjTreeNode(showInfo, child); } ImGui::Unindent(); } // END tree node ImGui::PopID(); }; #define GAMEOBJECT_CONSTRUCTOR(ClassName) [](GameWorld* world) -> GameObject* { return new ClassName(world); } struct CreatableGameObject { GameObject* (*factory)(GameWorld*); const char* name; GameObject::Kind kind; } creatableGameObjects[] = { { .factory = GAMEOBJECT_CONSTRUCTOR(GameObject), .name = "GameObject", .kind = GameObject::KD_Generic, }, { .factory = GAMEOBJECT_CONSTRUCTOR(SimpleGeometryObject), .name = "Simple Geometry", .kind = GameObject::KD_SimpleGeometry, }, { .factory = GAMEOBJECT_CONSTRUCTOR(BuildingObject), .name = "Building", .kind = GameObject::KD_Building, }, { .factory = GAMEOBJECT_CONSTRUCTOR(LevelWrapperObject), .name = "Level Wrapper", .kind = GameObject::KD_LevelWrapper, }, }; #undef GAMEOBJECT_CONSTRUCTOR } // namespace ProjectBrussel_UNITY_ID EditorInstance::EditorInstance(App* app, GameWorld* world) : mApp{ app } , mWorld{ world } , mEdContentBrowser(&mEdInspector) {} EditorInstance::~EditorInstance() { } void EditorInstance::Show() { using namespace ProjectBrussel_UNITY_ID; using namespace Tags; if (!mWorld) { return; } auto& io = ImGui::GetIO(); ImGui::BeginMainMenuBar(); if (ImGui::BeginMenu("View")) { ImGui::MenuItem("ImGui Demo", nullptr, &mWindowVisible_ImGuiDemo); ImGui::MenuItem("Command Palette", "Ctrl+Shift+P", &mWindowVisible_CommandPalette); ImGui::MenuItem("Inspector", nullptr, &mWindowVisible_Inspector); ImGui::MenuItem("Content Browser", "Ctrl+Space", &mWindowVisible_ContentBrowser); ImGui::MenuItem("World Structure", nullptr, &mWindowVisible_WorldStructure); ImGui::MenuItem("World Properties", nullptr, &mWindowVisible_WorldProperties); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); if (mWindowVisible_ImGuiDemo) { ImGui::ShowDemoWindow(&mWindowVisible_ImGuiDemo); } if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(GLFW_KEY_P, false)) { mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette; } if (mWindowVisible_CommandPalette) { mEdCommandPalette.Show(&mWindowVisible_CommandPalette); } if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { mWindowVisible_ContentBrowser = !mWindowVisible_ContentBrowser; } if (mWindowVisible_ContentBrowser) { mEdContentBrowser.Show(&mWindowVisible_ContentBrowser); } if (mWindowVisible_Inspector) { ImGui::Begin("Inspector"); switch (mEdInspector.selectedItt) { case EditorInspector::ITT_GameObject: { 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 (mWindowVisible_WorldProperties) { ImGui::Begin("World properties"); ShowWorldProperties(); ImGui::End(); } if (mWindowVisible_WorldStructure) { ImGui::Begin("World structure"); { GobjTreeNodeShowInfo showInfo{ .in_editor = this, }; GobjTreeNode(showInfo, &mWorld->GetRoot()); if (showInfo.out_openPopup) { mPopupCurrent_GameObject = showInfo.out_openPopup; ImGui::OpenPopup("GameObject Popup"); ImGui::SetNextWindowPos(ImGui::GetMousePos()); } if (ImGui::BeginPopup("GameObject Popup")) { // Target no longer selected during popup open if (!mPopupCurrent_GameObject) { ImGui::CloseCurrentPopup(); } if (ImGui::BeginMenu("Add child")) { for (size_t i = 0; i < std::size(creatableGameObjects); ++i) { auto& info = creatableGameObjects[i]; if (ImGui::MenuItem(info.name)) { auto object = info.factory(mWorld); mPopupCurrent_GameObject->AddChild(object); } } ImGui::EndMenu(); } ImGui::Separator(); if (ImGui::MenuItem("Remove")) { // TODO } ImGui::EndPopup(); } } ImGui::End(); } ShowSpriteViewer(); ImGui::ShowNotifications(); } void EditorInstance::ShowWorldProperties() { if (mApp->IsGameRunning()) { if (ImGui::Button("Pause")) { mApp->SetGameRunning(false); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("The game is currently running. Click to pause."); } } else { if (ImGui::Button("Play")) { mApp->SetGameRunning(true); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("The game is currently paused. Click to run."); } } } // TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism 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->GetKind(); switch (type) { case GameObject::KD_Player: { ShowFields(); ImGui::Separator(); auto player = static_cast(object); auto ea = static_cast(player->GetEditorAttachment()); auto& kb = player->keybinds; ImGui::Text("Player #%d", player->GetId()); ImGui::TextUnformatted("Spritesheet: "); ImGui::SameLine(); IresObject::ShowReferenceSafe(*this, ea->confSprite.Get()); if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Spritesheet).data())) { auto spritesheet = *static_cast(payload->Data); ea->confSprite.Attach(spritesheet); player->sprite.SetDefinition(spritesheet->GetInstance()); } ImGui::EndDragDropTarget(); } ImGui::TextUnformatted("Material: "); ImGui::SameLine(); IresObject::ShowReferenceSafe(*this, ea->confMaterial.Get()); if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Material).data())) { auto material = *static_cast(payload->Data); ea->confMaterial.Attach(material); player->SetMaterial(material->GetInstance()); } ImGui::EndDragDropTarget(); } 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 GameObject::KD_SimpleGeometry: { ShowFields(); ImGui::Separator(); auto sg = static_cast(object); // TODO } break; case GameObject::KD_Building: { ShowFields(); ImGui::Separator(); auto b = static_cast(object); // TODO } break; case GameObject::KD_LevelWrapper: { ShowFields(); ImGui::Separator(); auto lwo = static_cast(object); // TODO } break; default: { ShowFields(); } break; } } void EditorInstance::OpenSpriteViewer(SpriteDefinition* 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(); } }