changes for ImGui

This commit is contained in:
2026-09-15 02:23:22 -07:00
parent 12a1e6eca9
commit 05fe3c2ee8
14 changed files with 593 additions and 6 deletions
+14 -6
View File
@@ -3,10 +3,13 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp> #include <sophia/canvas.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -27,7 +30,7 @@ class Application {
Application(const Application&) = delete; Application(const Application&) = delete;
Application& operator=(const Application&) = delete; Application& operator=(const Application&) = delete;
Application(const Config& config); explicit Application(const Config& config);
virtual ~Application(); virtual ~Application();
void run(); void run();
@@ -40,15 +43,15 @@ class Application {
virtual void onRender() {} virtual void onRender() {}
virtual void onDestroy() {} virtual void onDestroy() {}
AssetRegistry& assets() { return this->assetRegistry; }
PanelRegistry& panels() { return this->panelRegistry; }
DebugUI& debugUI() { return this->_debugUI; }
Canvas canvas; Canvas canvas;
Camera2D camera; Camera2D camera;
std::vector<std::string> args; std::vector<std::string> args;
// Creates a mesh that persists across frames, for callers that want to
// draw the same geometry many times with different transforms (e.g.
// instancing) instead of resubmitting vertices every frame via Canvas.
MeshHandle createMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices);
void drawMesh(MeshHandle handle, const glm::mat4& transform); void drawMesh(MeshHandle handle, const glm::mat4& transform);
private: private:
@@ -59,6 +62,11 @@ class Application {
class Impl; class Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry;
PanelRegistry panelRegistry;
DebugUI _debugUI;
}; };
} // namespace sophia } // namespace sophia
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <sophia/handles.hpp>
#include <sophia/ui/panel_registry.hpp>
namespace sophia {
class DebugUI {
public:
DebugUI(const DebugUI&) = delete;
DebugUI& operator=(const DebugUI&) = delete;
explicit DebugUI(PanelRegistry& registry);
~DebugUI() = default;
void show();
void hide();
void showPanelRegistry(bool show = true);
void showFrameStats(bool show = true);
private:
PanelRegistry& registry;
PanelHandle panelRegistryHandle;
PanelHandle frameStatsHandle;
};
} // namespace sophia
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <string>
#include <utility>
namespace sophia {
class Panel {
public:
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)} {}
virtual ~Panel() = default;
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
const std::string& getName() const { return name; }
bool isVisible() const { return this->visible; }
void setVisible(bool visible) { this->visible = visible; }
void toggleVisible() { this->visible = !this->visible; }
protected:
bool visible = true;
private:
std::string name;
};
} // namespace sophia
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <memory>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/util/assert.hpp>
#include <string_view>
#include <typeinfo>
#include <utility>
#include <vector>
namespace sophia {
class PanelRegistry {
public:
PanelRegistry(const PanelRegistry&) = delete;
PanelRegistry& operator=(const PanelRegistry&) = delete;
PanelRegistry() = default;
~PanelRegistry() = default;
PanelHandle add(std::unique_ptr<Panel> panel);
template <typename T, typename... Args>
PanelHandle emplace(Args&&... args) {
return add(std::make_unique<T>(std::forward<Args>(args)...));
}
const Panel& get(PanelHandle handle) const;
Panel& get(PanelHandle handle) {
return const_cast<Panel&>(std::as_const(*this).get(handle));
}
template <typename T>
const T& get(PanelHandle handle) const {
const Panel& panel = get(handle);
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
"PanelHandle does not refer to a", typeid(T).name());
return static_cast<const T&>(panel);
}
template <typename T>
T& get(PanelHandle handle) {
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
}
void remove(PanelHandle handle);
bool isValid(PanelHandle handle) const;
template <typename T>
const T* find() const {
for (const Slot& slot : slots) {
if (slot.panel) {
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) {
return panel;
}
}
}
return nullptr;
}
template <typename T>
T* find() {
return const_cast<T*>(std::as_const(*this).template find<T>());
}
const Panel* find(std::string_view name) const {
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->getName() == name) {
return slot.panel.get();
}
}
return nullptr;
}
Panel* find(std::string_view name) {
return const_cast<Panel*>(std::as_const(*this).find(name));
}
u32 size() const;
u32 visibleCount() const;
std::vector<std::string_view> names() const {
std::vector<std::string_view> result;
result.reserve(slots.size());
for (const Slot& slot : slots) {
if (slot.panel) {
result.push_back(slot.panel->getName());
}
}
return result;
}
void update(const FrameInfo& frameInfo);
private:
struct Slot {
std::unique_ptr<Panel> panel;
u32 generation = 0;
};
std::vector<Slot> slots;
std::vector<u32> freeList;
};
} // namespace sophia
+85
View File
@@ -0,0 +1,85 @@
#include "imgui_renderer.hpp"
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>
namespace sophia {
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat,
u32 minImageCount) {
// 1. Construct ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true);
ImGui_ImplVulkan_InitInfo initInfo = {};
device.populateImGuiInitInfo(initInfo);
initInfo.DescriptorPoolSize =
IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5;
initInfo.MinImageCount = minImageCount;
initInfo.ImageCount = minImageCount;
VkFormat rawColorAttachmentFormat =
static_cast<VkFormat>(colorAttachmentFormat);
initInfo.UseDynamicRendering = true;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount =
1;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo
.pColorAttachmentFormats = &rawColorAttachmentFormat;
ImGui_ImplVulkan_Init(&initInfo);
// 2. Enable dark theme
ImGui::StyleColorsDark();
// 3. Enable docking / keyboard
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
/**
* When viewports are enabled we tweak WindowRounding/WindowBg so
* platform windows can look identical to regular ones.
*/
ImGuiStyle& style = ImGui::GetStyle();
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
}
ImGuiRenderer::~ImGuiRenderer() {
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
}
void ImGuiRenderer::beginFrame() {
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(),
ImGuiDockNodeFlags_PassthruCentralNode);
}
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
ImGui::Render();
ImDrawData* mainDrawData = ImGui::GetDrawData();
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer);
}
} // namespace sophia
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <sophia/types.hpp>
#include "device.hpp"
#include "window.hpp"
namespace sophia {
class ImGuiRenderer {
public:
ImGuiRenderer(const ImGuiRenderer&) = delete;
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, u32 minImageCount);
~ImGuiRenderer();
void beginFrame();
void endFrame(vk::CommandBuffer commandBuffer);
};
} // namespace sophia
+3
View File
@@ -45,6 +45,9 @@ class RenderContext {
vk::Format getImageFormat() const { vk::Format getImageFormat() const {
return this->swapchain->getImageFormat(); return this->swapchain->getImageFormat();
} }
u32 getImageCount() const {
return static_cast<u32>(this->swapchain->imageCount());
}
private: private:
void recreateSwapchain(); void recreateSwapchain();
+37
View File
@@ -0,0 +1,37 @@
#include <sophia/ui/debug_ui.hpp>
#include "panels/frame_stats_panel.hpp"
#include "panels/panel_registry_panel.hpp"
namespace sophia {
DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} {
// #ifndef NDEBUG
this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry);
this->frameStatsHandle = registry.emplace<FrameStatsPanel>();
// #endif // !NDEBUG
}
void DebugUI::show() {
showPanelRegistry(true);
showFrameStats(true);
}
void DebugUI::hide() {
showPanelRegistry(false);
showFrameStats(false);
}
void DebugUI::showPanelRegistry(bool show) {
if (registry.isValid(panelRegistryHandle)) {
registry.get(panelRegistryHandle).setVisible(show);
}
}
void DebugUI::showFrameStats(bool show) {
if (registry.isValid(frameStatsHandle)) {
registry.get(frameStatsHandle).setVisible(show);
}
}
} // namespace sophia
+88
View File
@@ -0,0 +1,88 @@
#include <sophia/ui/panel_registry.hpp>
#include <sophia/util/assert.hpp>
namespace sophia {
namespace {
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
} // namespace
PanelHandle PanelRegistry::add(std::unique_ptr<Panel> panel) {
if (!freeList.empty()) {
u32 index = freeList.back();
freeList.pop_back();
slots[index].panel = std::move(panel);
return PanelHandle{index, slots[index].generation};
}
slots.push_back(Slot{std::move(panel), 0});
return PanelHandle{static_cast<u32>(slots.size() - 1), 0};
}
const Panel& PanelRegistry::get(PanelHandle handle) const {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
const Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle refers to a removed panel:", handle.index);
return *slot.panel;
}
void PanelRegistry::remove(PanelHandle handle) {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle already removed:", handle.index);
slot.panel.reset();
slot.generation = (slot.generation + 1) & GENERATION_MASK;
freeList.push_back(handle.index);
}
bool PanelRegistry::isValid(PanelHandle handle) const {
if (!handle.isValid() || handle.index >= slots.size()) {
return false;
}
const Slot& slot = slots[handle.index];
return slot.generation == handle.generation && slot.panel != nullptr;
}
u32 PanelRegistry::size() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel) {
count++;
}
}
return count;
}
u32 PanelRegistry::visibleCount() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
count++;
}
}
return count;
}
void PanelRegistry::update(const FrameInfo& frameInfo) {
for (auto& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
slot.panel->onUpdate(frameInfo);
}
}
}
} // namespace sophia
@@ -0,0 +1,57 @@
#include "ecs_inspector_panel.hpp"
#include <imgui.h>
#include <sophia/components.hpp>
namespace sophia {
void EcsInspectorPanel::onUpdate(const FrameInfo&) {
ImGui::Begin("ECS Inspector");
entityScratch.clear();
for (entt::entity entity : registry.storage<entt::entity>()) {
entityScratch.push_back(entity);
}
ImGui::BeginChild("EntityList", ImVec2(0, 200), true);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(entityScratch.size()));
while (clipper.Step()) {
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
entt::entity entity = entityScratch[static_cast<size_t>(i)];
const TagComponent* tag = registry.try_get<TagComponent>(entity);
std::string label = tag ? tag->tag : "Entity";
ImGui::PushID(static_cast<int>(entt::to_integral(entity)));
if (ImGui::Selectable(label.c_str(), selected == entity)) {
selected = entity;
}
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::Separator();
if (registry.valid(selected)) {
if (auto* transform = registry.try_get<TransformComponent>(selected)) {
ImGui::DragFloat3("Position", &transform->position.x, 0.1f);
ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f);
ImGui::Text("Rotation: %.2f, %.2f, %.2f, %.2f", transform->rotation.w,
transform->rotation.x, transform->rotation.y,
transform->rotation.z);
}
if (auto* mesh = registry.try_get<MeshComponent>(selected)) {
ImGui::Text("Mesh: index=%u generation=%u", mesh->handle.index,
mesh->handle.generation);
}
} else {
ImGui::TextDisabled("No entity selected");
}
ImGui::End();
}
} // namespace sophia
@@ -0,0 +1,23 @@
#pragma once
#include <entt/entt.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <vector>
namespace sophia {
class EcsInspectorPanel : public Panel {
public:
explicit EcsInspectorPanel(entt::registry& registry)
: Panel{"ECS Inspector"}, registry{registry} {}
void onUpdate(const FrameInfo& frameInfo) override;
private:
entt::registry& registry;
entt::entity selected = entt::null;
std::vector<entt::entity> entityScratch;
};
} // namespace sophia
@@ -0,0 +1,44 @@
#pragma once
#include <imgui.h>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
namespace sophia {
class FrameStatsPanel : public Panel {
public:
FrameStatsPanel() : Panel{"Frame Stats"} {}
void onUpdate(const FrameInfo& frameInfo) override {
frameCount++;
accumulatedTime += frameInfo.deltaTime;
if (frameInfo.elapsedTime >= nextRefreshTime) {
displayedFps = static_cast<double>(frameCount) / accumulatedTime;
displayedFrameTimeMs =
(accumulatedTime / static_cast<double>(frameCount)) * 1000.0;
frameCount = 0;
accumulatedTime = 0.0;
nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL;
}
ImGui::Begin("Frame Stats");
ImGui::Text("FPS: %.1f", displayedFps);
ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs);
ImGui::End();
}
private:
static constexpr double REFRESH_INTERVAL = 1.0;
double accumulatedTime = 0.0;
u32 frameCount = 0;
double nextRefreshTime = 0.0;
double displayedFps = 0.0;
double displayedFrameTimeMs = 0.0;
};
} // namespace sophia
@@ -0,0 +1,28 @@
#include "panel_registry_panel.hpp"
#include <imgui.h>
namespace sophia {
void PanelRegistryPanel::onUpdate(const FrameInfo&) {
ImGui::Begin("PanelRegistry");
ImGui::Text("%u panels (%u visible)", registry.size(),
registry.visibleCount());
ImGui::Separator();
for (std::string_view name : registry.names()) {
Panel* panel = registry.find(name);
if (!panel || panel == this) {
continue;
}
bool visible = panel->isVisible();
if (ImGui::Checkbox(name.data(), &visible)) {
panel->setVisible(visible);
}
}
ImGui::End();
}
} // namespace sophia
@@ -0,0 +1,20 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/panel_registry.hpp>
namespace sophia {
class PanelRegistryPanel : public Panel {
public:
explicit PanelRegistryPanel(PanelRegistry& registry)
: Panel{"PanelRegistry"}, registry{registry} {}
void onUpdate(const FrameInfo& frameInfo) override;
private:
PanelRegistry& registry;
};
} // namespace sophia