Files
CoopAllTheThings/host/src/ui/app_chrome.cpp
BlackMark 699ca31d31 Persist the "Debug details" toggle in the layout .ini
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>
2026-06-21 23:59:03 +02:00

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