rexglue-skate3/src/ui/overlay/simple_settings_overlay.cpp
2026-06-04 18:36:22 +01:00

495 lines
17 KiB
C++

/**
* @file ui/overlay/simple_settings_overlay.cpp
*
* @brief Curated user-facing settings overlay.
*/
#include <rex/ui/overlay/simple_settings_overlay.h>
#include <algorithm>
#include <array>
#include <string>
#include <vector>
#include <imgui.h>
#include <rex/cvar.h>
#include <toml++/toml.hpp>
namespace rex::ui {
namespace {
constexpr std::array<int32_t, 3> kResolutionScales = {1, 2, 3};
constexpr std::array<const char*, 3> kResolutionLabels = {"720p", "1440p", "2160p"};
constexpr std::array<double, 6> kFrameCapRates = {60.0, 90.0, 120.0, 144.0, 165.0, 240.0};
constexpr std::array<const char*, 7> kFrameCapLabels = {"Unlimited", "60 FPS", "90 FPS",
"120 FPS", "144 FPS", "165 FPS",
"240 FPS"};
constexpr std::array<std::string_view, 7> kCoreSimpleSettingsCvars = {
"resolution_scale",
"draw_resolution_scale_x",
"draw_resolution_scale_y",
"fullscreen",
"vsync",
"mnk_mode",
"mnk_capture_mouse"};
bool HasCvar(std::string_view name) {
return rex::cvar::GetFlagInfo(name) != nullptr;
}
std::vector<std::string_view> GetSimpleSettingsCvars() {
std::vector<std::string_view> cvars;
cvars.reserve(14);
cvars.insert(cvars.end(), kCoreSimpleSettingsCvars.begin(), kCoreSimpleSettingsCvars.end());
if (HasCvar("d3d12_present_frame_limiter")) {
cvars.push_back("d3d12_present_frame_limiter");
}
if (HasCvar("d3d12_present_frame_limiter_fps")) {
cvars.push_back("d3d12_present_frame_limiter_fps");
}
if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) {
cvars.push_back("d3d12_allow_variable_refresh_rate_and_tearing");
}
if (HasCvar("vulkan_allow_present_mode_immediate")) {
cvars.push_back("vulkan_allow_present_mode_immediate");
}
if (HasCvar("vulkan_allow_present_mode_mailbox")) {
cvars.push_back("vulkan_allow_present_mode_mailbox");
}
if (HasCvar("vulkan_allow_present_mode_fifo_relaxed")) {
cvars.push_back("vulkan_allow_present_mode_fifo_relaxed");
}
return cvars;
}
int ResolutionIndexFromCvar() {
int32_t current = rex::cvar::Query<int32_t>("resolution_scale");
int best = 0;
int32_t best_delta = 1000;
for (int i = 0; i < static_cast<int>(kResolutionScales.size()); ++i) {
int32_t delta = kResolutionScales[i] > current ? kResolutionScales[i] - current
: current - kResolutionScales[i];
if (delta < best_delta) {
best = i;
best_delta = delta;
}
}
return best;
}
bool HasFrameCapCvars() {
return HasCvar("d3d12_present_frame_limiter") && HasCvar("d3d12_present_frame_limiter_fps");
}
int FrameCapIndexFromCvar() {
if (!HasFrameCapCvars()) {
return 0;
}
if (!rex::cvar::Query<bool>("d3d12_present_frame_limiter")) {
return 0;
}
double current = rex::cvar::Query<double>("d3d12_present_frame_limiter_fps");
int best = 1;
double best_delta = 1000.0;
for (int i = 0; i < static_cast<int>(kFrameCapRates.size()); ++i) {
double delta = std::abs(kFrameCapRates[i] - current);
if (delta < best_delta) {
best = i + 1;
best_delta = delta;
}
}
return best;
}
void CopyToBuffer(char* buffer, size_t buffer_size, const std::string& value) {
size_t count = std::min(value.size(), buffer_size - 1);
std::copy_n(value.data(), count, buffer);
buffer[count] = '\0';
}
void SetBoolCvar(std::string_view name, bool value) {
rex::cvar::SetFlagByName(name, value ? "true" : "false");
}
bool TearingFromCvar() {
if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) {
return rex::cvar::Query<bool>("d3d12_allow_variable_refresh_rate_and_tearing");
}
if (HasCvar("vulkan_allow_present_mode_immediate")) {
return rex::cvar::Query<bool>("vulkan_allow_present_mode_immediate");
}
return false;
}
void SetTearingCvars(bool value) {
if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) {
SetBoolCvar("d3d12_allow_variable_refresh_rate_and_tearing", value);
}
if (HasCvar("vulkan_allow_present_mode_immediate")) {
SetBoolCvar("vulkan_allow_present_mode_immediate", value);
}
if (HasCvar("vulkan_allow_present_mode_mailbox")) {
SetBoolCvar("vulkan_allow_present_mode_mailbox", value);
}
if (HasCvar("vulkan_allow_present_mode_fifo_relaxed")) {
SetBoolCvar("vulkan_allow_present_mode_fifo_relaxed", value);
}
}
void SectionTitle(const char* label) {
ImGui::PushFont(nullptr, 20.0f);
ImGui::TextUnformatted(label);
ImGui::PopFont();
ImGui::Spacing();
}
void BeginFieldRow(const char* label) {
ImGui::PushID(label);
ImGui::TextUnformatted(label);
ImGui::SameLine(260.0f);
ImGui::SetNextItemWidth(-1.0f);
}
void EndFieldRow() {
ImGui::PopID();
ImGui::Spacing();
}
bool PrimaryButton(const char* label) {
return ImGui::Button(label, ImVec2(170.0f, 42.0f));
}
} // namespace
void SaveSimpleSettingsConfig(const std::filesystem::path& config_path) {
rex::cvar::SaveConfigValues(config_path, GetSimpleSettingsCvars());
}
void EnsureSimpleSettingsConfig(const std::filesystem::path& config_path) {
bool should_save = !std::filesystem::exists(config_path);
if (!should_save) {
try {
auto config = toml::parse_file(config_path.string());
for (std::string_view name : GetSimpleSettingsCvars()) {
if (!config.contains(name)) {
should_save = true;
break;
}
}
} catch (const toml::parse_error&) {
should_save = true;
}
}
if (should_save) {
SaveSimpleSettingsConfig(config_path);
}
}
SimpleSettingsDialog::SimpleSettingsDialog(ImGuiDrawer* drawer, std::filesystem::path config_path,
LoadProfilesCallback load_profiles,
SaveProfileCallback save_profile,
CloseSettingsCallback close_settings,
CloseGameCallback close_game,
RestartGameCallback restart_game)
: ImGuiDialog(drawer),
config_path_(std::move(config_path)),
load_profiles_(std::move(load_profiles)),
save_profile_(std::move(save_profile)),
close_settings_(std::move(close_settings)),
close_game_(std::move(close_game)),
restart_game_(std::move(restart_game)) {
ReloadProfiles();
LoadSettingsFromCvars();
SetDrawActive(false);
}
SimpleSettingsDialog::~SimpleSettingsDialog() = default;
void SimpleSettingsDialog::Show() {
visible_ = true;
SetDrawActive(true);
ReloadProfiles();
LoadSettingsFromCvars();
}
void SimpleSettingsDialog::LoadSettingsFromCvars() {
resolution_scale_index_ = ResolutionIndexFromCvar();
frame_cap_index_ = FrameCapIndexFromCvar();
fullscreen_ = rex::cvar::Query<bool>("fullscreen");
vsync_ = rex::cvar::Query<bool>("vsync");
tearing_ = TearingFromCvar();
mnk_mode_ = rex::cvar::Query<bool>("mnk_mode");
mnk_capture_mouse_ = rex::cvar::Query<bool>("mnk_capture_mouse");
}
bool SimpleSettingsDialog::HasSettingsChanges() const {
return resolution_scale_index_ != ResolutionIndexFromCvar() ||
frame_cap_index_ != FrameCapIndexFromCvar() ||
fullscreen_ != rex::cvar::Query<bool>("fullscreen") ||
vsync_ != rex::cvar::Query<bool>("vsync") ||
tearing_ != TearingFromCvar() ||
mnk_mode_ != rex::cvar::Query<bool>("mnk_mode") ||
mnk_capture_mouse_ != rex::cvar::Query<bool>("mnk_capture_mouse");
}
void SimpleSettingsDialog::Toggle() {
if (visible_) {
Hide();
return;
}
Show();
}
void SimpleSettingsDialog::Hide() {
if (!visible_) {
return;
}
visible_ = false;
SetDrawActive(false);
if (close_settings_) {
close_settings_();
}
}
void SimpleSettingsDialog::ReloadProfiles() {
profiles_ = load_profiles_ ? load_profiles_() : SimpleProfileState{};
if (profiles_.profiles.empty()) {
profiles_.profiles.push_back({"default", "Player"});
profiles_.selected_index = 0;
}
profiles_.selected_index =
std::clamp(profiles_.selected_index, 0, static_cast<int>(profiles_.profiles.size()) - 1);
CopyToBuffer(gamertag_buf_, sizeof(gamertag_buf_),
profiles_.profiles[profiles_.selected_index].gamertag);
profile_signed_in_ = profiles_.profiles[profiles_.selected_index].signed_in;
}
void SimpleSettingsDialog::SaveVideo() {
resolution_scale_index_ =
std::clamp(resolution_scale_index_, 0, static_cast<int>(kResolutionScales.size()) - 1);
frame_cap_index_ =
std::clamp(frame_cap_index_, 0, static_cast<int>(kFrameCapLabels.size()) - 1);
const auto scale = std::to_string(kResolutionScales[resolution_scale_index_]);
rex::cvar::SetFlagByName("resolution_scale", scale);
rex::cvar::SetFlagByName("draw_resolution_scale_x", scale);
rex::cvar::SetFlagByName("draw_resolution_scale_y", scale);
if (HasFrameCapCvars()) {
SetBoolCvar("d3d12_present_frame_limiter", frame_cap_index_ != 0);
}
if (HasFrameCapCvars() && frame_cap_index_ != 0) {
rex::cvar::SetFlagByName("d3d12_present_frame_limiter_fps",
std::to_string(kFrameCapRates[frame_cap_index_ - 1]));
}
SetBoolCvar("fullscreen", fullscreen_);
SetBoolCvar("vsync", vsync_);
SetTearingCvars(tearing_);
SetBoolCvar("mnk_mode", mnk_mode_);
SetBoolCvar("mnk_capture_mouse", mnk_capture_mouse_);
SaveSimpleSettingsConfig(config_path_);
}
void SimpleSettingsDialog::SaveProfile() {
if (save_profile_) {
save_profile_(profiles_.selected_index, gamertag_buf_, profile_signed_in_);
}
ReloadProfiles();
}
void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
if (!visible_) {
return;
}
const float window_width =
std::max(420.0f, std::min(820.0f, std::max(0.0f, io.DisplaySize.x - 48.0f)));
const float window_height =
std::max(560.0f, std::min(900.0f, std::max(0.0f, io.DisplaySize.y - 16.0f)));
ImGui::PushFont(nullptr, 18.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 22.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(14.0f, 9.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f, 12.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.045f, 0.055f, 0.065f, 0.96f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.070f, 0.082f, 0.095f, 0.95f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.25f, 0.32f, 0.38f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(0.060f, 0.070f, 0.080f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.075f, 0.095f, 0.110f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.13f, 0.15f, 0.17f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.18f, 0.22f, 0.25f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.22f, 0.28f, 0.31f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.38f, 0.42f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.49f, 0.54f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.10f, 0.32f, 0.36f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.12f, 0.38f, 0.42f, 0.72f));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.15f, 0.49f, 0.54f, 0.88f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.17f, 0.56f, 0.62f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.52f, 0.88f, 0.82f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(0.52f, 0.88f, 0.82f, 0.86f));
ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, ImVec4(0.66f, 1.00f, 0.92f, 1.00f));
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(0.96f);
if (!ImGui::Begin("Game Settings", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
ImGui::End();
ImGui::PopStyleColor(17);
ImGui::PopStyleVar(7);
ImGui::PopFont();
return;
}
ImGui::PushFont(nullptr, 22.0f);
ImGui::TextUnformatted("Settings");
ImGui::PopFont();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Close Settings", ImVec2(170.0f, 40.0f))) {
Hide();
ImGui::End();
ImGui::PopStyleColor(17);
ImGui::PopStyleVar(7);
ImGui::PopFont();
return;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.48f, 0.12f, 0.10f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.62f, 0.16f, 0.13f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.08f, 0.07f, 1.00f));
if (ImGui::Button("Close Game", ImVec2(170.0f, 40.0f))) {
if (close_game_) {
close_game_();
}
}
ImGui::PopStyleColor(3);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::BeginChild("##simple_settings_nav", ImVec2(180.0f, 0.0f), true);
const char* tabs[] = {"Video", "Controls", "Profile"};
for (int i = 0; i < 3; ++i) {
if (ImGui::Selectable(tabs[i], selected_tab_ == i, 0, ImVec2(0.0f, 42.0f))) {
selected_tab_ = i;
}
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("##simple_settings_body", ImVec2(0.0f, 0.0f), true,
ImGuiWindowFlags_NoScrollbar);
const bool has_settings_changes = HasSettingsChanges();
if (selected_tab_ == 0) {
SectionTitle("Video");
BeginFieldRow("Resolution scale");
ImGui::Combo("##resolution_scale", &resolution_scale_index_, kResolutionLabels.data(),
static_cast<int>(kResolutionLabels.size()));
EndFieldRow();
if (HasFrameCapCvars()) {
BeginFieldRow("Framerate Cap");
ImGui::Combo("##frame_cap", &frame_cap_index_, kFrameCapLabels.data(),
static_cast<int>(kFrameCapLabels.size()));
EndFieldRow();
}
ImGui::Separator();
ImGui::Spacing();
ImGui::Checkbox("Fullscreen", &fullscreen_);
ImGui::Checkbox("VSync", &vsync_);
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.95f, 0.28f, 0.24f, 1.0f), "not recommended");
ImGui::Checkbox("Variable refresh / tearing", &tearing_);
ImGui::Dummy(ImVec2(0.0f, 8.0f));
if (has_settings_changes) {
ImGui::TextColored(ImVec4(0.95f, 0.76f, 0.30f, 1.0f),
"Restart required to apply these changes.");
if (PrimaryButton("Apply & Restart")) {
SaveVideo();
if (restart_game_) {
restart_game_();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel Changes", ImVec2(170.0f, 42.0f))) {
LoadSettingsFromCvars();
}
} else {
ImGui::TextDisabled("No pending changes");
}
} else if (selected_tab_ == 1) {
SectionTitle("Controls");
ImGui::Checkbox("Mouse and keyboard mode", &mnk_mode_);
ImGui::Checkbox("Capture mouse", &mnk_capture_mouse_);
ImGui::Dummy(ImVec2(0.0f, 8.0f));
if (has_settings_changes) {
ImGui::TextColored(ImVec4(0.95f, 0.76f, 0.30f, 1.0f),
"Restart required to apply these changes.");
if (PrimaryButton("Apply & Restart")) {
SaveVideo();
if (restart_game_) {
restart_game_();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel Changes", ImVec2(170.0f, 42.0f))) {
LoadSettingsFromCvars();
}
} else {
ImGui::TextDisabled("No pending changes");
}
} else {
SectionTitle("Profile");
std::vector<const char*> names;
names.reserve(profiles_.profiles.size());
for (const auto& profile : profiles_.profiles) {
names.push_back(profile.gamertag.c_str());
}
BeginFieldRow("Local profile");
if (ImGui::Combo("##profile", &profiles_.selected_index, names.data(),
static_cast<int>(names.size()))) {
CopyToBuffer(gamertag_buf_, sizeof(gamertag_buf_),
profiles_.profiles[profiles_.selected_index].gamertag);
profile_signed_in_ = profiles_.profiles[profiles_.selected_index].signed_in;
}
EndFieldRow();
BeginFieldRow("Gamertag");
ImGui::InputText("##gamertag", gamertag_buf_, sizeof(gamertag_buf_));
EndFieldRow();
BeginFieldRow("Local sign-in");
const char* sign_in_labels[] = {"Signed out", "Signed in"};
int sign_in_index = profile_signed_in_ ? 1 : 0;
if (ImGui::Combo("##signed_in", &sign_in_index, sign_in_labels, 2)) {
profile_signed_in_ = sign_in_index != 0;
}
EndFieldRow();
ImGui::Dummy(ImVec2(0.0f, 8.0f));
if (PrimaryButton("Save Profile")) {
SaveProfile();
}
}
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleColor(17);
ImGui::PopStyleVar(7);
ImGui::PopFont();
}
} // namespace rex::ui