The View-menu verbosity switch reset to off on every launch. Register a custom ImGui settings handler (a [CoopUI][State] section in coop_layout.ini, alongside the window layout) so the operator's choice survives restarts. Toggling it marks settings dirty so ImGui's auto-save writes it back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
226 lines
7.3 KiB
C++
226 lines
7.3 KiB
C++
#include "ui/app_chrome.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h" // ImGuiSettingsHandler / AddSettingsHandler (custom .ini section)
|
|
|
|
namespace coop
|
|
{
|
|
|
|
namespace
|
|
{
|
|
// --- Custom .ini persistence for the UI switches ---------------------------
|
|
// We piggy-back on ImGui's .ini so the "Debug details" verbosity survives restarts
|
|
// without inventing a separate settings file. The section looks like:
|
|
// [CoopUI][State]
|
|
// DebugDetails=1
|
|
// ImGui calls ReadOpenFn once per "[CoopUI][<name>]" entry; we return the bound
|
|
// UiState* as the entry handle so ReadLineFn can write into it.
|
|
constexpr const char* kUiSettingsType = "CoopUI";
|
|
|
|
void* ui_settings_read_open(ImGuiContext*, ImGuiSettingsHandler* handler, const char* name)
|
|
{
|
|
// Only one entry ("State"); hand back the UiState* so ReadLineFn can fill it.
|
|
return std::strcmp(name, "State") == 0 ? handler->UserData : nullptr;
|
|
}
|
|
|
|
void ui_settings_read_line(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line)
|
|
{
|
|
auto* ui = static_cast<UiState*>(entry);
|
|
// Manual parse (avoids the sscanf CRT-secure deprecation for a single int key).
|
|
constexpr char kKey[] = "DebugDetails=";
|
|
if (std::strncmp(line, kKey, sizeof(kKey) - 1) == 0)
|
|
{
|
|
ui->debug_details = std::atoi(line + sizeof(kKey) - 1) != 0;
|
|
}
|
|
}
|
|
|
|
void ui_settings_write_all(ImGuiContext*, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf)
|
|
{
|
|
const auto* ui = static_cast<const UiState*>(handler->UserData);
|
|
buf->appendf("[%s][State]\n", kUiSettingsType);
|
|
buf->appendf("DebugDetails=%d\n", ui->debug_details ? 1 : 0);
|
|
buf->append("\n");
|
|
}
|
|
|
|
// Set by "View -> Reset layout"; true for the one frame in which panels re-apply
|
|
// their default position/size, then cleared by apply_layout_end_frame().
|
|
bool g_layout_reset = false;
|
|
// Force the computed layout for the first few frames after launch: the viewport
|
|
// WorkSize isn't trustworthy on frame 0, and FirstUseEver would otherwise lock in
|
|
// those wrong positions (overlapping) until the user pressed Reset layout.
|
|
int g_startup_force = 4;
|
|
// True when ImGui loaded a saved layout (.ini) at startup; then the startup force is
|
|
// suppressed so the restored window positions survive (Reset layout still re-applies).
|
|
bool g_had_persisted_layout = false;
|
|
} // namespace
|
|
|
|
void register_ui_settings(UiState& ui)
|
|
{
|
|
// Idempotent: don't stack a second handler if this is somehow called twice.
|
|
if (ImGui::FindSettingsHandler(kUiSettingsType) != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
ImGuiSettingsHandler handler;
|
|
handler.TypeName = kUiSettingsType;
|
|
handler.TypeHash = ImHashStr(kUiSettingsType);
|
|
handler.ReadOpenFn = ui_settings_read_open;
|
|
handler.ReadLineFn = ui_settings_read_line;
|
|
handler.WriteAllFn = ui_settings_write_all;
|
|
handler.UserData = &ui;
|
|
ImGui::AddSettingsHandler(&handler); // copied into the context
|
|
}
|
|
|
|
void request_layout_reset()
|
|
{
|
|
g_layout_reset = true;
|
|
}
|
|
|
|
void set_layout_persisted(bool had_persisted_layout)
|
|
{
|
|
g_had_persisted_layout = had_persisted_layout;
|
|
}
|
|
|
|
void apply_layout_end_frame()
|
|
{
|
|
g_layout_reset = false;
|
|
if (g_startup_force > 0)
|
|
{
|
|
--g_startup_force;
|
|
}
|
|
}
|
|
|
|
void apply_panel_layout(Panel panel)
|
|
{
|
|
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
|
const ImVec2 o = vp->WorkPos; // below the main menu bar
|
|
const ImVec2 s = vp->WorkSize;
|
|
const float m = 12.0f; // outer margin
|
|
const float gap = 10.0f; // between panels
|
|
|
|
// Three columns: Injection (left) and Log (right) are the wide ones (window list
|
|
// titles / log lines); the center control-panel column is the narrow one.
|
|
const float usable_w = std::max(600.0f, s.x - 2.0f * m - 2.0f * gap);
|
|
const float left_w = usable_w * 0.36f;
|
|
const float center_w = usable_w * 0.28f;
|
|
const float right_w = usable_w * 0.36f;
|
|
const float left_x = o.x + m;
|
|
const float center_x = left_x + left_w + gap;
|
|
const float right_x = center_x + center_w + gap;
|
|
|
|
const float top = o.y + m;
|
|
const float full_h = std::max(200.0f, s.y - 2.0f * m);
|
|
|
|
// Center column stacks Controllers / Video / Audio. Controllers has the most
|
|
// content (incoming pads + poll + round-trip tables) so it gets the most height;
|
|
// Audio the least (proportions measured from the operator's preferred layout).
|
|
const float stack_avail = std::max(150.0f, full_h - 2.0f * gap);
|
|
const float ctrl_h = stack_avail * 0.45f;
|
|
const float vid_h = stack_avail * 0.32f;
|
|
const float audio_h = stack_avail * 0.23f;
|
|
|
|
ImVec2 pos, size;
|
|
switch (panel)
|
|
{
|
|
case Panel::Injection:
|
|
pos = ImVec2(left_x, top);
|
|
size = ImVec2(left_w, full_h);
|
|
break;
|
|
case Panel::Controllers:
|
|
pos = ImVec2(center_x, top);
|
|
size = ImVec2(center_w, ctrl_h);
|
|
break;
|
|
case Panel::Video:
|
|
pos = ImVec2(center_x, top + ctrl_h + gap);
|
|
size = ImVec2(center_w, vid_h);
|
|
break;
|
|
case Panel::Audio:
|
|
pos = ImVec2(center_x, top + ctrl_h + gap + vid_h + gap);
|
|
size = ImVec2(center_w, audio_h);
|
|
break;
|
|
case Panel::Log:
|
|
pos = ImVec2(right_x, top);
|
|
size = ImVec2(right_w, full_h);
|
|
break;
|
|
}
|
|
|
|
// Force the computed layout only when the user asks (Reset layout) or on a fresh
|
|
// install with no saved layout to restore. Otherwise FirstUseEver lets ImGui's
|
|
// restored .ini positions stand (and still seeds any brand-new panel).
|
|
const bool force = g_layout_reset || (!g_had_persisted_layout && g_startup_force > 0);
|
|
const ImGuiCond cond = force ? ImGuiCond_Always : ImGuiCond_FirstUseEver;
|
|
ImGui::SetNextWindowPos(pos, cond);
|
|
ImGui::SetNextWindowSize(size, cond);
|
|
}
|
|
|
|
float draw_main_menu_bar(UiState& ui, const FrameStats& stats)
|
|
{
|
|
float height = 0.0f;
|
|
if (!ImGui::BeginMainMenuBar())
|
|
{
|
|
return height;
|
|
}
|
|
|
|
ImGui::TextUnformatted("CoopAllTheThings");
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginMenu("View"))
|
|
{
|
|
ImGui::MenuItem("Controllers", nullptr, &ui.show_controllers);
|
|
ImGui::MenuItem("Injection", nullptr, &ui.show_injection);
|
|
ImGui::MenuItem("Video mirror", nullptr, &ui.show_video);
|
|
ImGui::MenuItem("Audio mirror", nullptr, &ui.show_audio);
|
|
ImGui::MenuItem("Log", nullptr, &ui.show_log);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Debug details", nullptr, &ui.debug_details))
|
|
{
|
|
ImGui::MarkIniSettingsDirty(); // persist the new verbosity to coop_layout.ini
|
|
}
|
|
if (ImGui::MenuItem("Reset layout"))
|
|
{
|
|
request_layout_reset();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Help"))
|
|
{
|
|
ImGui::TextDisabled("F1 hide/show this overlay");
|
|
ImGui::TextDisabled("F2 release/clip the operator cursor");
|
|
ImGui::TextDisabled("Esc quit");
|
|
ImGui::Separator();
|
|
ImGui::TextDisabled("This window is what Remote Play");
|
|
ImGui::TextDisabled("Together captures.");
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
// Right-aligned performance readout: stable FPS with the frame-time spread
|
|
// (min/max over the last second) so stutter is visible at a glance.
|
|
char perf[96];
|
|
// Fixed field widths so the readout doesn't jitter/blur as values cross digit
|
|
// thresholds (e.g. 99 -> 100) each frame.
|
|
std::snprintf(perf, sizeof(perf), "%4.0f FPS %6.2f ms (%6.2f-%6.2f)", stats.fps(), stats.avg_ms(),
|
|
stats.min_ms(), stats.max_ms());
|
|
const float text_w = ImGui::CalcTextSize(perf).x;
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - text_w - ImGui::GetStyle().FramePadding.x * 2.0f);
|
|
if (stats.max_ms() > 25.0f) // ~sub-40 FPS spike in the window
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", perf);
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextUnformatted(perf);
|
|
}
|
|
|
|
height = ImGui::GetWindowHeight();
|
|
ImGui::EndMainMenuBar();
|
|
return height;
|
|
}
|
|
|
|
} // namespace coop
|