diff --git a/editor/main.cpp b/editor/main.cpp index 58d939c..bb66b1f 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -4,6 +4,7 @@ #include "panels/asset_browser_panel.hpp" #include "panels/inspector_panel.hpp" #include "panels/scene_hierarchy_panel.hpp" +#include "panels/viewport_panel.hpp" class Editor : public sophia::Application { public: @@ -17,6 +18,13 @@ class Editor : public sophia::Application { panels().emplace(); panels().emplace(); panels().emplace(); + panels().emplace(); + + panels().setDefaultLayout( + {.left = {"Scene Hierarchy"}, + .right = {"Inspector", "Frame Stats", "PanelRegistry"}, + .bottom = {"Asset Browser", "Console"}, + .center = {"Viewport"}}); } void onUpdate(const sophia::FrameInfo& frameInfo) override {} diff --git a/editor/panels/asset_browser_panel.cpp b/editor/panels/asset_browser_panel.cpp index 5371e4f..426a462 100644 --- a/editor/panels/asset_browser_panel.cpp +++ b/editor/panels/asset_browser_panel.cpp @@ -3,8 +3,6 @@ #include void AssetBrowserPanel::onUpdate(const sophia::FrameInfo&) { - ImGui::Begin("Asset Browser"); - ImGui::BeginChild("Folders", ImVec2(150, 0), true); for (int i = 0; i < static_cast(folders.size()); i++) { if (ImGui::Selectable(folders[static_cast(i)].name.c_str(), @@ -22,6 +20,4 @@ void AssetBrowserPanel::onUpdate(const sophia::FrameInfo&) { ImGui::Selectable(file.c_str()); } ImGui::EndChild(); - - ImGui::End(); } diff --git a/editor/panels/asset_browser_panel.hpp b/editor/panels/asset_browser_panel.hpp index fc9577f..23993ab 100644 --- a/editor/panels/asset_browser_panel.hpp +++ b/editor/panels/asset_browser_panel.hpp @@ -7,7 +7,7 @@ class AssetBrowserPanel : public sophia::Panel { public: - AssetBrowserPanel() : Panel{"Asset Browser"} {} + AssetBrowserPanel() : Panel{{.name = "Asset Browser"}} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; diff --git a/editor/panels/inspector_panel.cpp b/editor/panels/inspector_panel.cpp index 0910b10..3fe5f83 100644 --- a/editor/panels/inspector_panel.cpp +++ b/editor/panels/inspector_panel.cpp @@ -5,8 +5,6 @@ #include void InspectorPanel::onUpdate(const sophia::FrameInfo&) { - ImGui::Begin("Inspector"); - char nameBuf[128]; std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str()); if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { @@ -31,6 +29,4 @@ void InspectorPanel::onUpdate(const sophia::FrameInfo&) { if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("%s", scriptName.c_str()); } - - ImGui::End(); } diff --git a/editor/panels/inspector_panel.hpp b/editor/panels/inspector_panel.hpp index a316315..3a080c1 100644 --- a/editor/panels/inspector_panel.hpp +++ b/editor/panels/inspector_panel.hpp @@ -7,7 +7,7 @@ class InspectorPanel : public sophia::Panel { public: - InspectorPanel() : Panel{"Inspector"} {} + InspectorPanel() : Panel{{.name = "Inspector"}} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; diff --git a/editor/panels/scene_hierarchy_panel.cpp b/editor/panels/scene_hierarchy_panel.cpp index 14d663c..08cf3c4 100644 --- a/editor/panels/scene_hierarchy_panel.cpp +++ b/editor/panels/scene_hierarchy_panel.cpp @@ -26,9 +26,7 @@ void SceneHierarchyPanel::drawNode(const Node& node) { } void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) { - ImGui::Begin("Scene Hierarchy"); for (const Node& node : nodes) { drawNode(node); } - ImGui::End(); } diff --git a/editor/panels/scene_hierarchy_panel.hpp b/editor/panels/scene_hierarchy_panel.hpp index 78d70c1..b4be4b4 100644 --- a/editor/panels/scene_hierarchy_panel.hpp +++ b/editor/panels/scene_hierarchy_panel.hpp @@ -7,7 +7,7 @@ class SceneHierarchyPanel : public sophia::Panel { public: - SceneHierarchyPanel() : Panel{"Scene Hierarchy"} {} + SceneHierarchyPanel() : Panel{{.name = "Scene Hierarchy"}} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; diff --git a/editor/panels/viewport_panel.cpp b/editor/panels/viewport_panel.cpp new file mode 100644 index 0000000..ef14bed --- /dev/null +++ b/editor/panels/viewport_panel.cpp @@ -0,0 +1,45 @@ +#include "viewport_panel.hpp" + +#include + +void ViewportPanel::onUpdate(const sophia::FrameInfo&) { + const struct { + const char* label; + GizmoMode mode; + } modes[] = {{"Move", GizmoMode::Move}, + {"Rotate", GizmoMode::Rotate}, + {"Scale", GizmoMode::Scale}}; + + for (const auto& entry : modes) { + if (ImGui::RadioButton(entry.label, gizmoMode == entry.mode)) { + gizmoMode = entry.mode; + } + ImGui::SameLine(); + } + ImGui::Checkbox("Grid", &showGrid); + + ImGui::Separator(); + + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const ImVec2 size = ImGui::GetContentRegionAvail(); + const ImVec2 end(origin.x + size.x, origin.y + size.y); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255)); + + if (showGrid) { + constexpr float GRID_STEP = 40.0f; + const ImU32 gridColor = IM_COL32(255, 255, 255, 18); + for (float x = GRID_STEP; x < size.x; x += GRID_STEP) { + drawList->AddLine(ImVec2(origin.x + x, origin.y), + ImVec2(origin.x + x, end.y), gridColor); + } + for (float y = GRID_STEP; y < size.y; y += GRID_STEP) { + drawList->AddLine(ImVec2(origin.x, origin.y + y), + ImVec2(end.x, origin.y + y), gridColor); + } + } + + ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f)); + ImGui::TextDisabled("%.0f x %.0f", size.x, size.y); +} diff --git a/editor/panels/viewport_panel.hpp b/editor/panels/viewport_panel.hpp new file mode 100644 index 0000000..b68205b --- /dev/null +++ b/editor/panels/viewport_panel.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class ViewportPanel : public sophia::Panel { + public: + ViewportPanel() : Panel{{.name = "Viewport"}} {} + + void onUpdate(const sophia::FrameInfo& frameInfo) override; + + private: + enum class GizmoMode { Move, Rotate, Scale }; + + GizmoMode gizmoMode = GizmoMode::Move; + bool showGrid = true; +}; diff --git a/engine/include/sophia/ui/dock_layout.hpp b/engine/include/sophia/ui/dock_layout.hpp new file mode 100644 index 0000000..fce3fc7 --- /dev/null +++ b/engine/include/sophia/ui/dock_layout.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace sophia { + +struct DockLayout { + float leftWidth = 0.2f; + float rightWidth = 0.25f; + float bottomHeight = 0.25f; + + std::vector left; + std::vector right; + std::vector bottom; + std::vector center; + + bool empty() const { + return left.empty() && right.empty() && bottom.empty() && center.empty(); + } +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel.hpp b/engine/include/sophia/ui/panel.hpp index ef81a54..c46b8b5 100644 --- a/engine/include/sophia/ui/panel.hpp +++ b/engine/include/sophia/ui/panel.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -8,28 +10,46 @@ namespace sophia { class Panel { public: + struct Config { + std::string name; + + // Locked panels can't be moved, resized, docked, collapsed or closed. + bool locked = false; + bool closable = true; + + // A zero component lets ImGui size the window. Applied on first use, or + // every frame when locked. + glm::vec2 size{0.0f, 0.0f}; + std::optional position; + }; + Panel(const Panel&) = delete; Panel& operator=(const Panel&) = delete; Panel(Panel&&) noexcept = default; Panel& operator=(Panel&&) noexcept = default; - explicit Panel(std::string name = {}) : name{std::move(name)} {} + explicit Panel(Config config) : config{std::move(config)} {} virtual ~Panel() = default; - virtual void onUpdate(const FrameInfo& frameInfo) = 0; + // Owns the ImGui window: Begin, onUpdate, End. + void draw(const FrameInfo& frameInfo); - const std::string& getName() const { return name; } + const std::string& getName() const { return config.name; } + bool isLocked() const { return config.locked; } bool isVisible() const { return this->visible; } void setVisible(bool visible) { this->visible = visible; } void toggleVisible() { this->visible = !this->visible; } protected: + // Draw the panel's contents; the window is already open. + virtual void onUpdate(const FrameInfo& frameInfo) = 0; + bool visible = true; private: - std::string name; + Config config; }; } // namespace sophia diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp index 9c425ed..09b3b87 100644 --- a/engine/include/sophia/ui/panel_registry.hpp +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -94,6 +95,11 @@ class PanelRegistry { void update(const FrameInfo& frameInfo); + void setDefaultLayout(DockLayout layout) { + defaultLayout = std::move(layout); + } + const DockLayout& getDefaultLayout() const { return defaultLayout; } + private: struct Slot { std::unique_ptr panel; @@ -102,6 +108,7 @@ class PanelRegistry { std::vector slots; std::vector freeList; + DockLayout defaultLayout; }; } // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 1123bcc..b838e7b 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -57,7 +57,16 @@ class Application::Impl { ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), renderContext.getImageCount()}; + this->application.consoleCommandRegistry.registerCommand( + "reset_layout", "Restore the default panel layout", + [&imguiRenderer](const std::vector&) { + imguiRenderer.resetLayout(); + return std::string{"Layout reset"}; + }); + this->application.onCreate(); + imguiRenderer.setDefaultLayout( + this->application.panelRegistry.getDefaultLayout()); auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = startTime; diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp index f0e2f04..fd32ded 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/imgui_renderer.cpp @@ -3,9 +3,21 @@ #include #include #include +#include // DockBuilder API namespace sophia { +namespace { +constexpr ImGuiDockNodeFlags DOCKSPACE_FLAGS = + ImGuiDockNodeFlags_PassthruCentralNode; + +void dockAll(const std::vector& windows, ImGuiID nodeId) { + for (const std::string& name : windows) { + ImGui::DockBuilderDockWindow(name.c_str(), nodeId); + } +} +} // namespace + ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, vk::Format colorAttachmentFormat, u32 minImageCount) { @@ -65,8 +77,59 @@ void ImGuiRenderer::beginFrame() { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), - ImGuiDockNodeFlags_PassthruCentralNode); + ImGuiID dockspaceId = ImGui::DockSpaceOverViewport( + 0, ImGui::GetMainViewport(), DOCKSPACE_FLAGS); + applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS); +} + +void ImGuiRenderer::setDefaultLayout(DockLayout layout) { + defaultLayout = std::move(layout); + seedIfUnsaved = true; +} + +void ImGuiRenderer::resetLayout() { forceReset = true; } + +void ImGuiRenderer::applyLayoutIfNeeded(ImGuiID dockspaceId, + ImGuiDockNodeFlags flags) { + if (defaultLayout.empty()) { + return; + } + + bool apply = forceReset; + if (seedIfUnsaved) { + // A layout restored from imgui.ini shows up as a split root node. + ImGuiDockNode* root = ImGui::DockBuilderGetNode(dockspaceId); + apply = apply || root == nullptr || !root->IsSplitNode(); + seedIfUnsaved = false; + } + if (!apply) { + return; + } + forceReset = false; + + const ImVec2 size = ImGui::GetMainViewport()->Size; + + ImGui::DockBuilderRemoveNode(dockspaceId); + ImGui::DockBuilderAddNode(dockspaceId, flags | ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspaceId, size); + + ImGuiID center = dockspaceId; + const ImGuiID left = ImGui::DockBuilderSplitNode( + center, ImGuiDir_Left, defaultLayout.leftWidth, nullptr, ¢er); + // The right split is taken from what's left after the left split. + const float rightRatio = + defaultLayout.rightWidth / (1.0f - defaultLayout.leftWidth); + const ImGuiID right = ImGui::DockBuilderSplitNode( + center, ImGuiDir_Right, rightRatio, nullptr, ¢er); + const ImGuiID bottom = ImGui::DockBuilderSplitNode( + center, ImGuiDir_Down, defaultLayout.bottomHeight, nullptr, ¢er); + + dockAll(defaultLayout.left, left); + dockAll(defaultLayout.right, right); + dockAll(defaultLayout.bottom, bottom); + dockAll(defaultLayout.center, center); + + ImGui::DockBuilderFinish(dockspaceId); } void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) { diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp index 0016730..a852a7d 100644 --- a/engine/src/imgui_renderer.hpp +++ b/engine/src/imgui_renderer.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include +#include #include "device.hpp" #include "window.hpp" @@ -18,6 +21,19 @@ class ImGuiRenderer { void beginFrame(); void endFrame(vk::CommandBuffer commandBuffer); + + // Seeds the layout on the next frame, but only if no saved layout exists. + void setDefaultLayout(DockLayout layout); + + // Discards the current layout and re-applies the default on the next frame. + void resetLayout(); + + private: + void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags); + + DockLayout defaultLayout; + bool seedIfUnsaved = false; + bool forceReset = false; }; } // namespace sophia diff --git a/engine/src/ui/panel.cpp b/engine/src/ui/panel.cpp new file mode 100644 index 0000000..56242cd --- /dev/null +++ b/engine/src/ui/panel.cpp @@ -0,0 +1,35 @@ +#include + +#include + +namespace sophia { + +void Panel::draw(const FrameInfo& frameInfo) { + ImGuiWindowFlags flags = ImGuiWindowFlags_None; + if (config.locked) { + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse; + } + + const ImGuiCond cond = + config.locked ? ImGuiCond_Always : ImGuiCond_FirstUseEver; + if (config.size.x > 0.0f || config.size.y > 0.0f) { + ImGui::SetNextWindowSize(ImVec2(config.size.x, config.size.y), cond); + } + if (config.position) { + ImGui::SetNextWindowPos(ImVec2(config.position->x, config.position->y), + cond); + } + + bool open = true; + const bool showCloseButton = config.closable && !config.locked; + ImGui::Begin(config.name.c_str(), showCloseButton ? &open : nullptr, flags); + onUpdate(frameInfo); + ImGui::End(); + + if (!open) { + visible = false; + } +} + +} // namespace sophia diff --git a/engine/src/ui/panel_registry.cpp b/engine/src/ui/panel_registry.cpp index 92b58d6..5b34ea9 100644 --- a/engine/src/ui/panel_registry.cpp +++ b/engine/src/ui/panel_registry.cpp @@ -80,7 +80,7 @@ u32 PanelRegistry::visibleCount() const { void PanelRegistry::update(const FrameInfo& frameInfo) { for (auto& slot : slots) { if (slot.panel && slot.panel->isVisible()) { - slot.panel->onUpdate(frameInfo); + slot.panel->draw(frameInfo); } } } diff --git a/engine/src/ui/panels/console_panel.cpp b/engine/src/ui/panels/console_panel.cpp index 4e9f192..dc9568c 100644 --- a/engine/src/ui/panels/console_panel.cpp +++ b/engine/src/ui/panels/console_panel.cpp @@ -111,8 +111,6 @@ int ConsolePanel::textEditCallback(ImGuiInputTextCallbackData* data) { } void ConsolePanel::onUpdate(const FrameInfo&) { - ImGui::Begin("Console"); - if (ImGui::SmallButton("Clear")) { clear(); } @@ -161,8 +159,6 @@ void ConsolePanel::onUpdate(const FrameInfo&) { if (reclaimFocus) { ImGui::SetKeyboardFocusHere(-1); } - - ImGui::End(); } } // namespace sophia diff --git a/engine/src/ui/panels/console_panel.hpp b/engine/src/ui/panels/console_panel.hpp index c35ef5f..a183d48 100644 --- a/engine/src/ui/panels/console_panel.hpp +++ b/engine/src/ui/panels/console_panel.hpp @@ -13,7 +13,7 @@ namespace sophia { class ConsolePanel : public Panel { public: explicit ConsolePanel(ConsoleCommandRegistry& consoleCommandRegistry) - : Panel{"Console"}, consoleCommandRegistry{consoleCommandRegistry} { + : Panel{{.name = "Console"}}, consoleCommandRegistry{consoleCommandRegistry} { log("Console ready. Type 'help' for a list of commands."); } diff --git a/engine/src/ui/panels/ecs_inspector_panel.cpp b/engine/src/ui/panels/ecs_inspector_panel.cpp index 49e504e..eb9384f 100644 --- a/engine/src/ui/panels/ecs_inspector_panel.cpp +++ b/engine/src/ui/panels/ecs_inspector_panel.cpp @@ -7,8 +7,6 @@ namespace sophia { void EcsInspectorPanel::onUpdate(const FrameInfo&) { - ImGui::Begin("ECS Inspector"); - entityScratch.clear(); for (entt::entity entity : registry.storage()) { entityScratch.push_back(entity); @@ -50,8 +48,6 @@ void EcsInspectorPanel::onUpdate(const FrameInfo&) { } else { ImGui::TextDisabled("No entity selected"); } - - ImGui::End(); } } // namespace sophia diff --git a/engine/src/ui/panels/ecs_inspector_panel.hpp b/engine/src/ui/panels/ecs_inspector_panel.hpp index 1666ff5..2a99231 100644 --- a/engine/src/ui/panels/ecs_inspector_panel.hpp +++ b/engine/src/ui/panels/ecs_inspector_panel.hpp @@ -10,7 +10,7 @@ namespace sophia { class EcsInspectorPanel : public Panel { public: explicit EcsInspectorPanel(entt::registry& registry) - : Panel{"ECS Inspector"}, registry{registry} {} + : Panel{{.name = "ECS Inspector"}}, registry{registry} {} void onUpdate(const FrameInfo& frameInfo) override; diff --git a/engine/src/ui/panels/frame_stats_panel.hpp b/engine/src/ui/panels/frame_stats_panel.hpp index 0d7917f..7923779 100644 --- a/engine/src/ui/panels/frame_stats_panel.hpp +++ b/engine/src/ui/panels/frame_stats_panel.hpp @@ -9,7 +9,7 @@ namespace sophia { class FrameStatsPanel : public Panel { public: - FrameStatsPanel() : Panel{"Frame Stats"} {} + FrameStatsPanel() : Panel{{.name = "Frame Stats"}} {} void onUpdate(const FrameInfo& frameInfo) override { frameCount++; @@ -25,10 +25,8 @@ class FrameStatsPanel : public Panel { nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL; } - ImGui::Begin("Frame Stats"); ImGui::Text("FPS: %.1f", displayedFps); ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs); - ImGui::End(); } private: diff --git a/engine/src/ui/panels/panel_registry_panel.cpp b/engine/src/ui/panels/panel_registry_panel.cpp index c481b5a..6746408 100644 --- a/engine/src/ui/panels/panel_registry_panel.cpp +++ b/engine/src/ui/panels/panel_registry_panel.cpp @@ -5,7 +5,6 @@ namespace sophia { void PanelRegistryPanel::onUpdate(const FrameInfo&) { - ImGui::Begin("PanelRegistry"); ImGui::Text("%u panels (%u visible)", registry.size(), registry.visibleCount()); ImGui::Separator(); @@ -21,8 +20,6 @@ void PanelRegistryPanel::onUpdate(const FrameInfo&) { panel->setVisible(visible); } } - - ImGui::End(); } } // namespace sophia diff --git a/engine/src/ui/panels/panel_registry_panel.hpp b/engine/src/ui/panels/panel_registry_panel.hpp index 29e16e2..cebefae 100644 --- a/engine/src/ui/panels/panel_registry_panel.hpp +++ b/engine/src/ui/panels/panel_registry_panel.hpp @@ -9,7 +9,7 @@ namespace sophia { class PanelRegistryPanel : public Panel { public: explicit PanelRegistryPanel(PanelRegistry& registry) - : Panel{"PanelRegistry"}, registry{registry} {} + : Panel{{.name = "PanelRegistry"}}, registry{registry} {} void onUpdate(const FrameInfo& frameInfo) override;