#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { [[nodiscard]] std::vector FindScenes() { std::vector 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, ¢er); const ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, ¢er); const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.28f, nullptr, ¢er); 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 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(); }