#include "ui/app_chrome.hpp" #include #include #include #include #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][]" 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(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(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