Files
spice2x.github.io/src/spice2x/overlay/windows/fps.cpp
T
bicarus 100caa0f5c 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
2026-06-07 17:23:08 -07:00

125 lines
5.0 KiB
C++

#include <algorithm>
#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_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::seconds>(std::chrono::system_clock::now());
}
void FPS::calculate_initial_window() {
// 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() {
ImGuiIO &io = ImGui::GetIO();
const auto now = std::chrono::system_clock::now();
const auto now_s = std::chrono::floor<std::chrono::seconds>(now);
const std::time_t tt = std::chrono::system_clock::to_time_t(now_s);
std::tm local_tm{};
localtime_s(&local_tm, &tt);
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("{:%H:%M:%S}",
std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
ImGui::EndTable();
}
ImGui::PopStyleVar();
}
}