aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/ui_layer.cpp104
-rw-r--r--src/ui/workspace.h16
2 files changed, 114 insertions, 6 deletions
diff --git a/src/ui/ui_layer.cpp b/src/ui/ui_layer.cpp
index 6748be2..4aa470b 100644
--- a/src/ui/ui_layer.cpp
+++ b/src/ui/ui_layer.cpp
@@ -6,7 +6,12 @@
#include "rendering/render_api.h"
#include <imgui.h>
+#include <ImGuizmo.h>
#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <algorithm>
+#include <cstdlib>
+#include <cstdio>
#include <string>
namespace Donut
@@ -52,7 +57,7 @@ namespace Donut
namespace
{
- // Setup: renderer backend + environment
+ // Setup: display / renderer / overlay / environment settings
class SetupWorkspace : public Workspace
{
public:
@@ -60,9 +65,52 @@ namespace Donut
auto view() const -> View override { return View::None; }
auto on_ui(const UIContext& ctx) -> void override
{
+ auto& s = SettingsManager::get_settings();
+
+ if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen))
+ {
+ const bool fs = ctx.actions.is_fullscreen();
+ int cw = 0, ch = 0; ctx.actions.get_window_size(cw, ch);
+
+ // Resolution presets (windowed only).
+ struct Res { const char* label; int w, h; };
+ static const Res kRes[] = {
+ { "1280 x 720", 1280, 720 }, { "1600 x 900", 1600, 900 },
+ { "1920 x 1080", 1920, 1080 }, { "2560 x 1440", 2560, 1440 },
+ };
+ char cur_label[32]; std::snprintf(cur_label, sizeof(cur_label), "%d x %d", cw, ch);
+ ImGui::BeginDisabled(fs);
+ if (ImGui::BeginCombo("Resolution", cur_label))
+ {
+ for (const auto& r : kRes)
+ if (ImGui::Selectable(r.label, cw == r.w && ch == r.h))
+ ctx.actions.set_resolution(r.w, r.h);
+ ImGui::EndCombo();
+ }
+ ImGui::EndDisabled();
+
+ bool fs_v = fs;
+ if (ImGui::Checkbox("Fullscreen", &fs_v)) ctx.actions.set_fullscreen(fs_v);
+
+ if (ImGui::Checkbox("V-Sync", &s.graphics.v_sync_enabled))
+ {
+ ctx.actions.set_vsync(s.graphics.v_sync_enabled);
+ SettingsManager::save_settings();
+ }
+
+ ImGui::BeginDisabled(s.graphics.v_sync_enabled);
+ if (ImGui::SliderInt("Frame cap", &s.simulation.target_fps, 30, 240, "%d FPS"))
+ SettingsManager::save_settings();
+ ImGui::EndDisabled();
+ if (s.graphics.v_sync_enabled)
+ ImGui::TextDisabled("Frame cap applies only with V-Sync off.");
+
+ if (ImGui::SliderFloat("UI scale", &m_ui_scale, 0.8f, 1.6f, "%.2fx"))
+ ctx.actions.set_ui_scale(m_ui_scale);
+ }
+
if (ImGui::CollapsingHeader("Renderer", ImGuiTreeNodeFlags_DefaultOpen))
{
- auto& s = SettingsManager::get_settings();
const char* apis[] = { "OpenGL", "Vulkan" };
int cur = (s.graphics.render_api == "Vulkan") ? 1 : 0;
if (ImGui::Combo("Graphics API", &cur, apis, 2))
@@ -74,7 +122,15 @@ namespace Donut
if (s.graphics.render_api != running)
ImGui::TextColored(ImVec4(0.98f, 0.62f, 0.20f, 1.0f), "Restart to apply (%s running)", running);
else
- ImGui::TextDisabled("Active backend: %s", running);
+ ImGui::TextDisabled("Active backend: %s (%s)", running, ctx.device_name.c_str());
+ }
+
+ if (ImGui::CollapsingHeader("Overlay", ImGuiTreeNodeFlags_DefaultOpen))
+ {
+ bool dirty = false;
+ dirty |= ImGui::Checkbox("Show FPS", &s.graphics.show_fps);
+ dirty |= ImGui::Checkbox("Show frame time", &s.graphics.show_performance_metrics);
+ if (dirty) SettingsManager::save_settings();
}
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
@@ -95,6 +151,8 @@ namespace Donut
ImGui::TextDisabled("Switching rebuilds the cubemap (brief pause).");
}
}
+ private:
+ float m_ui_scale = 1.0f;
};
// Scene: place and edit objects
@@ -141,8 +199,36 @@ namespace Donut
ImGui::DragFloat3("Position", &o.position.x, 0.1f);
ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f);
ImGui::ColorEdit3("Color", &o.color.x);
+
+ ImGui::Spacing();
+ if (ImGui::RadioButton("Move", m_op == ImGuizmo::TRANSLATE)) m_op = ImGuizmo::TRANSLATE;
+ ImGui::SameLine();
+ if (ImGui::RadioButton("Scale", m_op == ImGuizmo::SCALE)) m_op = ImGuizmo::SCALE;
+ ImGui::SameLine(); ImGui::TextDisabled("(drag the handles in the viewport)");
+
+ // Viewport gizmo for the selected sphere. Uses the scene camera
+ // (its projection was set for this frame by the Application) and
+ // draws into the background draw list so it sits over the render
+ // but under the panels.
+ ImGuizmo::SetOrthographic(false);
+ ImGuizmo::SetDrawlist(ImGui::GetBackgroundDrawList());
+ ImGuiIO& io = ImGui::GetIO();
+ ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y);
+ glm::mat4 view = sc.scene_camera.get_view_matrix();
+ glm::mat4 proj = sc.scene_camera.get_projection_matrix();
+ glm::mat4 xform = glm::translate(glm::mat4(1.0f), o.position)
+ * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius));
+ if (ImGuizmo::Manipulate(&view[0][0], &proj[0][0], m_op, ImGuizmo::WORLD, &xform[0][0]))
+ {
+ float t[3], r[3], s[3];
+ ImGuizmo::DecomposeMatrixToComponents(&xform[0][0], t, r, s);
+ o.position = glm::vec3(t[0], t[1], t[2]);
+ o.radius = std::max(0.05f, (s[0] + s[1] + s[2]) / 3.0f);
+ }
}
}
+ private:
+ ImGuizmo::OPERATION m_op = ImGuizmo::TRANSLATE;
};
// Simulation: camera + accretion disk + quality
@@ -239,6 +325,8 @@ namespace Donut
static bool styled = false;
if (!styled) { apply_donut_style(); styled = true; }
+ ImGuizmo::BeginFrame(); // gizmos are drawn by the Scene workspace
+
// Full-viewport dock space with a pass-through centre, so the panel docks to
// any edge while the render shows through the middle.
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode);
@@ -248,8 +336,14 @@ namespace Donut
ImGuiIO& io = ImGui::GetIO();
ImGui::TextDisabled("%s", ctx.device_name.empty() ? "GPU" : ctx.device_name.c_str());
- ImGui::Text("%.0f FPS", io.Framerate);
- ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f);
+ const bool show_fps = SettingsManager::get_show_fps();
+ const bool show_ms = SettingsManager::get_show_performance_metrics();
+ if (show_fps) ImGui::Text("%.0f FPS", io.Framerate);
+ if (show_ms)
+ {
+ if (show_fps) ImGui::SameLine();
+ ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f);
+ }
ImGui::Spacing();
if (ImGui::BeginTabBar("workspaces"))
diff --git a/src/ui/workspace.h b/src/ui/workspace.h
index 134d517..804d5a8 100644
--- a/src/ui/workspace.h
+++ b/src/ui/workspace.h
@@ -7,12 +7,26 @@ namespace Donut
{
class Scene;
+ // App-level actions a workspace can request (window/device changes it can't do
+ // itself). The Application implements this — it owns the window and device.
+ struct AppActions
+ {
+ virtual ~AppActions() = default;
+ virtual auto set_resolution(int w, int h) -> void = 0;
+ virtual auto set_fullscreen(bool on) -> void = 0;
+ virtual auto set_vsync(bool on) -> void = 0;
+ virtual auto set_ui_scale(float scale) -> void = 0;
+ virtual auto get_window_size(int& w, int& h) const -> void = 0;
+ virtual auto is_fullscreen() const -> bool = 0;
+ };
+
// What a workspace gets each frame to build its panels. `scene` is mutable —
- // workspaces edit the document through it.
+ // workspaces edit the document through it; `actions` performs app-level changes.
struct UIContext
{
Scene& scene;
std::string device_name;
+ AppActions& actions;
};
// A Dorico-style workspace: one tab that owns its side panels and declares