From 100caa0f5c1df1d920dba9d4e02eb1ba513c7c7b Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:23:08 -0700 Subject: [PATCH] overlay: refactor overlay layering (#739) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Create three distinct layers for the overlay: 1. Bottommost persistent layer - non-interactable layer that is always on. This was only for notifications, but now the FPS widget lives here as well. 2. Overlay windows layer - most interactable windows go here. 3. Topmost main menu - this is reserved for the main menu (escape key) and this is a modal dialog that occludes the layers below. Why? - `toggle overlay` behavior with FPS widget *also* toggling on/off was a bit confusing (now they're two separate keys) - FPS widget is popular, but it caused the entire overlay to be active, which affects how input is handled - the main menu being a standalone window was a little awkward (now it's a modal) ## Testing --- src/spice2x/cfg/configurator.cpp | 1 - src/spice2x/games/io.cpp | 4 +- src/spice2x/games/io.h | 3 +- src/spice2x/launcher/launcher.cpp | 15 ++- src/spice2x/launcher/options.cpp | 20 +++- src/spice2x/launcher/options.h | 1 + src/spice2x/overlay/notifications.cpp | 11 ++ src/spice2x/overlay/overlay.cpp | 79 ++++++++++----- src/spice2x/overlay/overlay.h | 26 +++-- src/spice2x/overlay/window.cpp | 55 ++++++++-- src/spice2x/overlay/window.h | 13 +++ src/spice2x/overlay/windows/config.cpp | 28 ++---- src/spice2x/overlay/windows/exitprompt.cpp | 73 +++++++++++--- src/spice2x/overlay/windows/exitprompt.h | 7 +- src/spice2x/overlay/windows/fps.cpp | 112 ++++++++++++++++----- src/spice2x/overlay/windows/fps.h | 2 - 16 files changed, 348 insertions(+), 102 deletions(-) diff --git a/src/spice2x/cfg/configurator.cpp b/src/spice2x/cfg/configurator.cpp index 549e672..8753eaf 100644 --- a/src/spice2x/cfg/configurator.cpp +++ b/src/spice2x/cfg/configurator.cpp @@ -93,7 +93,6 @@ namespace cfg { overlay::create_software(this->wnd.hWnd); } overlay::OVERLAY->set_active(true); - overlay::OVERLAY->hotkeys_enable = false; ImGui::GetIO().MouseDrawCursor = false; // run window diff --git a/src/spice2x/games/io.cpp b/src/spice2x/games/io.cpp index 547d617..f9a6eac 100644 --- a/src/spice2x/games/io.cpp +++ b/src/spice2x/games/io.cpp @@ -444,6 +444,8 @@ namespace games { // overlay button definitions names.emplace_back("Screenshot"); vkey_defaults.push_back(VK_SNAPSHOT); + names.emplace_back("Toggle All Windows"); + vkey_defaults.push_back(VK_OEM_3); // backtick ` names.emplace_back("Toggle Main Menu"); vkey_defaults.push_back(VK_ESCAPE); names.emplace_back("Toggle Sub Screen"); @@ -468,7 +470,7 @@ namespace games { vkey_defaults.push_back(VK_F10); names.emplace_back("Toggle Screen Resize"); vkey_defaults.push_back(VK_F11); - names.emplace_back("Toggle Overlay"); + names.emplace_back("Toggle FPS"); vkey_defaults.push_back(VK_F12); names.emplace_back("Toggle Camera Control"); vkey_defaults.push_back(0xFF); diff --git a/src/spice2x/games/io.h b/src/spice2x/games/io.h index 81c3de0..544316a 100644 --- a/src/spice2x/games/io.h +++ b/src/spice2x/games/io.h @@ -8,6 +8,7 @@ namespace games { namespace OverlayButtons { enum { Screenshot, + ToggleAllWindows, ToggleMainMenu, ToggleSubScreen, InsertCoin, @@ -20,7 +21,7 @@ namespace games { ToggleControl, TogglePatchManager, ToggleScreenResize, - ToggleOverlay, + ToggleFps, ToggleCameraControl, TriggerPinMacroP1, TriggerPinMacroP2, diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index fb7695e..e5f9bce 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1182,7 +1182,20 @@ int main_implementation(int argc, char *argv[]) { overlay::AUTO_SHOW_FPS = true; } if (options[launcher::Options::spice2x_FpsOpposite].value_bool()) { - overlay::FPS_SHOULD_FLIP = true; + // deprecated flag: equivalent to anchoring the FPS window top-left + overlay::FPS_LOCATION = overlay::FpsLocation::TopLeft; + } + if (options[launcher::Options::FpsLocation].is_active()) { + const auto txt = options[launcher::Options::FpsLocation].value_text(); + if (txt == "topright") { + overlay::FPS_LOCATION = overlay::FpsLocation::TopRight; + } else if (txt == "topleft") { + overlay::FPS_LOCATION = overlay::FpsLocation::TopLeft; + } else if (txt == "bottomleft") { + overlay::FPS_LOCATION = overlay::FpsLocation::BottomLeft; + } else if (txt == "bottomright") { + overlay::FPS_LOCATION = overlay::FpsLocation::BottomRight; + } } if (options[launcher::Options::spice2x_SubScreenAutoShow].value_bool()) { overlay::AUTO_SHOW_SUBSCREEN = true; diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 9f8e5a5..9a4ff68 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -496,12 +496,28 @@ static const std::vector OPTION_DEFINITIONS = { }, { // spice2x_FpsOpposite - .title = "Show FPS/Clock top-left", + .title = "Show FPS/Clock top-left (DEPRECATED - use -fpslocation instead)", .name = "fpsflip", - .desc = "Show FPS / clock / timer overlay on the top left of the screen instead of the top right.", + .desc = "Show FPS / clock / timer overlay on the top left of the screen instead of the top right. " + "Deprecated - use -fpslocation instead.", .type = OptionType::Bool, + .hidden = true, .category = "Overlay", }, + { + // FpsLocation + .title = "FPS/Clock Location", + .name = "fpslocation", + .desc = "Select which corner of the screen the FPS / clock / timer overlay appears in.", + .type = OptionType::Enum, + .category = "Overlay", + .elements = { + {"topright", ""}, + {"topleft", ""}, + {"bottomleft", ""}, + {"bottomright", ""}, + }, + }, { // spice2x_SubScreenAutoShow .title = "Auto Show Subscreen", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 4c79b5b..2d7ec9d 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -56,6 +56,7 @@ namespace launcher { NotificationPosition, spice2x_FpsAutoShow, spice2x_FpsOpposite, + FpsLocation, spice2x_SubScreenAutoShow, spice2x_IOPanelAutoShow, spice2x_KeypadAutoShow, diff --git a/src/spice2x/overlay/notifications.cpp b/src/spice2x/overlay/notifications.cpp index 1f2fa9b..0cc0ab6 100644 --- a/src/spice2x/overlay/notifications.cpp +++ b/src/spice2x/overlay/notifications.cpp @@ -146,6 +146,17 @@ namespace overlay::notifications { float height = 0.f; if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) { + // keep toasts above other overlay windows (e.g. the persistent FPS + // window, which may be toggled on after a toast already exists), but + // tuck them behind a blocking modal so they get dimmed/occluded by the + // modal backdrop instead of floating on top of it. + ImGuiWindow *toast_window = ImGui::GetCurrentWindow(); + if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) { + ImGui::BringWindowToDisplayBehind(toast_window, modal); + } else { + ImGui::BringWindowToDisplayFront(toast_window); + } + const ImVec2 win_pos = ImGui::GetWindowPos(); const ImVec2 win_size = ImGui::GetWindowSize(); diff --git a/src/spice2x/overlay/overlay.cpp b/src/spice2x/overlay/overlay.cpp index 05366a7..cd888f5 100644 --- a/src/spice2x/overlay/overlay.cpp +++ b/src/spice2x/overlay/overlay.cpp @@ -66,7 +66,7 @@ namespace overlay { bool AUTO_SHOW_KEYPAD_P1 = false; bool AUTO_SHOW_KEYPAD_P2 = false; bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false; - bool FPS_SHOULD_FLIP = false; + FpsLocation FPS_LOCATION = FpsLocation::TopRight; std::optional UI_SCALE_PERCENT; // global @@ -298,6 +298,7 @@ void overlay::SpiceOverlay::init() { colors[ImGuiCol_Separator] = ImVec4(0.32f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_SeparatorHovered] = ImVec4(0.42f, 0.22f, 0.22f, 1.00f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.52f, 0.22f, 0.22f, 1.00f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.5f); #ifdef IMGUI_HAS_DOCK colors[ImGuiCol_DockingPreview] = ImVec4(0.85f, 0.15f, 0.15f, 0.40f); @@ -396,14 +397,14 @@ void overlay::SpiceOverlay::init() { bool set_overlay_active = false; - // referenced windows - this->window_add(window_fps = new overlay::windows::FPS(this)); + // owned separately from `windows` so it never affects overlay activation/input gating + window_fps = std::make_unique(this); if (!cfg::CONFIGURATOR_STANDALONE && AUTO_SHOW_FPS) { window_fps->set_active(true); - set_overlay_active = true; } - this->window_add(window_main_menu = new overlay::windows::ExitPrompt(this)); + // owned separately from `windows` so it is not part of the overlay window layer + window_main_menu = std::make_unique(this); // add default windows this->window_add(window_config = new overlay::windows::Config(this)); @@ -544,9 +545,13 @@ void overlay::SpiceOverlay::new_frame() { const bool draw_notifications = this->renderer != OverlayRenderer::SOFTWARE && overlay::notifications::has_pending(); + // persistent FPS window: drawn whenever active, independent of the overlay + const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE + && this->window_fps->get_active(); + // check if there is nothing to draw this->has_pending_frame = false; - if (!this->active && !draw_notifications) { + if (!this->active && !draw_notifications && !draw_fps_persistent) { return; } @@ -577,11 +582,17 @@ void overlay::SpiceOverlay::new_frame() { window->build(); } + // draw the main menu on top of the overlay windows + this->window_main_menu->build(); + if (SHOW_DEBUG_LOG_WINDOW) { ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW); } } + if (draw_fps_persistent) { + this->window_fps->build(); + } // draw notifications last so they paint on top of any overlay windows if (draw_notifications) { overlay::notifications::draw(); @@ -750,13 +761,19 @@ void overlay::SpiceOverlay::d3d9_render_draw(const bool force_submit) { void overlay::SpiceOverlay::update() { - // check overlay toggle + // there are three layers - + // bottommost layer - FPS, notifications (non-interactable) + // overlay layer - most windows + // topmost layer - main menu (popup) + auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); - bool toggle_down_new = overlay_buttons + + // check overlay toggle + const bool toggle_down_new = overlay_buttons && this->hotkeys_triggered() - && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleOverlay)); + && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleAllWindows)); if (toggle_down_new && !this->toggle_down) { - toggle_active(true); + toggle_active(); } this->toggle_down = toggle_down_new; @@ -769,17 +786,35 @@ void overlay::SpiceOverlay::update() { } this->main_menu_down = main_menu_down_new; + // check FPS toggle - controls the persistent FPS window only, never the overlay + const auto fps_down_new = overlay_buttons + && this->hotkeys_triggered() + && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(games::OverlayButtons::ToggleFps)); + if (fps_down_new && !this->fps_down) { + this->window_fps->toggle_active(); + } + this->fps_down = fps_down_new; + // update windows for (auto &window : this->windows) { window->update(); } - // deactivate if no windows are shown - bool window_active = false; - for (auto &window : this->windows) { - if (window->get_active()) { - window_active = true; - break; + // FPS window + this->window_fps->update(); + + // main menu (owned separately from the overlay window layer) + this->window_main_menu->update(); + + // deactivate if nothing is shown - the main menu keeps the overlay active + // while open even though it is not part of `windows` + bool window_active = this->window_main_menu->get_active(); + if (!window_active) { + for (auto &window : this->windows) { + if (window->get_active()) { + window_active = true; + break; + } } } if (!window_active) { @@ -791,7 +826,7 @@ bool overlay::SpiceOverlay::update_cursor() { return ImGui_ImplSpice_UpdateMouseCursor(); } -void overlay::SpiceOverlay::toggle_active(bool overlay_key) { +void overlay::SpiceOverlay::toggle_active() { // invert active state this->active = !this->active; @@ -800,11 +835,6 @@ void overlay::SpiceOverlay::toggle_active(bool overlay_key) { if (this->window_main_menu) { this->window_main_menu->set_active(false); } - - // show FPS window if toggled with overlay key - if (overlay_key) { - this->window_fps->set_active(this->active); - } } void overlay::SpiceOverlay::show_main_menu() { @@ -847,9 +877,8 @@ bool overlay::SpiceOverlay::has_focus() { } bool overlay::SpiceOverlay::hotkeys_triggered() { - - // check if disabled first - if (!this->hotkeys_enable) { + // prevent hotkeys in spicecfg + if (cfg::CONFIGURATOR_STANDALONE) { return false; } diff --git a/src/spice2x/overlay/overlay.h b/src/spice2x/overlay/overlay.h index d6e5f39..add1772 100644 --- a/src/spice2x/overlay/overlay.h +++ b/src/spice2x/overlay/overlay.h @@ -29,6 +29,14 @@ namespace overlay { SOFTWARE, }; + // corner of the screen the FPS / clock / timer window is anchored to + enum class FpsLocation { + TopLeft, + TopRight, + BottomLeft, + BottomRight, + }; + // settings extern bool ENABLED; extern bool AUTO_SHOW_FPS; @@ -37,7 +45,7 @@ namespace overlay { extern bool AUTO_SHOW_KEYPAD_P1; extern bool AUTO_SHOW_KEYPAD_P2; extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT; - extern bool FPS_SHOULD_FLIP; + extern FpsLocation FPS_LOCATION; extern bool SHOW_DEBUG_LOG_WINDOW; extern std::optional UI_SCALE_PERCENT; @@ -57,10 +65,8 @@ namespace overlay { D3DDEVICE_CREATION_PARAMETERS creation_parameters {}; D3DADAPTER_IDENTIFIER9 adapter_identifier {}; - bool hotkeys_enable = true; // windows - Window *window_fps = nullptr; Window *window_iopanel = nullptr; Window *window_config = nullptr; Window *window_keypad1 = nullptr; @@ -72,6 +78,15 @@ namespace overlay { Window *window_sub = nullptr; Window *window_log = nullptr; + // not part of `windows`: drawn/updated on the persistent layer (like + // notifications), independent of the overlay's active state and input gates. + std::unique_ptr window_fps; + + // not part of `windows`: the main menu / launcher. owned and drawn + // separately from the overlay window layer; it drives the overlay's + // active state while shown so its buttons still receive input. + std::unique_ptr window_main_menu; + explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device); #ifdef SPICE_D3D11 explicit SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device, @@ -87,7 +102,7 @@ namespace overlay { // after render() when the frame is being presented. In-game overlay draws in render(). void d3d9_render_draw(bool force_submit = false); void update(); - void toggle_active(bool overlay_key = false); + void toggle_active(); void show_main_menu(); void set_active(bool active); bool get_active(); @@ -182,13 +197,12 @@ namespace overlay { std::vector> windows; - Window *window_main_menu = nullptr; - std::function subscreen_mouse_handler = nullptr; bool active = false; bool toggle_down = false; bool main_menu_down = false; + bool fps_down = false; bool hotkey_toggle = false; bool hotkey_toggle_last = false; diff --git a/src/spice2x/overlay/window.cpp b/src/spice2x/overlay/window.cpp index d2351a7..34572b4 100644 --- a/src/spice2x/overlay/window.cpp +++ b/src/spice2x/overlay/window.cpp @@ -3,6 +3,7 @@ #include "util/logging.h" #include "games/io.h" #include "misc/eamuse.h" +#include "external/imgui/imgui_internal.h" overlay::Window::Window(SpiceOverlay *overlay) : overlay(overlay) { @@ -35,6 +36,12 @@ void overlay::Window::update() { } else { this->toggle_active(); } + + // raise to the top, but only because the user pressed the hotkey and + // the window is now visible + if (this->active) { + this->bring_to_front(); + } } this->toggle_button_state = toggle_button_new; } @@ -81,14 +88,29 @@ void overlay::Window::build() { ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); } - // create window - if (ImGui::Begin( - (this->title + "###" + to_string(this)).c_str(), - &this->active, - this->flags)) { + const bool custom_window_padding = + !this->remove_window_padding && this->window_padding.x >= 0.f; + if (custom_window_padding) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, this->window_padding); + } - // window attributes - this->calculate_initial_window(); + // create window + // NoFocusOnAppearing: when the overlay is re-shown every window reappears at + // once and ImGui would auto-focus whichever is submitted last, stealing the + // top spot. suppress that so only an explicit focus request (see below) + // decides what comes to the front. + const std::string window_id = this->title + "###" + to_string(this); + if (ImGui::Begin( + window_id.c_str(), + &this->active, + this->flags | ImGuiWindowFlags_NoFocusOnAppearing)) { + + // window attributes - init_pos / init_size are only honored once + // (ImGuiCond_Once), so compute them a single time instead of every frame + if (!this->initial_window_calculated) { + this->initial_window_calculated = true; + this->calculate_initial_window(); + } ImGui::SetWindowPos(this->init_pos, ImGuiCond_Once); ImGui::SetWindowSize(this->init_size, ImGuiCond_Once); @@ -103,12 +125,27 @@ void overlay::Window::build() { // end window ImGui::End(); + // apply an explicit focus request now that the window exists. FocusWindow + // with UnlessBelowModal raises it to the front, but keeps it right below + // any blocking popup/modal instead of jumping in front of it (this also + // re-orders brand-new windows that ImGui places on top by default). + if (this->request_focus) { + this->request_focus = false; + if (ImGuiWindow *w = ImGui::FindWindowByName(window_id.c_str())) { + ImGui::FocusWindow(w, ImGuiFocusRequestFlags_UnlessBelowModal); + } + } + if (this->remove_window_padding) { ImGui::PopStyleVar(); ImGui::PopStyleVar(); ImGui::PopStyleVar(); } + if (custom_window_padding) { + ImGui::PopStyleVar(); + } + } else { // add raw content @@ -142,6 +179,10 @@ void overlay::Window::set_active(bool active) { } } +void overlay::Window::bring_to_front() { + this->request_focus = true; +} + bool overlay::Window::get_active() { // check for active children diff --git a/src/spice2x/overlay/window.h b/src/spice2x/overlay/window.h index d57d3fb..7c778ed 100644 --- a/src/spice2x/overlay/window.h +++ b/src/spice2x/overlay/window.h @@ -24,15 +24,26 @@ namespace overlay { void toggle_active(); void set_active(bool active); bool get_active(); + + // raise this window to the top of the z-order on its next build. only + // call in response to an explicit user action (keyboard toggle, UI + // button) - not for auto-show / programmatic shows. + void bring_to_front(); protected: // state SpiceOverlay *overlay; bool active = false; + // set when the window transitions to visible so build() raises it to the + // top of the z-order on the next frame + bool request_focus = false; std::vector children; // settings bool remove_window_padding = false; + // custom inner window padding, applied before Begin() (preserving border / + // rounding) when x >= 0; ignored if remove_window_padding is set + ImVec2 window_padding = ImVec2(-1, -1); bool draws_window = true; ImGuiSizeCallback resize_callback = nullptr; std::string title = "Title"; @@ -41,6 +52,8 @@ namespace overlay { bool toggle_button_state = false; // init settings + // calculate_initial_window() runs only once; results feed ImGuiCond_Once + bool initial_window_calculated = false; ImVec2 init_pos = ImVec2(0, 0); ImVec2 init_size = ImVec2(0, 0); ImVec2 size_min = ImVec2(0, 0); diff --git a/src/spice2x/overlay/windows/config.cpp b/src/spice2x/overlay/windows/config.cpp index faee123..cb238e7 100644 --- a/src/spice2x/overlay/windows/config.cpp +++ b/src/spice2x/overlay/windows/config.cpp @@ -324,20 +324,12 @@ namespace overlay::windows { } if (ImGui::BeginTabItem("Overlay")) { tab_selected_new = ConfigTab::CONFIG_TAB_OVERLAY; - - const auto offset = cfg::CONFIGURATOR_STANDALONE ? page_offset : page_offset2; - ImGui::BeginChild("Overlay", ImVec2( - 0, ImGui::GetWindowContentRegionMax().y - offset), false); + 0, ImGui::GetWindowContentRegionMax().y - page_offset2), false); // overlay buttons this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name)); ImGui::EndChild(); - - // standalone configurator extras - if (cfg::CONFIGURATOR_STANDALONE) { - ImGui::Checkbox("Enable Overlay in Config", &OVERLAY->hotkeys_enable); - } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Lights")) { @@ -4316,7 +4308,7 @@ namespace overlay::windows { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "NFC card reader status"); + ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "NFC / API card reader status"); ImGui::Spacing(); if (cfg::CONFIGURATOR_STANDALONE) { @@ -4379,14 +4371,16 @@ namespace overlay::windows { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "More tips"); + ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card Manager"); ImGui::Spacing(); - ImGui::BeginDisabled(); - ImGui::TextWrapped("To debug card reader issues, run spice.exe -cfg in command line and check the log."); - ImGui::TextWrapped( - "If you have multiple players, try opening Card Manager window in the game. " - "Check the key bind in Overlay tab for Toggle Card Manager."); - ImGui::EndDisabled(); + if (ImGui::Button("Open Card Manager")) { + if (this->overlay->window_cards != nullptr) { + this->overlay->window_cards->set_active(true); + this->overlay->window_cards->bring_to_front(); + } + } + ImGui::TextUnformatted(""); + ImGui::TextUnformatted(""); } bool Config::validate_ea_card(char card_number[16]) { diff --git a/src/spice2x/overlay/windows/exitprompt.cpp b/src/spice2x/overlay/windows/exitprompt.cpp index 4e286dc..a5f312f 100644 --- a/src/spice2x/overlay/windows/exitprompt.cpp +++ b/src/spice2x/overlay/windows/exitprompt.cpp @@ -1,5 +1,6 @@ #include "exitprompt.h" #include "avs/game.h" +#include "build/defs.h" #include "misc/eamuse.h" #include "util/logging.h" #include "games/iidx/iidx.h" @@ -9,7 +10,7 @@ namespace overlay::windows { ExitPrompt::ExitPrompt(SpiceOverlay *overlay) : Window(overlay) { - this->title = "spice2x"; + this->title = "spice2x (" + to_string(VERSION_STRING_CFG) + ")"; this->init_size = ImVec2( (ImGui::GetFontSize() * 14) + (ImGui::GetStyle().ItemSpacing.x * 2), overlay::apply_scaling(120)); @@ -20,22 +21,35 @@ namespace overlay::windows { this->flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize - | ImGuiWindowFlags_NoDocking; + | ImGuiWindowFlags_NoDocking + | ImGuiWindowFlags_NoMove; + + // the menu renders itself as a popup, not a normal overlay window + this->draws_window = false; + } + + void ExitPrompt::update() { + Window::update(); + + // allow the popup to be reopened the next time the menu is shown + if (!this->active) { + this->popup_opened = false; + } } void ExitPrompt::build_button( - Window *window, std::string label, const ImVec2 &size, NextItem next, bool is_toggle) { + Window *window, std::string label, const ImVec2 &size, NextItem next) { if (window == nullptr) { return; } if (ImGui::Button(label.c_str(), size)) { - if (is_toggle) { - window->toggle_active(); - } else { - window->set_active(true); - this->set_active(false); + window->toggle_active(); + + // raise to the top when the user toggled it visible from the menu + if (window->get_active()) { + window->bring_to_front(); } } if (next == NextItem::NEW_LINE) { @@ -46,6 +60,31 @@ namespace overlay::windows { } void ExitPrompt::build_content() { + // use ### so the visible label is the full title (with version) while the + // popup keeps a stable identifier regardless of the title text + const std::string popup_id_str = this->title + "###mainmenu"; + const char *popup_id = popup_id_str.c_str(); + + // open the popup once when the menu becomes active; popup_opened is reset + // in update() while inactive so it reopens the next time it is shown + if (!this->popup_opened) { + ImGui::OpenPopup(popup_id); + this->popup_opened = true; + } + + // position at the top center of the screen + const ImVec2 menu_pos( + ImGui::GetIO().DisplaySize.x / 2 - this->init_size.x / 2, + overlay::apply_scaling(10)); + ImGui::SetNextWindowPos(menu_pos, ImGuiCond_Appearing); + + bool open = true; + if (!ImGui::BeginPopupModal(popup_id, &open, this->flags)) { + // dismissed via escape - close the menu + this->set_active(false); + return; + } + const ImVec2 size(ImGui::GetFontSize() * 14, ImGui::GetFontSize() * 1.9f); const ImVec2 size_half( (size.x - ImGui::GetStyle().ItemSpacing.x) / 2, @@ -54,11 +93,7 @@ namespace overlay::windows { (size.x - (ImGui::GetStyle().ItemSpacing.x * 2)) / 3, ImGui::GetFontSize() * 2.5f); - if (ImGui::Button("Hide overlay", size)) { - overlay::OVERLAY->set_active(false); - } - ImGui::Spacing(); - build_button(this->overlay->window_config, "Show Config", size, NextItem::NEW_LINE, false); + build_button(this->overlay->window_config, "Options", size, NextItem::NEW_LINE); std::string sub = "Show Subscreen"; if (avs::game::is_model("LDJ")) { @@ -77,11 +112,12 @@ namespace overlay::windows { sub = "Show Pop'n Subscreen"; } - build_button(this->overlay->window_sub, sub, size, NextItem::NEW_LINE, false); + build_button(this->overlay->window_sub, sub, size, NextItem::NEW_LINE); ImGui::TextDisabled("Graphics"); build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE); - build_button(this->overlay->window_fps, "FPS", size_half, NextItem::SAME_LINE); + + build_button(this->overlay->window_fps.get(), "FPS", size_half, NextItem::SAME_LINE); build_button(this->overlay->window_resize, "Resize", size_half, NextItem::NEW_LINE); ImGui::TextDisabled("I/O"); @@ -157,5 +193,12 @@ namespace overlay::windows { ImGui::EndPopup(); } ImGui::Spacing(); + + ImGui::EndPopup(); + + // title-bar X was clicked - close the menu + if (!open) { + this->set_active(false); + } } } diff --git a/src/spice2x/overlay/windows/exitprompt.h b/src/spice2x/overlay/windows/exitprompt.h index ed3e6b9..01d7aa1 100644 --- a/src/spice2x/overlay/windows/exitprompt.h +++ b/src/spice2x/overlay/windows/exitprompt.h @@ -14,8 +14,13 @@ namespace overlay::windows { public: ExitPrompt(SpiceOverlay *overlay); void build_content() override; + void update() override; private: - void build_button(Window *window, std::string label, const ImVec2 &size, NextItem next, bool is_toggle=true); + void build_button(Window *window, std::string label, const ImVec2 &size, NextItem next); + + // latch so the popup is opened once per activation; reset in update() + // whenever the menu is inactive + bool popup_opened = false; }; } diff --git a/src/spice2x/overlay/windows/fps.cpp b/src/spice2x/overlay/windows/fps.cpp index 42e05cf..36e6ad2 100644 --- a/src/spice2x/overlay/windows/fps.cpp +++ b/src/spice2x/overlay/windows/fps.cpp @@ -1,58 +1,124 @@ -#include -#include +#include #include "external/fmt/include/fmt/chrono.h" #include "fps.h" namespace overlay::windows { + // tighter internal cell padding than the imgui default (4, 2) + static const ImVec2 FPS_CELL_PADDING(4.0f, 1.0f); + + // tighter window padding than the imgui default (8, 8) + static const ImVec2 FPS_WINDOW_PADDING(6.0f, 4.0f); + FPS::FPS(SpiceOverlay *overlay) : Window(overlay) { this->title = "Stats"; this->flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse - | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing + | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoNavInputs + | ImGuiWindowFlags_NoNav + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDocking; this->bg_alpha = 0.5f; + this->window_padding = FPS_WINDOW_PADDING; this->start_time = std::chrono::floor(std::chrono::system_clock::now()); } void FPS::calculate_initial_window() { - // width is 114x82 px with window decoration, 98x47 for the content - int pos_x = - overlay::FPS_SHOULD_FLIP ? - overlay::apply_scaling(8) : - ImGui::GetIO().DisplaySize.x - overlay::apply_scaling(122); - this->init_pos = ImVec2(pos_x, overlay::apply_scaling(8)); + // size the window explicitly (no AlwaysAutoResize) so the corner anchoring + // below is exact; the footprint mirrors the fixed-fit table in build_content() + const float line_h = ImGui::GetTextLineHeight(); + const int rows = 3; + + // widest label and widest value drive the two fixed-fit columns + const float label_w = (std::max)( + ImGui::CalcTextSize("Time").x, + ImGui::CalcTextSize("Game").x); + const float value_w = ImGui::CalcTextSize("00:00:00").x; + + const float win_w = label_w + value_w + + FPS_CELL_PADDING.x * 2 + + FPS_WINDOW_PADDING.x * 2; + const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * rows + + FPS_WINDOW_PADDING.y * 2; + this->init_size = ImVec2(win_w, win_h); + + // bottom-anchored windows use a larger edge margin (matching notification + // toasts) since they overlap the same on-screen UI; other edges hug closer + const float edge_margin = overlay::apply_scaling(4); + const float bottom_margin = overlay::apply_scaling(20); + const ImVec2 &display = ImGui::GetIO().DisplaySize; + + const bool right = + overlay::FPS_LOCATION == overlay::FpsLocation::TopRight || + overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight; + const bool bottom = + overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft || + overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight; + + const float pos_x = right ? display.x - win_w - edge_margin : edge_margin; + const float pos_y = bottom ? display.y - win_h - bottom_margin : edge_margin; + this->init_pos = ImVec2(pos_x, pos_y); } void FPS::build_content() { - // frame timers ImGuiIO &io = ImGui::GetIO(); - ImGui::Text("FPS: %.1f", io.Framerate); - // ImGui::Text("FT: %.2fms", 1000 / io.Framerate); const auto now = std::chrono::system_clock::now(); const auto now_s = std::chrono::floor(now); - // current time - { - const std::time_t tt = std::chrono::system_clock::to_time_t(now_s); - std::tm local_tm{}; - localtime_s(&local_tm, &tt); - ImGui::TextUnformatted(fmt::format("Time: {:%H:%M:%S}", local_tm).c_str()); - } + const std::time_t tt = std::chrono::system_clock::to_time_t(now_s); + std::tm local_tm{}; + localtime_s(&local_tm, &tt); - // elapsed time - { - const auto uptime = now_s - this->start_time; + const auto uptime = now_s - this->start_time; + + // right-align a label within the current cell so the label column reads + // flush against the value column instead of looking ragged. the label is + // only slightly dimmer than normal text (not the much darker "disabled" tone) + const ImGuiStyle &style = ImGui::GetStyle(); + ImVec4 label_col = style.Colors[ImGuiCol_Text]; + label_col.w *= 0.7f; + const auto label = [&label_col](const char *text) { + const float avail = ImGui::GetContentRegionAvail().x; + const float text_w = ImGui::CalcTextSize(text).x; + if (avail > text_w) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail - text_w)); + } + ImGui::TextColored(label_col, "%s", text); + }; + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, FPS_CELL_PADDING); + if (ImGui::BeginTable("##fps_stats", 2, ImGuiTableFlags_SizingFixedFit)) { + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + label("FPS"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.2f", io.Framerate); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + label("Time"); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(fmt::format("{:%H:%M:%S}", local_tm).c_str()); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + label("Game"); + ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted( - fmt::format("Up: {:%H:%M:%S}", + fmt::format("{:%H:%M:%S}", std::chrono::floor(uptime)).c_str()); + + ImGui::EndTable(); } + ImGui::PopStyleVar(); } } diff --git a/src/spice2x/overlay/windows/fps.h b/src/spice2x/overlay/windows/fps.h index 98500e9..67d74cd 100644 --- a/src/spice2x/overlay/windows/fps.h +++ b/src/spice2x/overlay/windows/fps.h @@ -15,7 +15,5 @@ namespace overlay::windows { void calculate_initial_window() override; void build_content() override; - - bool should_flip = false; }; }