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
This commit is contained in:
bicarus
2026-06-07 17:23:08 -07:00
committed by GitHub
parent bb87aa2944
commit 100caa0f5c
16 changed files with 348 additions and 102 deletions
-1
View File
@@ -93,7 +93,6 @@ namespace cfg {
overlay::create_software(this->wnd.hWnd); overlay::create_software(this->wnd.hWnd);
} }
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false; ImGui::GetIO().MouseDrawCursor = false;
// run window // run window
+3 -1
View File
@@ -444,6 +444,8 @@ namespace games {
// overlay button definitions // overlay button definitions
names.emplace_back("Screenshot"); names.emplace_back("Screenshot");
vkey_defaults.push_back(VK_SNAPSHOT); 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"); names.emplace_back("Toggle Main Menu");
vkey_defaults.push_back(VK_ESCAPE); vkey_defaults.push_back(VK_ESCAPE);
names.emplace_back("Toggle Sub Screen"); names.emplace_back("Toggle Sub Screen");
@@ -468,7 +470,7 @@ namespace games {
vkey_defaults.push_back(VK_F10); vkey_defaults.push_back(VK_F10);
names.emplace_back("Toggle Screen Resize"); names.emplace_back("Toggle Screen Resize");
vkey_defaults.push_back(VK_F11); vkey_defaults.push_back(VK_F11);
names.emplace_back("Toggle Overlay"); names.emplace_back("Toggle FPS");
vkey_defaults.push_back(VK_F12); vkey_defaults.push_back(VK_F12);
names.emplace_back("Toggle Camera Control"); names.emplace_back("Toggle Camera Control");
vkey_defaults.push_back(0xFF); vkey_defaults.push_back(0xFF);
+2 -1
View File
@@ -8,6 +8,7 @@ namespace games {
namespace OverlayButtons { namespace OverlayButtons {
enum { enum {
Screenshot, Screenshot,
ToggleAllWindows,
ToggleMainMenu, ToggleMainMenu,
ToggleSubScreen, ToggleSubScreen,
InsertCoin, InsertCoin,
@@ -20,7 +21,7 @@ namespace games {
ToggleControl, ToggleControl,
TogglePatchManager, TogglePatchManager,
ToggleScreenResize, ToggleScreenResize,
ToggleOverlay, ToggleFps,
ToggleCameraControl, ToggleCameraControl,
TriggerPinMacroP1, TriggerPinMacroP1,
TriggerPinMacroP2, TriggerPinMacroP2,
+14 -1
View File
@@ -1182,7 +1182,20 @@ int main_implementation(int argc, char *argv[]) {
overlay::AUTO_SHOW_FPS = true; overlay::AUTO_SHOW_FPS = true;
} }
if (options[launcher::Options::spice2x_FpsOpposite].value_bool()) { 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()) { if (options[launcher::Options::spice2x_SubScreenAutoShow].value_bool()) {
overlay::AUTO_SHOW_SUBSCREEN = true; overlay::AUTO_SHOW_SUBSCREEN = true;
+18 -2
View File
@@ -496,12 +496,28 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_FpsOpposite // spice2x_FpsOpposite
.title = "Show FPS/Clock top-left", .title = "Show FPS/Clock top-left (DEPRECATED - use -fpslocation instead)",
.name = "fpsflip", .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, .type = OptionType::Bool,
.hidden = true,
.category = "Overlay", .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 // spice2x_SubScreenAutoShow
.title = "Auto Show Subscreen", .title = "Auto Show Subscreen",
+1
View File
@@ -56,6 +56,7 @@ namespace launcher {
NotificationPosition, NotificationPosition,
spice2x_FpsAutoShow, spice2x_FpsAutoShow,
spice2x_FpsOpposite, spice2x_FpsOpposite,
FpsLocation,
spice2x_SubScreenAutoShow, spice2x_SubScreenAutoShow,
spice2x_IOPanelAutoShow, spice2x_IOPanelAutoShow,
spice2x_KeypadAutoShow, spice2x_KeypadAutoShow,
+11
View File
@@ -146,6 +146,17 @@ namespace overlay::notifications {
float height = 0.f; float height = 0.f;
if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) { 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_pos = ImGui::GetWindowPos();
const ImVec2 win_size = ImGui::GetWindowSize(); const ImVec2 win_size = ImGui::GetWindowSize();
+54 -25
View File
@@ -66,7 +66,7 @@ namespace overlay {
bool AUTO_SHOW_KEYPAD_P1 = false; bool AUTO_SHOW_KEYPAD_P1 = false;
bool AUTO_SHOW_KEYPAD_P2 = false; bool AUTO_SHOW_KEYPAD_P2 = false;
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false; bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
bool FPS_SHOULD_FLIP = false; FpsLocation FPS_LOCATION = FpsLocation::TopRight;
std::optional<uint32_t> UI_SCALE_PERCENT; std::optional<uint32_t> UI_SCALE_PERCENT;
// global // global
@@ -298,6 +298,7 @@ void overlay::SpiceOverlay::init() {
colors[ImGuiCol_Separator] = ImVec4(0.32f, 0.22f, 0.22f, 1.00f); 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_SeparatorHovered] = ImVec4(0.42f, 0.22f, 0.22f, 1.00f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.52f, 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 #ifdef IMGUI_HAS_DOCK
colors[ImGuiCol_DockingPreview] = ImVec4(0.85f, 0.15f, 0.15f, 0.40f); 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; bool set_overlay_active = false;
// referenced windows // owned separately from `windows` so it never affects overlay activation/input gating
this->window_add(window_fps = new overlay::windows::FPS(this)); window_fps = std::make_unique<overlay::windows::FPS>(this);
if (!cfg::CONFIGURATOR_STANDALONE && AUTO_SHOW_FPS) { if (!cfg::CONFIGURATOR_STANDALONE && AUTO_SHOW_FPS) {
window_fps->set_active(true); 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<overlay::windows::ExitPrompt>(this);
// add default windows // add default windows
this->window_add(window_config = new overlay::windows::Config(this)); 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 const bool draw_notifications = this->renderer != OverlayRenderer::SOFTWARE
&& overlay::notifications::has_pending(); && 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 // check if there is nothing to draw
this->has_pending_frame = false; this->has_pending_frame = false;
if (!this->active && !draw_notifications) { if (!this->active && !draw_notifications && !draw_fps_persistent) {
return; return;
} }
@@ -577,11 +582,17 @@ void overlay::SpiceOverlay::new_frame() {
window->build(); window->build();
} }
// draw the main menu on top of the overlay windows
this->window_main_menu->build();
if (SHOW_DEBUG_LOG_WINDOW) { if (SHOW_DEBUG_LOG_WINDOW) {
ImGui::ShowDebugLogWindow(&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 // draw notifications last so they paint on top of any overlay windows
if (draw_notifications) { if (draw_notifications) {
overlay::notifications::draw(); overlay::notifications::draw();
@@ -750,13 +761,19 @@ void overlay::SpiceOverlay::d3d9_render_draw(const bool force_submit) {
void overlay::SpiceOverlay::update() { 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()); 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() && 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) { if (toggle_down_new && !this->toggle_down) {
toggle_active(true); toggle_active();
} }
this->toggle_down = toggle_down_new; this->toggle_down = toggle_down_new;
@@ -769,17 +786,35 @@ void overlay::SpiceOverlay::update() {
} }
this->main_menu_down = main_menu_down_new; 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 // update windows
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->update(); window->update();
} }
// deactivate if no windows are shown // FPS window
bool window_active = false; this->window_fps->update();
for (auto &window : this->windows) {
if (window->get_active()) { // main menu (owned separately from the overlay window layer)
window_active = true; this->window_main_menu->update();
break;
// 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) { if (!window_active) {
@@ -791,7 +826,7 @@ bool overlay::SpiceOverlay::update_cursor() {
return ImGui_ImplSpice_UpdateMouseCursor(); return ImGui_ImplSpice_UpdateMouseCursor();
} }
void overlay::SpiceOverlay::toggle_active(bool overlay_key) { void overlay::SpiceOverlay::toggle_active() {
// invert active state // invert active state
this->active = !this->active; this->active = !this->active;
@@ -800,11 +835,6 @@ void overlay::SpiceOverlay::toggle_active(bool overlay_key) {
if (this->window_main_menu) { if (this->window_main_menu) {
this->window_main_menu->set_active(false); 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() { void overlay::SpiceOverlay::show_main_menu() {
@@ -847,9 +877,8 @@ bool overlay::SpiceOverlay::has_focus() {
} }
bool overlay::SpiceOverlay::hotkeys_triggered() { bool overlay::SpiceOverlay::hotkeys_triggered() {
// prevent hotkeys in spicecfg
// check if disabled first if (cfg::CONFIGURATOR_STANDALONE) {
if (!this->hotkeys_enable) {
return false; return false;
} }
+20 -6
View File
@@ -29,6 +29,14 @@ namespace overlay {
SOFTWARE, SOFTWARE,
}; };
// corner of the screen the FPS / clock / timer window is anchored to
enum class FpsLocation {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};
// settings // settings
extern bool ENABLED; extern bool ENABLED;
extern bool AUTO_SHOW_FPS; extern bool AUTO_SHOW_FPS;
@@ -37,7 +45,7 @@ namespace overlay {
extern bool AUTO_SHOW_KEYPAD_P1; extern bool AUTO_SHOW_KEYPAD_P1;
extern bool AUTO_SHOW_KEYPAD_P2; extern bool AUTO_SHOW_KEYPAD_P2;
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT; 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 bool SHOW_DEBUG_LOG_WINDOW;
extern std::optional<uint32_t> UI_SCALE_PERCENT; extern std::optional<uint32_t> UI_SCALE_PERCENT;
@@ -57,10 +65,8 @@ namespace overlay {
D3DDEVICE_CREATION_PARAMETERS creation_parameters {}; D3DDEVICE_CREATION_PARAMETERS creation_parameters {};
D3DADAPTER_IDENTIFIER9 adapter_identifier {}; D3DADAPTER_IDENTIFIER9 adapter_identifier {};
bool hotkeys_enable = true;
// windows // windows
Window *window_fps = nullptr;
Window *window_iopanel = nullptr; Window *window_iopanel = nullptr;
Window *window_config = nullptr; Window *window_config = nullptr;
Window *window_keypad1 = nullptr; Window *window_keypad1 = nullptr;
@@ -72,6 +78,15 @@ namespace overlay {
Window *window_sub = nullptr; Window *window_sub = nullptr;
Window *window_log = 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> 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> window_main_menu;
explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device); explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device);
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
explicit SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device, 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(). // after render() when the frame is being presented. In-game overlay draws in render().
void d3d9_render_draw(bool force_submit = false); void d3d9_render_draw(bool force_submit = false);
void update(); void update();
void toggle_active(bool overlay_key = false); void toggle_active();
void show_main_menu(); void show_main_menu();
void set_active(bool active); void set_active(bool active);
bool get_active(); bool get_active();
@@ -182,13 +197,12 @@ namespace overlay {
std::vector<std::unique_ptr<Window>> windows; std::vector<std::unique_ptr<Window>> windows;
Window *window_main_menu = nullptr;
std::function<bool(LONG *, LONG *)> subscreen_mouse_handler = nullptr; std::function<bool(LONG *, LONG *)> subscreen_mouse_handler = nullptr;
bool active = false; bool active = false;
bool toggle_down = false; bool toggle_down = false;
bool main_menu_down = false; bool main_menu_down = false;
bool fps_down = false;
bool hotkey_toggle = false; bool hotkey_toggle = false;
bool hotkey_toggle_last = false; bool hotkey_toggle_last = false;
+48 -7
View File
@@ -3,6 +3,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "games/io.h" #include "games/io.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "external/imgui/imgui_internal.h"
overlay::Window::Window(SpiceOverlay *overlay) : overlay(overlay) { overlay::Window::Window(SpiceOverlay *overlay) : overlay(overlay) {
@@ -35,6 +36,12 @@ void overlay::Window::update() {
} else { } else {
this->toggle_active(); 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; this->toggle_button_state = toggle_button_new;
} }
@@ -81,14 +88,29 @@ void overlay::Window::build() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
} }
// create window const bool custom_window_padding =
if (ImGui::Begin( !this->remove_window_padding && this->window_padding.x >= 0.f;
(this->title + "###" + to_string(this)).c_str(), if (custom_window_padding) {
&this->active, ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, this->window_padding);
this->flags)) { }
// window attributes // create window
this->calculate_initial_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::SetWindowPos(this->init_pos, ImGuiCond_Once);
ImGui::SetWindowSize(this->init_size, ImGuiCond_Once); ImGui::SetWindowSize(this->init_size, ImGuiCond_Once);
@@ -103,12 +125,27 @@ void overlay::Window::build() {
// end window // end window
ImGui::End(); 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) { if (this->remove_window_padding) {
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
} }
if (custom_window_padding) {
ImGui::PopStyleVar();
}
} else { } else {
// add raw content // 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() { bool overlay::Window::get_active() {
// check for active children // check for active children
+13
View File
@@ -24,15 +24,26 @@ namespace overlay {
void toggle_active(); void toggle_active();
void set_active(bool active); void set_active(bool active);
bool get_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: protected:
// state // state
SpiceOverlay *overlay; SpiceOverlay *overlay;
bool active = false; 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<Window*> children; std::vector<Window*> children;
// settings // settings
bool remove_window_padding = false; 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; bool draws_window = true;
ImGuiSizeCallback resize_callback = nullptr; ImGuiSizeCallback resize_callback = nullptr;
std::string title = "Title"; std::string title = "Title";
@@ -41,6 +52,8 @@ namespace overlay {
bool toggle_button_state = false; bool toggle_button_state = false;
// init settings // 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_pos = ImVec2(0, 0);
ImVec2 init_size = ImVec2(0, 0); ImVec2 init_size = ImVec2(0, 0);
ImVec2 size_min = ImVec2(0, 0); ImVec2 size_min = ImVec2(0, 0);
+11 -17
View File
@@ -324,20 +324,12 @@ namespace overlay::windows {
} }
if (ImGui::BeginTabItem("Overlay")) { if (ImGui::BeginTabItem("Overlay")) {
tab_selected_new = ConfigTab::CONFIG_TAB_OVERLAY; tab_selected_new = ConfigTab::CONFIG_TAB_OVERLAY;
const auto offset = cfg::CONFIGURATOR_STANDALONE ? page_offset : page_offset2;
ImGui::BeginChild("Overlay", ImVec2( ImGui::BeginChild("Overlay", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - offset), false); 0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
// overlay buttons // overlay buttons
this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name)); this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name));
ImGui::EndChild(); ImGui::EndChild();
// standalone configurator extras
if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::Checkbox("Enable Overlay in Config", &OVERLAY->hotkeys_enable);
}
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Lights")) { if (ImGui::BeginTabItem("Lights")) {
@@ -4316,7 +4308,7 @@ namespace overlay::windows {
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); 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(); ImGui::Spacing();
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
@@ -4379,14 +4371,16 @@ namespace overlay::windows {
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); 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::Spacing();
ImGui::BeginDisabled(); if (ImGui::Button("Open Card Manager")) {
ImGui::TextWrapped("To debug card reader issues, run spice.exe -cfg in command line and check the log."); if (this->overlay->window_cards != nullptr) {
ImGui::TextWrapped( this->overlay->window_cards->set_active(true);
"If you have multiple players, try opening Card Manager window in the game. " this->overlay->window_cards->bring_to_front();
"Check the key bind in Overlay tab for Toggle Card Manager."); }
ImGui::EndDisabled(); }
ImGui::TextUnformatted("");
ImGui::TextUnformatted("");
} }
bool Config::validate_ea_card(char card_number[16]) { bool Config::validate_ea_card(char card_number[16]) {
+58 -15
View File
@@ -1,5 +1,6 @@
#include "exitprompt.h" #include "exitprompt.h"
#include "avs/game.h" #include "avs/game.h"
#include "build/defs.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
@@ -9,7 +10,7 @@
namespace overlay::windows { namespace overlay::windows {
ExitPrompt::ExitPrompt(SpiceOverlay *overlay) : Window(overlay) { ExitPrompt::ExitPrompt(SpiceOverlay *overlay) : Window(overlay) {
this->title = "spice2x"; this->title = "spice2x (" + to_string(VERSION_STRING_CFG) + ")";
this->init_size = ImVec2( this->init_size = ImVec2(
(ImGui::GetFontSize() * 14) + (ImGui::GetStyle().ItemSpacing.x * 2), (ImGui::GetFontSize() * 14) + (ImGui::GetStyle().ItemSpacing.x * 2),
overlay::apply_scaling(120)); overlay::apply_scaling(120));
@@ -20,22 +21,35 @@ namespace overlay::windows {
this->flags = ImGuiWindowFlags_NoResize this->flags = ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_AlwaysAutoResize | 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( 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) { if (window == nullptr) {
return; return;
} }
if (ImGui::Button(label.c_str(), size)) { if (ImGui::Button(label.c_str(), size)) {
if (is_toggle) { window->toggle_active();
window->toggle_active();
} else { // raise to the top when the user toggled it visible from the menu
window->set_active(true); if (window->get_active()) {
this->set_active(false); window->bring_to_front();
} }
} }
if (next == NextItem::NEW_LINE) { if (next == NextItem::NEW_LINE) {
@@ -46,6 +60,31 @@ namespace overlay::windows {
} }
void ExitPrompt::build_content() { 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(ImGui::GetFontSize() * 14, ImGui::GetFontSize() * 1.9f);
const ImVec2 size_half( const ImVec2 size_half(
(size.x - ImGui::GetStyle().ItemSpacing.x) / 2, (size.x - ImGui::GetStyle().ItemSpacing.x) / 2,
@@ -54,11 +93,7 @@ namespace overlay::windows {
(size.x - (ImGui::GetStyle().ItemSpacing.x * 2)) / 3, (size.x - (ImGui::GetStyle().ItemSpacing.x * 2)) / 3,
ImGui::GetFontSize() * 2.5f); ImGui::GetFontSize() * 2.5f);
if (ImGui::Button("Hide overlay", size)) { build_button(this->overlay->window_config, "Options", size, NextItem::NEW_LINE);
overlay::OVERLAY->set_active(false);
}
ImGui::Spacing();
build_button(this->overlay->window_config, "Show Config", size, NextItem::NEW_LINE, false);
std::string sub = "Show Subscreen"; std::string sub = "Show Subscreen";
if (avs::game::is_model("LDJ")) { if (avs::game::is_model("LDJ")) {
@@ -77,11 +112,12 @@ namespace overlay::windows {
sub = "Show Pop'n Subscreen"; 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"); ImGui::TextDisabled("Graphics");
build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE); 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); build_button(this->overlay->window_resize, "Resize", size_half, NextItem::NEW_LINE);
ImGui::TextDisabled("I/O"); ImGui::TextDisabled("I/O");
@@ -157,5 +193,12 @@ namespace overlay::windows {
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::EndPopup();
// title-bar X was clicked - close the menu
if (!open) {
this->set_active(false);
}
} }
} }
+6 -1
View File
@@ -14,8 +14,13 @@ namespace overlay::windows {
public: public:
ExitPrompt(SpiceOverlay *overlay); ExitPrompt(SpiceOverlay *overlay);
void build_content() override; void build_content() override;
void update() override;
private: 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;
}; };
} }
+89 -23
View File
@@ -1,58 +1,124 @@
#include <iomanip> #include <algorithm>
#include <sstream>
#include "external/fmt/include/fmt/chrono.h" #include "external/fmt/include/fmt/chrono.h"
#include "fps.h" #include "fps.h"
namespace overlay::windows { 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) { FPS::FPS(SpiceOverlay *overlay) : Window(overlay) {
this->title = "Stats"; this->title = "Stats";
this->flags = ImGuiWindowFlags_NoTitleBar this->flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_AlwaysAutoResize
| ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoNavFocus
| ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavInputs
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoInputs
| ImGuiWindowFlags_NoDocking; | ImGuiWindowFlags_NoDocking;
this->bg_alpha = 0.5f; this->bg_alpha = 0.5f;
this->window_padding = FPS_WINDOW_PADDING;
this->start_time = this->start_time =
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now()); std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
} }
void FPS::calculate_initial_window() { void FPS::calculate_initial_window() {
// width is 114x82 px with window decoration, 98x47 for the content // size the window explicitly (no AlwaysAutoResize) so the corner anchoring
int pos_x = // below is exact; the footprint mirrors the fixed-fit table in build_content()
overlay::FPS_SHOULD_FLIP ? const float line_h = ImGui::GetTextLineHeight();
overlay::apply_scaling(8) : const int rows = 3;
ImGui::GetIO().DisplaySize.x - overlay::apply_scaling(122);
this->init_pos = ImVec2(pos_x, overlay::apply_scaling(8)); // 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() { void FPS::build_content() {
// frame timers
ImGuiIO &io = ImGui::GetIO(); 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 = std::chrono::system_clock::now();
const auto now_s = std::chrono::floor<std::chrono::seconds>(now); const auto now_s = std::chrono::floor<std::chrono::seconds>(now);
// current time const std::time_t tt = std::chrono::system_clock::to_time_t(now_s);
{ std::tm local_tm{};
const std::time_t tt = std::chrono::system_clock::to_time_t(now_s); localtime_s(&local_tm, &tt);
std::tm local_tm{};
localtime_s(&local_tm, &tt);
ImGui::TextUnformatted(fmt::format("Time: {:%H:%M:%S}", local_tm).c_str());
}
// 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( ImGui::TextUnformatted(
fmt::format("Up: {:%H:%M:%S}", fmt::format("{:%H:%M:%S}",
std::chrono::floor<std::chrono::seconds>(uptime)).c_str()); std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
ImGui::EndTable();
} }
ImGui::PopStyleVar();
} }
} }
-2
View File
@@ -15,7 +15,5 @@ namespace overlay::windows {
void calculate_initial_window() override; void calculate_initial_window() override;
void build_content() override; void build_content() override;
bool should_flip = false;
}; };
} }