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
+48 -7
View File
@@ -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