Files
Destrum/editor/src/EditorMenus.cpp
T
2026-11-01 23:16:58 +01:00

412 lines
15 KiB
C++

#include <editor/Editor.h>
#include <algorithm>
#include <array>
#include <filesystem>
#include <string>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include <destrum/App.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] std::vector<std::string> FindScenes()
{
std::vector<std::string> scenes;
for (const FSMount& mount : AssetFS::GetInstance().GetMounts()) {
std::error_code error;
for (const auto& entry : std::filesystem::recursive_directory_iterator(mount.root, error)) {
if (entry.is_regular_file(error) && AssetTypes::IsScene(entry.path().string())) {
scenes.push_back(mount.scheme + "://" + entry.path().lexically_relative(mount.root).generic_string());
}
}
}
std::ranges::sort(scenes);
return scenes;
}
// Layouts saved while the scene tree was part of the inspector have no spot for it, split one off the inspector
void DockSceneTreeIntoSavedLayout(ImGuiID dockspaceId)
{
const ImGuiWindowSettings* inspector = ImGui::FindWindowSettingsByID(ImHashStr("Inspector"));
if (ImGui::FindWindowSettingsByID(ImHashStr("Scene Tree")) != nullptr || inspector == nullptr) {
return;
}
const ImGuiID oldNode = inspector->DockId;
const ImGuiDockNode* node = ImGui::DockBuilderGetNode(oldNode);
if (node == nullptr || node->IsSplitNode()) {
return;
}
ImGuiID inspectorNode = 0;
const ImGuiID sceneTreeNode = ImGui::DockBuilderSplitNode(oldNode, ImGuiDir_Up, 0.4f, nullptr, &inspectorNode);
// Windows aren't bound to their nodes yet, the ones tabbed with the inspector would point at the split node
ImGuiContext& context = *ImGui::GetCurrentContext();
for (ImGuiWindowSettings* settings = context.SettingsWindows.begin(); settings != nullptr;
settings = context.SettingsWindows.next_chunk(settings)) {
if (settings->DockId == oldNode) {
settings->DockId = inspectorNode;
}
}
ImGui::DockBuilderDockWindow("Scene Tree", sceneTreeNode);
ImGui::DockBuilderFinish(dockspaceId);
}
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
const bool hasSavedLayout = ImGui::DockBuilderGetNode(dockspaceId) != nullptr;
const ImGuiViewport* mainViewport = ImGui::GetMainViewport();
ImGui::DockSpaceOverViewport(dockspaceId, mainViewport);
if (hasSavedLayout) {
if (!std::exchange(m_DockLayoutChecked, true)) {
DockSceneTreeIntoSavedLayout(dockspaceId);
}
return;
}
m_DockLayoutChecked = true;
// First run without an imgui.ini: scene in the middle, scene tree and inspector right, tools left, assets below
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, mainViewport->WorkSize);
ImGuiID center = dockspaceId;
ImGuiID right = ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.25f, nullptr, &center);
const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, &center);
const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, &center);
const ImGuiID rightTop = ImGui::DockBuilderSplitNode(right, ImGuiDir_Up, 0.4f, nullptr, &right);
ImGui::DockBuilderDockWindow("Viewport", center);
ImGui::DockBuilderDockWindow("Scene Tree", rightTop);
ImGui::DockBuilderDockWindow("Inspector", right);
ImGui::DockBuilderDockWindow("Stats", left);
ImGui::DockBuilderDockWindow("Physics", left);
ImGui::DockBuilderDockWindow("Assets", bottom);
ImGui::DockBuilderFinish(dockspaceId);
}
void Editor::DrawMainMenuBar()
{
if (!ImGui::BeginMainMenuBar()) {
return;
}
DrawFileMenu();
DrawEditMenu();
DrawViewMenu();
DrawDisplayMenu();
DrawPlayControls();
ImGui::EndMainMenuBar();
}
void Editor::DrawFileMenu()
{
if (!ImGui::BeginMenu("File")) {
return;
}
const bool editing = m_PlayMode.IsEditing();
if (ImGui::MenuItem("New Scene", nullptr, false, editing)) {
NewScene();
}
if (ImGui::MenuItem("Open Scene...", nullptr, false, editing)) {
m_OpenSceneDialog = true;
}
if (ImGui::MenuItem("Save Scene", "Ctrl+S", false, editing)) {
SaveScene();
}
if (ImGui::MenuItem("Save Scene As...", nullptr, false, editing)) {
m_SaveAsDialog = true;
}
ImGui::Separator();
Scene* scene = CurrentScene();
GameObject* selected = scene != nullptr ? scene->GetSelectedObject() : nullptr;
if (ImGui::MenuItem("Save Selection as Prefab", nullptr, false, selected != nullptr)) {
const std::string path = "game://prefabs/" + selected->GetName() + std::string{AssetTypes::PrefabExtension};
const auto prefab = PrefabSerializer::Capture(*selected);
SetStatus(prefab && AssetFS::GetInstance().WriteFile(path, prefab->dump(4))
? "Saved " + path + ", drag it from Assets into the viewport to place it"
: "Could not save " + path);
}
ImGui::Separator();
if (ImGui::MenuItem("Quit")) {
m_App.requestQuit();
}
ImGui::Separator();
ImGui::TextDisabled("%s", m_ScenePath.empty() ? "Unsaved scene" : m_ScenePath.c_str());
if (!m_Status.empty()) {
ImGui::TextWrapped("%s", m_Status.c_str());
}
ImGui::EndMenu();
}
void Editor::DrawEditMenu()
{
if (!ImGui::BeginMenu("Edit")) {
return;
}
const bool editing = m_PlayMode.IsEditing();
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, editing && m_UndoStack.CanUndo())) {
Undo();
}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, editing && m_UndoStack.CanRedo())) {
Redo();
}
ImGui::Separator();
Scene* scene = CurrentScene();
const bool hasSelection = scene != nullptr && scene->GetSelectedObject() != nullptr;
if (ImGui::MenuItem("Duplicate", "Ctrl+D", false, hasSelection)) {
DuplicateSelection();
}
if (ImGui::MenuItem("Delete", "Del", false, hasSelection)) {
DeleteSelection();
}
if (ImGui::MenuItem("Focus Selection", "F", false, hasSelection)) {
FocusSelection();
}
ImGui::EndMenu();
}
void Editor::DrawViewMenu()
{
if (!ImGui::BeginMenu("View")) {
return;
}
ImGui::MenuItem("Scene Tree", nullptr, &m_Panels.sceneTree);
ImGui::MenuItem("Inspector", nullptr, &m_Panels.inspector);
ImGui::MenuItem("Assets", nullptr, &m_Panels.assets);
ImGui::MenuItem("Stats", nullptr, &m_Panels.stats);
ImGui::MenuItem("Physics", nullptr, &m_Panels.physics);
ImGui::MenuItem("Image Cache", nullptr, &m_Panels.imageCache);
m_App.customWindowsMenu();
ImGui::SeparatorText("Scene View");
GameRenderer& renderer = m_App.getRenderer();
if (bool canvas = m_CanvasView.IsEnabled(); ImGui::MenuItem("Canvas Mode", "2", &canvas)) {
m_CanvasView.SetEnabled(canvas);
}
ImGui::MenuItem("Gizmos", nullptr, &m_View.gizmos);
ImGui::MenuItem("Colliders", nullptr, &m_View.colliders);
if (bool wireframe = renderer.getRenderWireframe(); ImGui::MenuItem("Wireframe", nullptr, &wireframe)) {
renderer.setRenderWireframe(wireframe);
}
if (bool heatmap = renderer.getRenderLightHeatmap(); ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &heatmap)) {
renderer.setRenderLightHeatmap(heatmap);
}
if (bool selectionView = renderer.getRenderSelectionView(); ImGui::MenuItem("Selection Buffer", nullptr, &selectionView)) {
renderer.setRenderSelectionView(selectionView);
}
if (Scene* scene = CurrentScene(); scene != nullptr && ImGui::BeginMenu("Play Camera")) {
const auto cameras = scene->GetCameras();
if (cameras.empty()) {
ImGui::TextDisabled("No cameras in the scene");
}
for (CameraComponent* camera : cameras) {
const GameObject* owner = camera->GetGameObject();
ImGui::PushID(owner);
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, camera->IsCurrent())) {
scene->SetCurrentCamera(camera);
}
ImGui::PopID();
}
ImGui::EndMenu();
}
ImGui::Separator();
const bool cooking = m_ShaderHotReload.IsCooking();
if (ImGui::MenuItem(cooking ? "Cooking Shaders..." : "Reload Shaders", nullptr, false, m_ShaderHotReload.IsConfigured() && !cooking)) {
m_ShaderHotReload.RequestCook();
}
if (ImGui::MenuItem("Free Unused Resources")) {
m_App.collectUnusedResources();
}
ImGui::EndMenu();
}
void Editor::DrawDisplayMenu()
{
if (!ImGui::BeginMenu("Display")) {
return;
}
Window& window = m_App.getWindow();
const WindowMode mode = window.GetMode();
const bool fullscreen = window.IsFullscreen();
const bool maximized = window.IsMaximized();
if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) {
window.ToggleFullscreen();
}
if (ImGui::BeginMenu("Mode")) {
if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) {
window.SetMode(WindowMode::Windowed);
}
if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) {
window.SetMode(WindowMode::BorderlessFullscreen);
}
if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) {
window.SetMode(WindowMode::Fullscreen);
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) {
window.ToggleMaximized();
}
if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) {
window.Center();
}
if (ImGui::BeginMenu("Size", !fullscreen)) {
constexpr std::array sizes{glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}};
for (const glm::ivec2& size : sizes) {
const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y);
if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) {
if (maximized) {
window.Restore();
}
window.SetSize(size);
window.Center();
}
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
void Editor::DrawPlayControls()
{
const bool playing = m_PlayMode.IsPlaying();
const bool paused = m_PlayMode.IsPaused();
const char* state = !playing ? "EDIT" : paused ? "PAUSED" : "PLAYING";
const char* action = playing ? "Stop (F5)" : "Play (F5)";
const char* pauseAction = paused ? "Resume (F7)" : "Pause (F7)";
const ImGuiStyle& style = ImGui::GetStyle();
float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x + style.ItemSpacing.x * 2.0f +
style.FramePadding.x * 2.0f;
if (playing) {
width += ImGui::CalcTextSize(pauseAction).x + style.ItemSpacing.x + style.FramePadding.x * 2.0f;
}
// Centered like Godot's run buttons, unless the menus already reach past the middle
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), (ImGui::GetWindowWidth() - width) * 0.5f));
const ImVec4 stateColor = !playing ? ImVec4{0.7f, 0.7f, 0.7f, 1.0f}
: paused ? ImVec4{0.95f, 0.75f, 0.3f, 1.0f}
: ImVec4{0.4f, 0.9f, 0.4f, 1.0f};
ImGui::TextColored(stateColor, "%s", state);
if (playing && ImGui::MenuItem(pauseAction)) {
m_PlayMode.SetPaused(!paused);
}
if (ImGui::MenuItem(action)) {
m_PlayMode.RequestToggle();
}
}
void Editor::DrawSceneDialogs()
{
if (std::exchange(m_OpenSceneDialog, false)) {
ImGui::OpenPopup("Open Scene");
}
if (std::exchange(m_SaveAsDialog, false)) {
m_SaveAsPath = m_ScenePath.empty() ? "game://scenes/untitled.scene" : m_ScenePath;
ImGui::OpenPopup("Save Scene As");
}
if (ImGui::BeginPopupModal("Open Scene", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
const std::vector<std::string> scenes = FindScenes();
if (scenes.empty()) {
ImGui::TextDisabled("No .scene files in the mounted asset folders");
}
for (const std::string& path : scenes) {
if (ImGui::Selectable(path.c_str(), path == m_ScenePath)) {
OpenScene(path);
ImGui::CloseCurrentPopup();
}
}
ImGui::Separator();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Save Scene As", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SetNextItemWidth(420.0f);
const bool submitted = ImGuiUtils::InputString("##Path", m_SaveAsPath, ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::TextDisabled("A virtual path like game://scenes/level.scene");
if (ImGui::Button("Save") || submitted) {
SaveSceneAs(m_SaveAsPath);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void Editor::DrawStatsPanel()
{
if (ImGui::Begin("Stats", &m_Panels.stats)) {
ImGui::Text("FPS: %.1f", m_App.getAverageFps());
const auto& stats = m_App.getRenderer().getDrawStats();
ImGui::SeparatorText("Meshes");
ImGui::Text("Submitted meshes: %u", stats.meshDraws);
ImGui::Text("Camera: %u indirect draws, %u instances", stats.mainViewCommands, stats.mainViewInstances);
ImGui::Text("All views: %u indirect draws, %u instances", stats.indirectCommands, stats.instances);
ImGui::SeparatorText("Memory");
ImGui::Text("Pending GPU deletions: %zu", m_App.getGfxDevice().getPendingDeletionCount());
ImGui::Text("Undo steps: %zu", m_UndoStack.UndoCount());
}
ImGui::End();
}
void Editor::DrawPhysicsPanel()
{
if (!ImGui::Begin("Physics", &m_Panels.physics)) {
ImGui::End();
return;
}
App::SimulationControls& simulation = m_App.getSimulation();
ImGui::Text("Fixed dt: %.3f ms", simulation.fixedDeltaTime * 1000.0f);
ImGui::Text("Accumulator: %.3f ms", simulation.accumulator * 1000.0f);
ImGui::Text("Steps last frame: %d", simulation.stepsLastFrame);
ImGui::Separator();
ImGui::Checkbox("Paused", &simulation.paused);
ImGui::SameLine();
ImGui::BeginDisabled(!simulation.paused);
if (ImGui::Button("Step")) {
simulation.stepOnce = true;
}
ImGui::EndDisabled();
ImGui::SliderFloat("Time Scale", &simulation.timeScale, 0.0f, 2.0f, "%.2fx");
if (ImGui::Button("Reset Time Scale")) {
simulation.timeScale = 1.0f;
}
ImGui::End();
}