diff options
| author | Malkierian <malkierian@live.com> | 2025-02-04 11:43:20 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-04 11:43:20 -0700 |
| commit | 695879c4cb6e09dae01eb6f204359619c177b79e (patch) | |
| tree | f4b76c257c7450d0890357cf1659b3eb8e524c79 | |
| parent | 301bbd3cd9d899baf8bb16b88675f1ff3d6d7f19 (diff) | |
Modern Menu (#183)
* Add modern menu.
Add and include LUS cvars .cmake file for macro usage in modern menu.
* Un-ignore new .cmake
* Copy new Resolution Editor stuff from 2ship (incomplete).
Fix fullscreen checkbox.
Finish changes for new UIWidgets.
Cleanup ShipUtils.
* Remove duplicate asm-differ entry from .gitmodules.
* Re-implement Freecam options.
* Cleanup freecam character follow buttons.
Partially re-implement Multiplayer window as enhancement sidebar (needs text input widget to finish).
Cleanup disables.
---------
Co-authored-by: MegaMech <MegaMech@users.noreply.github.com>
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | assets/fonts/Inconsolata-Regular.ttf | bin | 0 -> 101752 bytes | |||
| -rw-r--r-- | assets/fonts/Montserrat-Regular.ttf | bin | 0 -> 197624 bytes | |||
| -rw-r--r-- | cmake/lus-cvars.cmake | 3 | ||||
| -rw-r--r-- | src/port/Engine.cpp | 42 | ||||
| -rw-r--r-- | src/port/Engine.h | 9 | ||||
| -rw-r--r-- | src/port/ShipUtils.cpp | 37 | ||||
| -rw-r--r-- | src/port/ShipUtils.h | 19 | ||||
| -rw-r--r-- | src/port/ui/FreecamWindow.cpp | 223 | ||||
| -rw-r--r-- | src/port/ui/FreecamWindow.h | 10 | ||||
| -rw-r--r-- | src/port/ui/ImguiUI.cpp | 692 | ||||
| -rw-r--r-- | src/port/ui/Menu.cpp | 801 | ||||
| -rw-r--r-- | src/port/ui/Menu.h | 66 | ||||
| -rw-r--r-- | src/port/ui/MenuTypes.h | 303 | ||||
| -rw-r--r-- | src/port/ui/MultiplayerWindow.cpp | 203 | ||||
| -rw-r--r-- | src/port/ui/MultiplayerWindow.h | 10 | ||||
| -rw-r--r-- | src/port/ui/PortMenu.cpp | 497 | ||||
| -rw-r--r-- | src/port/ui/PortMenu.h | 64 | ||||
| -rw-r--r-- | src/port/ui/ResolutionEditor.cpp | 836 | ||||
| -rw-r--r-- | src/port/ui/ResolutionEditor.h | 25 | ||||
| -rw-r--r-- | src/port/ui/UIWidgets.cpp | 1103 | ||||
| -rw-r--r-- | src/port/ui/UIWidgets.h | 912 |
23 files changed, 3911 insertions, 1948 deletions
diff --git a/.gitmodules b/.gitmodules index e57c0ae29..29bd1ef14 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "tools/decomp-permuter"] path = tools/decomp-permuter url = https://github.com/simonlindholm/decomp-permuter -[submodule "tools/asm-differ"] - path = tools/asm-differ - url = https://github.com/simonlindholm/asm-differ [submodule "doxygen-awesome-css"] path = doxygen-awesome-css url = https://github.com/jothepro/doxygen-awesome-css.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a4bcd524..216bf854b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Spaghetti # Add a custom module path to locate additional CMake modules set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") include(FindFontconfig) +include(CMake/lus-cvars.cmake) if (WIN32) include(libultraship/cmake/automate-vcpkg.cmake) diff --git a/assets/fonts/Inconsolata-Regular.ttf b/assets/fonts/Inconsolata-Regular.ttf Binary files differnew file mode 100644 index 000000000..d1241516b --- /dev/null +++ b/assets/fonts/Inconsolata-Regular.ttf diff --git a/assets/fonts/Montserrat-Regular.ttf b/assets/fonts/Montserrat-Regular.ttf Binary files differnew file mode 100644 index 000000000..f4a266dd3 --- /dev/null +++ b/assets/fonts/Montserrat-Regular.ttf diff --git a/cmake/lus-cvars.cmake b/cmake/lus-cvars.cmake new file mode 100644 index 000000000..90130f657 --- /dev/null +++ b/cmake/lus-cvars.cmake @@ -0,0 +1,3 @@ +# kept for future reference +#set(CVAR_VSYNC_ENABLED "${CVAR_PREFIX_SETTING}.VsyncEnabled") +include("libultraship/cmake/cvars.cmake") diff --git a/src/port/Engine.cpp b/src/port/Engine.cpp index fd6db00b4..b180f3202 100644 --- a/src/port/Engine.cpp +++ b/src/port/Engine.cpp @@ -18,6 +18,9 @@ #include "resource/importers/UnkActorSpawnDataFactory.h" #include "resource/importers/ArrayFactory.h" #include <Fast3D/Fast3dWindow.h> +#include <Fonts.h> +#include "window/gui/resource/Font.h" +#include "window/gui/resource/FontFactory.h" #include <Fast3D/gfx_pc.h> #include <Fast3D/gfx_rendering_api.h> @@ -139,6 +142,14 @@ GameEngine::GameEngine() { loader->RegisterResourceFactory(std::make_shared<MK64::ResourceFactoryBinaryUnkActorSpawnDataV0>(), RESOURCE_FORMAT_BINARY, "UnkSpawnData", static_cast<uint32_t>(MK64::ResourceType::UnkSpawnData), 0); + + fontMono = CreateFontWithSize(16.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLarger = CreateFontWithSize(20.0f, "fonts/Inconsolata-Regular.ttf"); + fontMonoLargest = CreateFontWithSize(24.0f, "fonts/Inconsolata-Regular.ttf"); + fontStandard = CreateFontWithSize(16.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLarger = CreateFontWithSize(20.0f, "fonts/Montserrat-Regular.ttf"); + fontStandardLargest = CreateFontWithSize(24.0f, "fonts/Montserrat-Regular.ttf"); + ImGui::GetIO().FontDefault = fontMono; } void GameEngine::Create() { @@ -296,6 +307,37 @@ uint8_t GameEngine::GetBankIdByName(const std::string& name) { return 0; } +ImFont* GameEngine::CreateFontWithSize(float size, std::string fontPath) { + auto mImGuiIo = &ImGui::GetIO(); + ImFont* font; + if (fontPath == "") { + ImFontConfig fontCfg = ImFontConfig(); + fontCfg.OversampleH = fontCfg.OversampleV = 1; + fontCfg.PixelSnapH = true; + fontCfg.SizePixels = size; + font = mImGuiIo->Fonts->AddFontDefault(&fontCfg); + } else { + auto initData = std::make_shared<Ship::ResourceInitData>(); + initData->Format = RESOURCE_FORMAT_BINARY; + initData->Type = static_cast<uint32_t>(RESOURCE_TYPE_FONT); + initData->ResourceVersion = 0; + initData->Path = fontPath; + std::shared_ptr<Ship::Font> fontData = std::static_pointer_cast<Ship::Font>( + Ship::Context::GetInstance()->GetResourceManager()->LoadResource(fontPath, false, initData)); + font = mImGuiIo->Fonts->AddFontFromMemoryTTF(fontData->Data, fontData->DataSize, size); + } + // FontAwesome fonts need to have their sizes reduced by 2.0f/3.0f in order to align correctly + float iconFontSize = size * 2.0f / 3.0f; + static const ImWchar sIconsRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; + ImFontConfig iconsConfig; + iconsConfig.MergeMode = true; + iconsConfig.PixelSnapH = true; + iconsConfig.GlyphMinAdvanceX = iconFontSize; + mImGuiIo->Fonts->AddFontFromMemoryCompressedBase85TTF(fontawesome_compressed_data_base85, iconFontSize, + &iconsConfig, sIconsRanges); + return font; +} + // End extern "C" uint32_t GameEngine_GetSampleRate() { diff --git a/src/port/Engine.h b/src/port/Engine.h index aab8cb8d0..0b5d7943e 100644 --- a/src/port/Engine.h +++ b/src/port/Engine.h @@ -29,6 +29,13 @@ class GameEngine { std::vector<std::string> sequenceTable; std::vector<AudioSequenceData*> audioSequenceTable; + ImFont* fontStandard; + ImFont* fontStandardLarger; + ImFont* fontStandardLargest; + ImFont* fontMono; + ImFont* fontMonoLarger; + ImFont* fontMonoLargest; + std::unordered_map<std::string, uint8_t> bankMapTable; GameEngine(); static void Create(); @@ -54,6 +61,8 @@ class GameEngine { uint32_t OTRGetGameRenderHeight(); uint32_t OTRCalculateCenterOfAreaFromRightEdge(int32_t center); uint32_t OTRCalculateCenterOfAreaFromLeftEdge(int32_t center); + private: + ImFont* CreateFontWithSize(float size, std::string fontPath = ""); }; #endif diff --git a/src/port/ShipUtils.cpp b/src/port/ShipUtils.cpp new file mode 100644 index 000000000..a04f4b8ce --- /dev/null +++ b/src/port/ShipUtils.cpp @@ -0,0 +1,37 @@ +#include "ShipUtils.h" +#include <libultraship/libultraship.h> + +extern "C" { +#include "macros.h" +} + +constexpr f32 fourByThree = 4.0f / 3.0f; + +extern "C" bool Ship_IsCStringEmpty(const char* str) { + return str == NULL || str[0] == '\0'; +} + +// Build vertex coordinates for a quad command +// In order of top left, top right, bottom left, then bottom right +// Supports flipping the texture horizontally +extern "C" void Ship_CreateQuadVertexGroup(Vtx* vtxList, s32 xStart, s32 yStart, s32 width, s32 height, u8 flippedH) { + vtxList[0].v.ob[0] = xStart; + vtxList[0].v.ob[1] = yStart; + vtxList[0].v.tc[0] = (flippedH ? width : 0) << 5; + vtxList[0].v.tc[1] = 0 << 5; + + vtxList[1].v.ob[0] = xStart + width; + vtxList[1].v.ob[1] = yStart; + vtxList[1].v.tc[0] = (flippedH ? width * 2 : width) << 5; + vtxList[1].v.tc[1] = 0 << 5; + + vtxList[2].v.ob[0] = xStart; + vtxList[2].v.ob[1] = yStart + height; + vtxList[2].v.tc[0] = (flippedH ? width : 0) << 5; + vtxList[2].v.tc[1] = height << 5; + + vtxList[3].v.ob[0] = xStart + width; + vtxList[3].v.ob[1] = yStart + height; + vtxList[3].v.tc[0] = (flippedH ? width * 2 : width) << 5; + vtxList[3].v.tc[1] = height << 5; +} diff --git a/src/port/ShipUtils.h b/src/port/ShipUtils.h new file mode 100644 index 000000000..cd7bd2da9 --- /dev/null +++ b/src/port/ShipUtils.h @@ -0,0 +1,19 @@ +#ifndef SHIP_UTILS_H +#define SHIP_UTILS_H + +#include <libultraship/libultraship.h> + +#ifdef __cplusplus + +void LoadGuiTextures(); + +extern "C" { +#endif + +bool Ship_IsCStringEmpty(const char* str); + +#ifdef __cplusplus +} +#endif + +#endif // SHIP_UTILS_H diff --git a/src/port/ui/FreecamWindow.cpp b/src/port/ui/FreecamWindow.cpp index 399b28690..e5cf5eea1 100644 --- a/src/port/ui/FreecamWindow.cpp +++ b/src/port/ui/FreecamWindow.cpp @@ -1,10 +1,13 @@ #include "FreecamWindow.h" +#include "port/ui/PortMenu.h" #include "UIWidgets.h" #include "libultraship/src/Context.h" #include <imgui.h> +#include <map> #include <libultraship/libultraship.h> -#include "spdlog/spdlog.h" +#include <spdlog/fmt/fmt.h> +#include "spdlog/formatter.h" #include <common_structs.h> #include <defines.h> #include "enhancements/freecam/freecam_engine.h" @@ -30,80 +33,166 @@ extern u32 gFreecamControllerType; void freecam_get_player_from_character(s32 characterId); } +namespace GameUI { +extern std::shared_ptr<PortMenu> mPortMenu; namespace Freecam { -FreecamWindow::~FreecamWindow() { - SPDLOG_TRACE("destruct freecam window"); -} - -void FreecamWindow::InitElement() { -} static s32 sReadyUpBool = false; -float dampMin = 0.970; -float dampMax = 1.0f; -float minSpeed = 1.0; -float maxSpeed = 30.0f; -float minSpeedMultiplier = 1.5f; -float maxSpeedMultiplier = 15.0f; -float minFreecamRotateFactor = 0.0f; -float maxFreecamRotateFactor = 1.0f; -float minFreecamFollowFactor = 0.0f; -float maxFreecamFollowFactor = 1.0f; +int32_t controllerType = 0; +std::unordered_map<int32_t, const char*> controlType = {{ 0, "Mouse/Keyboard" }, { 1, "Controller" }}; uint32_t focusPlayer; -void FreecamWindow::DrawElement() { - const float framerate = ImGui::GetIO().Framerate; - const float deltatime = ImGui::GetIO().DeltaTime; - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); - - ImGui::Text("Controller mode is not configured yet."); - - const char* items[] = { "Mouse/Keyboard", "Controller" }; - static int current_item = 0; - if (ImGui::Combo("Dropdown", ¤t_item, items, IM_ARRAYSIZE(items))) { - gFreecamControllerType = current_item; - } - - ImGui::Text("Move: W,A,S,D\nUp: Space, Down: Shift\nFaster: Ctrl\nLook: Right-mouse button\nTarget Player Mode: F, Next: M, Previous: N"); - ImGui::Spacing(); - UIWidgets::CVarCheckbox("Enable Flycam", "gFreecam", { .tooltip = "Allows you to fly around the course" }); - - ImGui::Spacing(); - - if (ImGui::SliderScalar("Camera Damping", ImGuiDataType_Float, &gDampValue, &dampMin, &dampMax, "%f")) {}; - if (ImGui::SliderScalar("Camera Speed", ImGuiDataType_Float, &gFreecamSpeed, &minSpeed, &maxSpeed, "%f")) {}; - if (ImGui::SliderScalar("Camera Speed Multiplier", ImGuiDataType_Float, &gFreecamSpeedMultiplier, - &minSpeedMultiplier, &maxSpeedMultiplier, "%f")) {}; - if (ImGui::SliderScalar("Camera Rotation Smoothing", ImGuiDataType_Float, &gFreecamRotateSmoothingFactor, - &minFreecamRotateFactor, &maxFreecamRotateFactor, "%f")) {}; - if (ImGui::SliderScalar("Follow Factor", ImGuiDataType_Float, &gFreecamFollowFactor, - &minFreecamFollowFactor, &maxFreecamFollowFactor, "%f")) {}; - - ImGui::Spacing(); - - ImGui::Text("Target Player"); - - if (ImGui::Button("None")) { - fTargetPlayer = false; - } - - for (size_t i = 0; i < NUM_PLAYERS; i++) { - // These are euc-jp characters that look sort of like a hyphen - - if (D_800E76A8[i] == NULL || D_800E76A8[i] == "\xA1\xBC\xA1\xBC\xA1\xBC\xA1\xBC") { - break; - } - ImGui::SameLine(); - if (ImGui::Button(D_800E76A8[i])) { - freecam_get_player_from_character(i); - fTargetPlayer = true; - } - } - - ImGui::PopStyleColor(); +bool IsPlayerValid(const char* string) { + return string != NULL && (strncmp(string, "\xA1\xBC\xA1\xBC\xA1\xBC\xA1\xBC", 8) != 0); } -void FreecamWindow::UpdateElement() { +void RegisterFreecamWidgets() { + mPortMenu->AddSidebarEntry("Enhancements", "Freecam", 2); + WidgetPath path = { "Enhancements", "Freecam", SECTION_COLUMN_1 }; + //const float framerate = ImGui::GetIO().Framerate; + //const float deltatime = ImGui::GetIO().DeltaTime; + //ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + + //mPortMenu->AddWidget(path, "Controller mode is not configured yet.", WIDGET_TEXT); + + //static int current_item = 0; + mPortMenu->AddWidget(path, "Control Type", WIDGET_COMBOBOX) + .ValuePointer(&controllerType) + .Callback([](WidgetInfo& info) { gFreecamControllerType = (uint32_t)*std::get<int32_t*>(info.valuePointer); }) + .Options(UIWidgets::ComboboxOptions().ComboMap(controlType)); + + mPortMenu->AddWidget(path, "Move: W,A,S,D\nUp: Space, Down: Shift\nFaster: Ctrl\nLook: Right-mouse button\nTarget Player Mode: F, Next: M, Previous: N", WIDGET_TEXT); + mPortMenu->AddWidget(path, "Enable Flycam", WIDGET_CVAR_CHECKBOX) + .CVar("gFreecam") + .Options(UIWidgets::CheckboxOptions({{ .tooltip = "Allows you to fly around the course"}})); + + mPortMenu->AddWidget(path, "Camera Damping", WIDGET_SLIDER_FLOAT) + .ValuePointer(&gDampValue) + .Options(UIWidgets::FloatSliderOptions().Min(0.970f).Max(1.0f).Step(0.001f).Format("%.3f")); + mPortMenu->AddWidget(path, "Camera Speed", WIDGET_SLIDER_FLOAT) + .ValuePointer(&gFreecamSpeed) + .Options(UIWidgets::FloatSliderOptions().Min(1.0f).Max(30.0f).Step(0.1f).Format("%.1f")); + mPortMenu->AddWidget(path, "Camera Speed Multiplier", WIDGET_SLIDER_FLOAT) + .ValuePointer(&gFreecamSpeedMultiplier) + .Options(UIWidgets::FloatSliderOptions().Min(1.5f).Max(15.0f).Step(0.1f).Format("%.1f")); + mPortMenu->AddWidget(path, "Camera Rotation Smoothing", WIDGET_SLIDER_FLOAT) + .ValuePointer(&gFreecamRotateSmoothingFactor) + .Options(UIWidgets::FloatSliderOptions().Min(0.0f).Max(1.0f).Step(0.01f).Format("%.2f")); + mPortMenu->AddWidget(path, "Follow Factor", WIDGET_SLIDER_FLOAT) + .ValuePointer(&gFreecamFollowFactor) + .Options(UIWidgets::FloatSliderOptions().Min(0.0f).Max(1.0f).Step(0.01f).Format("%.2f")); + + mPortMenu->AddWidget(path, "Target Player", WIDGET_TEXT); + mPortMenu->AddWidget(path, "None", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { fTargetPlayer = false; }); + + mPortMenu->AddWidget(path, "Player 1", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[0]); + if (!info.isHidden) { + info.name = D_800E76A8[0]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(0); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 2", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[1]); + if (!info.isHidden) { + info.name = D_800E76A8[1]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(1); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 3", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[2]); + if (!info.isHidden) { + info.name = D_800E76A8[2]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(2); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 4", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[3]); + if (!info.isHidden) { + info.name = D_800E76A8[3]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(3); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 5", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[4]); + if (!info.isHidden) { + info.name = D_800E76A8[4]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(4); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 6", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[5]); + if (!info.isHidden) { + info.name = D_800E76A8[5]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(5); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 7", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[6]); + if (!info.isHidden) { + info.name = D_800E76A8[6]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(6); + fTargetPlayer = true; + }); + mPortMenu->AddWidget(path, "Player 8", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !IsPlayerValid(D_800E76A8[7]); + if (!info.isHidden) { + info.name = D_800E76A8[7]; + } + }) + .Options(UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline)) + .SameLine(true) + .Callback([](WidgetInfo& info) { + freecam_get_player_from_character(7); + fTargetPlayer = true; + }); } + +static RegisterMenuInitFunc initFunc(RegisterFreecamWidgets); + } // namespace Freecam +} // namespace GameUI diff --git a/src/port/ui/FreecamWindow.h b/src/port/ui/FreecamWindow.h index 6208666d1..4418b8d61 100644 --- a/src/port/ui/FreecamWindow.h +++ b/src/port/ui/FreecamWindow.h @@ -3,14 +3,4 @@ #include <libultraship/libultraship.h> namespace Freecam { -class FreecamWindow : public Ship::GuiWindow { - public: - using Ship::GuiWindow::GuiWindow; - ~FreecamWindow(); - - private: - void InitElement() override; - void DrawElement() override; - void UpdateElement() override; -}; } // namespace Freecam diff --git a/src/port/ui/ImguiUI.cpp b/src/port/ui/ImguiUI.cpp index 9801267ce..bbaa97454 100644 --- a/src/port/ui/ImguiUI.cpp +++ b/src/port/ui/ImguiUI.cpp @@ -14,6 +14,7 @@ #include <libultraship/libultraship.h> #include <Fast3D/gfx_pc.h> #include "port/Engine.h" +#include "PortMenu.h" extern "C" { extern s32 gGamestateNext; @@ -24,26 +25,23 @@ extern "C" { namespace GameUI { std::shared_ptr<GameMenuBar> mGameMenuBar; +std::shared_ptr<PortMenu> mPortMenu; std::shared_ptr<Ship::GuiWindow> mConsoleWindow; std::shared_ptr<Ship::GuiWindow> mStatsWindow; std::shared_ptr<Ship::GuiWindow> mInputEditorWindow; std::shared_ptr<Ship::GuiWindow> mGfxDebuggerWindow; std::shared_ptr<Ship::GuiWindow> mGameInfoWindow; std::shared_ptr<Ship::GuiWindow> mMultiplayerWindow; -std::shared_ptr<Ship::GuiWindow> mFreecamWindow; -std::shared_ptr<AdvancedResolutionSettings::AdvancedResolutionSettingsWindow> mAdvancedResolutionSettingsWindow; void SetupGuiElements() { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); - auto& style = ImGui::GetStyle(); - style.FramePadding = ImVec2(4.0f, 6.0f); - style.ItemSpacing = ImVec2(8.0f, 6.0f); - style.Colors[ImGuiCol_MenuBarBg] = UIWidgets::Colors::DarkGray; - mGameMenuBar = std::make_shared<GameMenuBar>("gOpenMenuBar", CVarGetInteger("gOpenMenuBar", 0)); gui->SetMenuBar(mGameMenuBar); + mPortMenu = std::make_shared<PortMenu>("gOpenMenu", "Port Menu"); + gui->SetMenu(mPortMenu); + mMultiplayerWindow = gui->GetGuiWindow("Multiplayer"); if (mMultiplayerWindow == nullptr) { SPDLOG_ERROR("Could not find multiplayer window"); @@ -75,27 +73,11 @@ void SetupGuiElements() { SPDLOG_ERROR("Could not find input GfxDebuggerWindow"); } - mMultiplayerWindow = std::make_shared<Multiplayer::MultiplayerWindow>("gMultiplayerWindowEnabled", "Multiplayer"); - gui->AddGuiWindow(mMultiplayerWindow); - - mFreecamWindow = gui->GetGuiWindow("FreecamWindow"); - if (mFreecamWindow == nullptr) { - SPDLOG_ERROR("Could not find input FreecamWindow"); - } - - mFreecamWindow = std::make_shared<Freecam::FreecamWindow>("gFreecamEnabled", "Freecam"); - gui->AddGuiWindow(mFreecamWindow); - - mGameInfoWindow = std::make_shared<GameInfo::GameInfoWindow>("gGameInfoEnabled", "Game info"); + mGameInfoWindow = std::make_shared<GameInfo::GameInfoWindow>("gGameInfoEnabled", "Game Info"); gui->AddGuiWindow(mGameInfoWindow); - - mAdvancedResolutionSettingsWindow = std::make_shared<AdvancedResolutionSettings::AdvancedResolutionSettingsWindow>( - "gAdvancedResolutionEditorEnabled", "Advanced Resolution Settings"); - gui->AddGuiWindow(mAdvancedResolutionSettingsWindow); } void Destroy() { - mAdvancedResolutionSettingsWindow = nullptr; mGameInfoWindow = nullptr; mConsoleWindow = nullptr; mStatsWindow = nullptr; @@ -125,294 +107,294 @@ static const char* filters[3] = { }; void DrawSettingsMenu() { - if (UIWidgets::BeginMenu("Settings")) { - if (UIWidgets::BeginMenu("Audio")) { - UIWidgets::CVarSliderFloat("Master Volume", "gGameMasterVolume", 0.0f, 1.0f, 1.0f, { - .format = "%.0f%%", - .isPercentage = true, - }); - if (UIWidgets::CVarSliderFloat("Main Music Volume", "gMainMusicVolume", 0.0f, 1.0f, 1.0f, - { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_LEVEL, CVarGetFloat("gMainMusicVolume", 1.0f)); - } - if (UIWidgets::CVarSliderFloat("Sound Effects Volume", "gSFXMusicVolume", - 0.0f, 1.0f, 1.0f, { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_SFX, CVarGetFloat("gSFXMusicVolume", 1.0f)); - } - if (UIWidgets::CVarSliderFloat("Environment Volume", "gEnvironmentVolume", - 0.0f, 1.0f, 1.0f, { - .format = "%.0f%%", - .isPercentage = true, - })) { - audio_set_player_volume(SEQ_PLAYER_ENV, CVarGetFloat("gEnvironmentVolume", 1.0f)); - } - - static std::unordered_map<Ship::AudioBackend, const char*> audioBackendNames = { - { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, - { Ship::AudioBackend::SDL, "SDL" }, - }; - - ImGui::Text("Audio API (Needs reload)"); - auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetCurrentAudioBackend(); - - if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::BeginCombo("##AApi", audioBackendNames[currentAudioBackend])) { - for (uint8_t i = 0; i < - Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size(); i++) { - auto backend = Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->data()[i]; - if (ImGui::Selectable(audioBackendNames[backend], backend == currentAudioBackend)) { - Ship::Context::GetInstance()->GetAudio()->SetCurrentAudioBackend(backend); - } - } - ImGui::EndCombo(); - } - if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { - UIWidgets::ReEnableComponent(""); - } - - ImGui::EndMenu(); - } - - UIWidgets::Spacer(0); - - if (UIWidgets::BeginMenu("Controller")) { - UIWidgets::WindowButton("Controller Mapping", "gInputEditorWindow", GameUI::mInputEditorWindow); - - UIWidgets::Spacer(0); - -#ifndef __SWITCH__ - UIWidgets::CVarCheckbox( - "Menubar Controller Navigation", "gControlNav", - { .tooltip = "Allows controller navigation of the SOH menu bar (Settings, Enhancements,...)\nCAUTION: " - "This will disable game inputs while the menubar is visible.\n\nD-pad to move between " - "items, A to select, and X to grab focus on the menu bar" }); -#endif - UIWidgets::CVarCheckbox("Show Inputs", "gInputEnabled", - { .tooltip = "Shows currently pressed inputs on the bottom right of the screen" }); - if (CVarGetInteger("gInputEnabled", 0)) { - UIWidgets::CVarSliderFloat( - "Input Scale", "gInputScale", 1.0f, 3.0f, 1.0f, - { - .tooltip = "Sets the on screen size of the displayed inputs from the Show Inputs setting", - .format = "%.1fx", - }); - } - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } - - ImGui::SetCursorPosY(0.0f); - if (UIWidgets::BeginMenu("Graphics")) { - UIWidgets::WindowButton("Resolution Editor", "gAdvancedResolutionEditorEnabled", - GameUI::mAdvancedResolutionSettingsWindow); - - UIWidgets::Spacer(0); - - // Previously was running every frame, and nothing was setting it? Maybe a bad copy/paste? - // Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); - // UIWidgets::Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but - // effective form of anti-aliasing"); -#ifndef __WIIU__ - if (UIWidgets::CVarSliderInt( - "MSAA: %d", "gMSAAValue", 1, 8, 1, - { .tooltip = - "Activates multi-sample anti-aliasing when above 1x up to 8x for 8 samples for every pixel" })) { - Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger("gMSAAValue", 1)); - } -#endif - - { // FPS Slider - const int minFps = 30; - static int maxFps; - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == - Ship::WindowBackend::FAST3D_DXGI_DX11) { - maxFps = 360; - } else { - maxFps = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); - } - int currentFps = 0; -#ifdef __WIIU__ - UIWidgets::Spacer(0); - // only support divisors of 60 on the Wii U - if (currentFps > 60) { - currentFps = 60; - } else { - currentFps = 60 / (60 / currentFps); - } - - int fpsSlider = 1; - if (currentFps == 30) { - ImGui::Text("FPS: Original (30)"); - } else { - ImGui::Text("FPS: %d", currentFps); - if (currentFps == 30) { - fpsSlider = 2; - } else { // currentFps == 60 - fpsSlider = 3; - } - } - if (CVarGetInteger("gMatchRefreshRate", 0)) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - if (ImGui::Button(" - ##WiiUFPS")) { - fpsSlider--; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - - UIWidgets::Spacer(0); - - ImGui::PushItemWidth(std::min((ImGui::GetContentRegionAvail().x - 60.0f), 260.0f)); - ImGui::SliderInt("##WiiUFPSSlider", &fpsSlider, 1, 3, "", ImGuiSliderFlags_AlwaysClamp); - ImGui::PopItemWidth(); - - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(" + ##WiiUFPS")) { - fpsSlider++; - } - - if (CVarGetInteger("gMatchRefreshRate", 0)) { - UIWidgets::ReEnableComponent(""); - } - if (fpsSlider > 3) { - fpsSlider = 3; - } else if (fpsSlider < 1) { - fpsSlider = 1; - } - - if (fpsSlider == 1) { - currentFps = 20; - } else if (fpsSlider == 2) { - currentFps = 30; - } else if (fpsSlider == 3) { - currentFps = 60; - } - CVarSetInteger("gInterpolationFPS", currentFps); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); -#else - bool matchingRefreshRate = - CVarGetInteger("gMatchRefreshRate", 0) && - Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != Ship::WindowBackend::FAST3D_DXGI_DX11; - UIWidgets::CVarSliderInt((currentFps == 20) ? "FPS: Original (20)" : "FPS: %d", "gInterpolationFPS", minFps, - maxFps, 1, { .disabled = matchingRefreshRate }); -#endif - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == - Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::Tooltip( - "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. " - "This is purely " - "visual and does not impact game logic, execution of glitches etc.\n\n" - "A higher target FPS than your monitor's refresh rate will waste resources, and might give a worse " - "result."); - } else { - UIWidgets::Tooltip( - "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is purely " - "visual and does not impact game logic, execution of glitches etc."); - } - } // END FPS Slider - - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::Spacer(0); - if (ImGui::Button("Match Refresh Rate")) { - int hz = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); - if (hz >= 30 && hz <= 360) { - CVarSetInteger("gInterpolationFPS", hz); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - } else { - UIWidgets::PaddedEnhancementCheckbox("Match Refresh Rate", "gMatchRefreshRate", true, false); - } - - UIWidgets::Tooltip("Matches interpolation value to the current game's window refresh rate"); - - if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { - UIWidgets::PaddedEnhancementSliderInt( - CVarGetInteger("gExtraLatencyThreshold", 0) == 0 ? "Jitter fix: Off" : "Jitter fix: >= %d FPS", - "##ExtraLatencyThreshold", "gExtraLatencyThreshold", 0, 360, "", 0, true, true, false); - UIWidgets::Tooltip("When Interpolation FPS setting is at least this threshold, add one frame of input lag " - "(e.g. 16.6 ms for 60 FPS) in order to avoid jitter. This setting allows the CPU to " - "work on one frame while GPU works on the previous frame.\nThis setting should be used " - "when your computer is too slow to do CPU + GPU work in time."); - } - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - static std::unordered_map<Ship::WindowBackend, const char*> windowBackendNames = { - { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, - { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, - { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, - }; - - ImGui::Text("Renderer API (Needs reload)"); - Ship::WindowBackend runningWindowBackend = Ship::Context::GetInstance()->GetWindow()->GetWindowBackend(); - Ship::WindowBackend configWindowBackend; - int configWindowBackendId = Ship::Context::GetInstance()->GetConfig()->GetInt("Window.Backend.Id", -1); - if (Ship::Context::GetInstance()->GetWindow()->IsAvailableWindowBackend(configWindowBackendId)) { - configWindowBackend = static_cast<Ship::WindowBackend>(configWindowBackendId); - } else { - configWindowBackend = runningWindowBackend; - } - - if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { - UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::BeginCombo("##RApi", windowBackendNames[configWindowBackend])) { - for (size_t i = 0; i < Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size(); - i++) { - auto backend = Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->data()[i]; - if (ImGui::Selectable(windowBackendNames[backend], backend == configWindowBackend)) { - Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", static_cast<int>(backend)); - Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", - windowBackendNames[backend]); - Ship::Context::GetInstance()->GetConfig()->Save(); - } - } - ImGui::EndCombo(); - } - if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { - UIWidgets::ReEnableComponent(""); - } - - if (Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync()) { - UIWidgets::PaddedEnhancementCheckbox("Enable Vsync", "gVsyncEnabled", true, false); - } - - if (Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen()) { - UIWidgets::PaddedEnhancementCheckbox("Windowed fullscreen", "gSdlWindowedFullscreen", true, false); - } - - if (Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports()) { - UIWidgets::PaddedEnhancementCheckbox("Allow multi-windows", "gEnableMultiViewports", true, false, false, "", - UIWidgets::CheckboxGraphics::Cross, true); - UIWidgets::Tooltip("Allows windows to be able to be dragged off of the main game window. Requires a reload " - "to take effect."); - } - - // If more filters are added to LUS, make sure to add them to the filters list here - ImGui::Text("Texture Filter (Needs reload)"); - - UIWidgets::EnhancementCombobox("gTextureFilter", filters, 0); - - UIWidgets::Spacer(0); - - Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->DrawSettings(); - - ImGui::EndMenu(); - } +// if (UIWidgets::BeginMenu("Settings")) { +// if (UIWidgets::BeginMenu("Audio")) { +// UIWidgets::CVarSliderFloat("Master Volume", "gGameMasterVolume", 0.0f, 1.0f, 1.0f, { +// .format = "%.0f%%", +// .isPercentage = true, +// }); +// if (UIWidgets::CVarSliderFloat("Main Music Volume", "gMainMusicVolume", 0.0f, 1.0f, 1.0f, +// { +// .format = "%.0f%%", +// .isPercentage = true, +// })) { +// audio_set_player_volume(SEQ_PLAYER_LEVEL, CVarGetFloat("gMainMusicVolume", 1.0f)); +// } +// if (UIWidgets::CVarSliderFloat("Sound Effects Volume", "gSFXMusicVolume", +// 0.0f, 1.0f, 1.0f, { +// .format = "%.0f%%", +// .isPercentage = true, +// })) { +// audio_set_player_volume(SEQ_PLAYER_SFX, CVarGetFloat("gSFXMusicVolume", 1.0f)); +// } +// if (UIWidgets::CVarSliderFloat("Environment Volume", "gEnvironmentVolume", +// 0.0f, 1.0f, 1.0f, { +// .format = "%.0f%%", +// .isPercentage = true, +// })) { +// audio_set_player_volume(SEQ_PLAYER_ENV, CVarGetFloat("gEnvironmentVolume", 1.0f)); +// } +// +// static std::unordered_map<Ship::AudioBackend, const char*> audioBackendNames = { +// { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, +// { Ship::AudioBackend::SDL, "SDL" }, +// }; +// +// ImGui::Text("Audio API (Needs reload)"); +// auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetCurrentAudioBackend(); +// +// if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { +// UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); +// } +// if (ImGui::BeginCombo("##AApi", audioBackendNames[currentAudioBackend])) { +// for (uint8_t i = 0; i < +// Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size(); i++) { +// auto backend = Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->data()[i]; +// if (ImGui::Selectable(audioBackendNames[backend], backend == currentAudioBackend)) { +// Ship::Context::GetInstance()->GetAudio()->SetCurrentAudioBackend(backend); +// } +// } +// ImGui::EndCombo(); +// } +// if (Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1) { +// UIWidgets::ReEnableComponent(""); +// } +// +// ImGui::EndMenu(); +// } +// +// UIWidgets::Spacer(0); +// +// if (UIWidgets::BeginMenu("Controller")) { +// UIWidgets::WindowButton("Controller Mapping", "gInputEditorWindow", GameUI::mInputEditorWindow); +// +// UIWidgets::Spacer(0); +// +//#ifndef __SWITCH__ +// UIWidgets::CVarCheckbox( +// "Menubar Controller Navigation", "gControlNav", +// { .tooltip = "Allows controller navigation of the SOH menu bar (Settings, Enhancements,...)\nCAUTION: " +// "This will disable game inputs while the menubar is visible.\n\nD-pad to move between " +// "items, A to select, and X to grab focus on the menu bar" }); +//#endif +// UIWidgets::CVarCheckbox("Show Inputs", "gInputEnabled", +// { .tooltip = "Shows currently pressed inputs on the bottom right of the screen" }); +// if (CVarGetInteger("gInputEnabled", 0)) { +// UIWidgets::CVarSliderFloat( +// "Input Scale", "gInputScale", 1.0f, 3.0f, 1.0f, +// { +// .tooltip = "Sets the on screen size of the displayed inputs from the Show Inputs setting", +// .format = "%.1fx", +// }); +// } +// +// ImGui::EndMenu(); +// } +// +// ImGui::EndMenu(); +// } +// +// ImGui::SetCursorPosY(0.0f); +// if (UIWidgets::BeginMenu("Graphics")) { +// UIWidgets::WindowButton("Resolution Editor", "gAdvancedResolutionEditorEnabled", +// GameUI::mAdvancedResolutionSettingsWindow); +// +// UIWidgets::Spacer(0); +// +// // Previously was running every frame, and nothing was setting it? Maybe a bad copy/paste? +// // Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); +// // UIWidgets::Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but +// // effective form of anti-aliasing"); +//#ifndef __WIIU__ +// if (UIWidgets::CVarSliderInt( +// "MSAA: %d", "gMSAAValue", 1, 8, 1, +// { .tooltip = +// "Activates multi-sample anti-aliasing when above 1x up to 8x for 8 samples for every pixel" })) { +// Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger("gMSAAValue", 1)); +// } +//#endif +// +// { // FPS Slider +// const int minFps = 30; +// static int maxFps; +// if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == +// Ship::WindowBackend::FAST3D_DXGI_DX11) { +// maxFps = 360; +// } else { +// maxFps = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); +// } +// int currentFps = 0; +//#ifdef __WIIU__ +// UIWidgets::Spacer(0); +// // only support divisors of 60 on the Wii U +// if (currentFps > 60) { +// currentFps = 60; +// } else { +// currentFps = 60 / (60 / currentFps); +// } +// +// int fpsSlider = 1; +// if (currentFps == 30) { +// ImGui::Text("FPS: Original (30)"); +// } else { +// ImGui::Text("FPS: %d", currentFps); +// if (currentFps == 30) { +// fpsSlider = 2; +// } else { // currentFps == 60 +// fpsSlider = 3; +// } +// } +// if (CVarGetInteger("gMatchRefreshRate", 0)) { +// UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); +// } +// +// if (ImGui::Button(" - ##WiiUFPS")) { +// fpsSlider--; +// } +// ImGui::SameLine(); +// ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); +// +// UIWidgets::Spacer(0); +// +// ImGui::PushItemWidth(std::min((ImGui::GetContentRegionAvail().x - 60.0f), 260.0f)); +// ImGui::SliderInt("##WiiUFPSSlider", &fpsSlider, 1, 3, "", ImGuiSliderFlags_AlwaysClamp); +// ImGui::PopItemWidth(); +// +// ImGui::SameLine(); +// ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); +// if (ImGui::Button(" + ##WiiUFPS")) { +// fpsSlider++; +// } +// +// if (CVarGetInteger("gMatchRefreshRate", 0)) { +// UIWidgets::ReEnableComponent(""); +// } +// if (fpsSlider > 3) { +// fpsSlider = 3; +// } else if (fpsSlider < 1) { +// fpsSlider = 1; +// } +// +// if (fpsSlider == 1) { +// currentFps = 20; +// } else if (fpsSlider == 2) { +// currentFps = 30; +// } else if (fpsSlider == 3) { +// currentFps = 60; +// } +// CVarSetInteger("gInterpolationFPS", currentFps); +// Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +//#else +// bool matchingRefreshRate = +// CVarGetInteger("gMatchRefreshRate", 0) && +// Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != Ship::WindowBackend::FAST3D_DXGI_DX11; +// UIWidgets::CVarSliderInt((currentFps == 20) ? "FPS: Original (20)" : "FPS: %d", "gInterpolationFPS", minFps, +// maxFps, 1, { .disabled = matchingRefreshRate }); +//#endif +// if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == +// Ship::WindowBackend::FAST3D_DXGI_DX11) { +// UIWidgets::Tooltip( +// "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. " +// "This is purely " +// "visual and does not impact game logic, execution of glitches etc.\n\n" +// "A higher target FPS than your monitor's refresh rate will waste resources, and might give a worse " +// "result."); +// } else { +// UIWidgets::Tooltip( +// "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is purely " +// "visual and does not impact game logic, execution of glitches etc."); +// } +// } // END FPS Slider +// +// if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { +// UIWidgets::Spacer(0); +// if (ImGui::Button("Match Refresh Rate")) { +// int hz = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); +// if (hz >= 30 && hz <= 360) { +// CVarSetInteger("gInterpolationFPS", hz); +// Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +// } +// } +// } else { +// UIWidgets::PaddedEnhancementCheckbox("Match Refresh Rate", "gMatchRefreshRate", true, false); +// } +// +// UIWidgets::Tooltip("Matches interpolation value to the current game's window refresh rate"); +// +// if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { +// UIWidgets::PaddedEnhancementSliderInt( +// CVarGetInteger("gExtraLatencyThreshold", 0) == 0 ? "Jitter fix: Off" : "Jitter fix: >= %d FPS", +// "##ExtraLatencyThreshold", "gExtraLatencyThreshold", 0, 360, "", 0, true, true, false); +// UIWidgets::Tooltip("When Interpolation FPS setting is at least this threshold, add one frame of input lag " +// "(e.g. 16.6 ms for 60 FPS) in order to avoid jitter. This setting allows the CPU to " +// "work on one frame while GPU works on the previous frame.\nThis setting should be used " +// "when your computer is too slow to do CPU + GPU work in time."); +// } +// +// UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); +// +// static std::unordered_map<Ship::WindowBackend, const char*> windowBackendNames = { +// { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, +// { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, +// { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, +// }; +// +// ImGui::Text("Renderer API (Needs reload)"); +// Ship::WindowBackend runningWindowBackend = Ship::Context::GetInstance()->GetWindow()->GetWindowBackend(); +// Ship::WindowBackend configWindowBackend; +// int configWindowBackendId = Ship::Context::GetInstance()->GetConfig()->GetInt("Window.Backend.Id", -1); +// if (Ship::Context::GetInstance()->GetWindow()->IsAvailableWindowBackend(configWindowBackendId)) { +// configWindowBackend = static_cast<Ship::WindowBackend>(configWindowBackendId); +// } else { +// configWindowBackend = runningWindowBackend; +// } +// +// if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { +// UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); +// } +// if (ImGui::BeginCombo("##RApi", windowBackendNames[configWindowBackend])) { +// for (size_t i = 0; i < Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size(); +// i++) { +// auto backend = Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->data()[i]; +// if (ImGui::Selectable(windowBackendNames[backend], backend == configWindowBackend)) { +// Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", static_cast<int>(backend)); +// Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", +// windowBackendNames[backend]); +// Ship::Context::GetInstance()->GetConfig()->Save(); +// } +// } +// ImGui::EndCombo(); +// } +// if (Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends()->size() <= 1) { +// UIWidgets::ReEnableComponent(""); +// } +// +// if (Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync()) { +// UIWidgets::PaddedEnhancementCheckbox("Enable Vsync", "gVsyncEnabled", true, false); +// } +// +// if (Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen()) { +// UIWidgets::PaddedEnhancementCheckbox("Windowed fullscreen", "gSdlWindowedFullscreen", true, false); +// } +// +// if (Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports()) { +// UIWidgets::PaddedEnhancementCheckbox("Allow multi-windows", "gEnableMultiViewports", true, false, false, "", +// UIWidgets::CheckboxGraphics::Cross, true); +// UIWidgets::Tooltip("Allows windows to be able to be dragged off of the main game window. Requires a reload " +// "to take effect."); +// } +// +// // If more filters are added to LUS, make sure to add them to the filters list here +// ImGui::Text("Texture Filter (Needs reload)"); +// +// UIWidgets::EnhancementCombobox("gTextureFilter", filters, 0); +// +// UIWidgets::Spacer(0); +// +// Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGameOverlay()->DrawSettings(); +// +// ImGui::EndMenu(); +// } } void DrawMenuBarIcon() { @@ -442,56 +424,15 @@ void DrawMenuBarIcon() { } void DrawGameMenu() { - if (UIWidgets::BeginMenu("Spaghetti")) { - if (UIWidgets::MenuItem("Reset", -#ifdef __APPLE__ - "Command-R" -#else - "Ctrl+R" -#endif - )) { - gGamestateNext = MAIN_MENU_FROM_QUIT; - if (CVarGetInteger("gEnableDebugMode", 0) == true) { - gMenuSelection = START_MENU; - } else { - gMenuSelection = LOGO_INTRO_MENU; - } - } -#if !defined(__SWITCH__) && !defined(__WIIU__) - - if (UIWidgets::MenuItem("Toggle Fullscreen", "F11")) { - Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); - } - if (UIWidgets::MenuItem("Quit")) { - Ship::Context::GetInstance()->GetWindow()->Close(); - } -#endif - ImGui::EndMenu(); - } } void DrawEnhancementsMenu() { if (UIWidgets::BeginMenu("Enhancements")) { - UIWidgets::WindowButton("Multiplayer", "gMultiplayerWindowEnabled", GameUI::mMultiplayerWindow, - { .tooltip = "Shows the multiplayer window" }); if (UIWidgets::BeginMenu("Gameplay")) { - UIWidgets::WindowButton("Freecam", "gFreecam", GameUI::mFreecamWindow, - { .tooltip = "Allows you to fly around the course" }); - UIWidgets::CVarCheckbox("No multiplayer feature cuts", "gMultiplayerNoFeatureCuts", - { .tooltip = "Allows full train and jumbotron in multiplayer, etc." }); - UIWidgets::CVarCheckbox("General Improvements", "gImprovements", { .tooltip = "General improvements to the game experience." }); - UIWidgets::CVarCheckbox( - "No Level of Detail (LOD)", "gDisableLod", - { .tooltip = "Disable Level of Detail (LOD) to avoid models using lower poly versions at a distance" }); - - - UIWidgets::CVarCheckbox("Disable Culling", "gNoCulling", { .tooltip = "Disable original culling of mk64" }); - UIWidgets::CVarSliderFloat( - "Far Frustrum", "gFarFrustrum", 0.0f, 10000.0f, 10000.0f, - { .tooltip = "Say how Far the Frustrum are when 'Disable Culling' are enable", .step = 10.0f }); + ImGui::EndMenu(); } @@ -500,16 +441,7 @@ void DrawEnhancementsMenu() { } void DrawCheatsMenu() { - if (UIWidgets::BeginMenu("Cheats")) { - UIWidgets::CVarCheckbox("Moon Jump", "gEnableMoonJump"); - UIWidgets::CVarCheckbox("Enable Custom CC", "gEnableCustomCC"); - UIWidgets::CVarSliderFloat("Custom CC", "gCustomCC", 0.0, 1000.0, 150.0, { .step = 10.0 }); - UIWidgets::CVarCheckbox("Disable Wall Collision", "gNoWallColision", { .tooltip = "Disable wall collision." }); - UIWidgets::CVarSliderFloat( - "Min Height", "gMinHeight", -50.0f, 50.0f, 0.0f, - { .tooltip = "When Disable Wall Collision are enable what is the minimal height you can get." }); - ImGui::EndMenu(); - } + } const char* debugInfoPages[6] = { @@ -517,31 +449,7 @@ const char* debugInfoPages[6] = { }; void DrawDebugMenu() { - if (UIWidgets::BeginMenu("Developer")) { - UIWidgets::WindowButton( - "Gfx Debugger", "gGfxDebuggerEnabled", GameUI::mGfxDebuggerWindow, - { .tooltip = - "Enables the Gfx Debugger window, allowing you to input commands, type help for some examples" }); - - UIWidgets::CVarCheckbox("Debug mode", "gEnableDebugMode", { .tooltip = "Enable debug mode" }); - - UIWidgets::CVarCheckbox("Render Collision", "gRenderCollisionMesh", - { .tooltip = "Renders the collision mesh instead of the course mesh" }); - - UIWidgets::Spacer(0); - UIWidgets::WindowButton("GameInfo", "gGameInfoEnabled", GameUI::mGameInfoWindow, - { .tooltip = "Shows the game info window, contains player and actor information" }); - - UIWidgets::WindowButton( - "Stats", "gStatsEnabled", GameUI::mStatsWindow, - { .tooltip = "Shows the stats window, with your FPS and frametimes, and the OS you're playing on" }); - UIWidgets::WindowButton( - "Console", "gConsoleEnabled", GameUI::mConsoleWindow, - { .tooltip = "Enables the console window, allowing you to input commands, type help for some examples" }); - - ImGui::EndMenu(); - } } void GameMenuBar::DrawElement() { @@ -552,19 +460,19 @@ void GameMenuBar::DrawElement() { ImGui::SetCursorPosY(0.0f); - DrawSettingsMenu(); + //DrawSettingsMenu(); ImGui::SetCursorPosY(0.0f); - DrawEnhancementsMenu(); + //DrawEnhancementsMenu(); ImGui::SetCursorPosY(0.0f); - DrawCheatsMenu(); + //DrawCheatsMenu(); ImGui::SetCursorPosY(0.0f); - DrawDebugMenu(); + //DrawDebugMenu(); ImGui::EndMenuBar(); } diff --git a/src/port/ui/Menu.cpp b/src/port/ui/Menu.cpp new file mode 100644 index 000000000..0832fb2fd --- /dev/null +++ b/src/port/ui/Menu.cpp @@ -0,0 +1,801 @@ +#include "Menu.h" +#include "UIWidgets.h" +#include "port/Engine.h" +#include "window/gui/GuiMenuBar.h" +#include "window/gui/GuiElement.h" +#include <variant> +#include <spdlog/fmt/fmt.h> +#include <variant> +#include <tuple> + +extern "C" { + extern s32 gGamestateNext; + extern s32 gMenuSelection; + #include "audio/external.h" + #include "defines.h" +} +std::vector<ImVec2> windowTypeSizes = { {} }; + +extern std::unordered_map<s16, const char*> warpPointSceneList; +extern void Warp(); + +namespace BenGui {} + +namespace Ship { +std::string disabledTempTooltip; +const char* disabledTooltip; +bool disabledValue = false; + +bool operator==(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b; +} + +bool operator==(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return l.r == r.r && l.g == r.g && l.b == r.b && l.a == r.a; +} + +bool operator<(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b) || (l.r <= r.r && l.g < r.g && l.b <= r.b) || + (l.r <= r.r && l.g <= r.g && l.b < r.b); +} + +bool operator<(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r < r.r && l.g <= r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g < r.g && l.b <= r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b < r.b && l.a <= r.a) || + (l.r <= r.r && l.g <= r.g && l.b <= r.b && l.a < r.a); +} + +bool operator>(Color_RGB8 const& l, Color_RGB8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b) || (l.r >= r.r && l.g > r.g && l.b >= r.b) || + (l.r >= r.r && l.g >= r.g && l.b > r.b); +} + +bool operator>(Color_RGBA8 const& l, Color_RGBA8 const& r) noexcept { + return (l.r > r.r && l.g >= r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g > r.g && l.b >= r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b > r.b && l.a >= r.a) || + (l.r >= r.r && l.g >= r.g && l.b >= r.b && l.a > r.a); +} + +uint32_t GetVectorIndexOf(std::vector<std::string>& vector, std::string value) { + return std::distance(vector.begin(), std::find(vector.begin(), vector.end(), value)); +} + +void Menu::InsertSidebarSearch() { + menuEntries["Settings"].sidebars.emplace("Search", searchSidebarEntry); + uint32_t curIndex = 0; + if (!Ship_IsCStringEmpty(CVarGetString(menuEntries["Settings"].sidebarCvar, ""))) { + curIndex = GetVectorIndexOf(menuEntries["Settings"].sidebarOrder, + CVarGetString(menuEntries["Settings"].sidebarCvar, "")); + } + menuEntries["Settings"].sidebarOrder.insert(menuEntries["Settings"].sidebarOrder.begin() + searchSidebarIndex, + "Search"); + if (curIndex > searchSidebarIndex) { + CVarSetString(menuEntries["Settings"].sidebarCvar, menuEntries["Settings"].sidebarOrder.at(curIndex).c_str()); + } +} + +void Menu::RemoveSidebarSearch() { + uint32_t curIndex = + GetVectorIndexOf(menuEntries["Settings"].sidebarOrder, CVarGetString(menuEntries["Settings"].sidebarCvar, "")); + menuEntries["Settings"].sidebars.erase("Search"); + std::erase_if(menuEntries["Settings"].sidebarOrder, [](std::string& name) { return name == "Search"; }); + if (curIndex > searchSidebarIndex) { + curIndex--; + } else if (curIndex >= menuEntries["Settings"].sidebarOrder.size()) { + curIndex = menuEntries["Settings"].sidebarOrder.size() - 1; + } + CVarSetString(menuEntries["Settings"].sidebarCvar, menuEntries["Settings"].sidebarOrder.at(curIndex).c_str()); +} + +void Menu::UpdateWindowBackendObjects() { + Ship::WindowBackend runningWindowBackend = Ship::Context::GetInstance()->GetWindow()->GetWindowBackend(); + int32_t configWindowBackendId = Ship::Context::GetInstance()->GetConfig()->GetInt("Window.Backend.Id", -1); + if (Ship::Context::GetInstance()->GetWindow()->IsAvailableWindowBackend(configWindowBackendId)) { + configWindowBackend = static_cast<Ship::WindowBackend>(configWindowBackendId); + } else { + configWindowBackend = runningWindowBackend; + } + + availableWindowBackends = Ship::Context::GetInstance()->GetWindow()->GetAvailableWindowBackends(); + for (auto& backend : *availableWindowBackends) { + availableWindowBackendsMap[backend] = windowBackendsMap.at(backend); + } +} + +Menu::Menu(const std::string& cVar, const std::string& name, uint8_t searchSidebarIndex_, + UIWidgets::Colors defaultThemeIndex_) + : GuiWindow(cVar, name), searchSidebarIndex(searchSidebarIndex_), defaultThemeIndex(defaultThemeIndex_) { +} + +void Menu::InitElement() { + popped = CVarGetInteger("gSettings.Menu.Popout", 0); + poppedSize.x = CVarGetInteger("gSettings.Menu.PoppedWidth", 1280); + poppedSize.y = CVarGetInteger("gSettings.Menu.PoppedHeight", 800); + poppedPos.x = CVarGetInteger("gSettings.Menu.PoppedPos.x", 0); + poppedPos.y = CVarGetInteger("gSettings.Menu.PoppedPos.y", 0); + + UpdateWindowBackendObjects(); +} + +void Menu::UpdateElement() { +} + +bool ModernMenuSidebarEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID sidebarId = window->GetID(std::string(label + "##Sidebar").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + pos.y += style.FramePadding.y; + pos.x = window->WorkRect.GetCenter().x - labelSize.x / 2; + ImRect bb = { pos - style.FramePadding, pos + labelSize + style.FramePadding }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, sidebarId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, sidebarId, &hovered, &held); + if (pressed) { + ImGui::MarkItemEdited(sidebarId); + } + window->DrawList->AddRectFilled(pos - style.FramePadding, pos + labelSize + style.FramePadding, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +bool ModernMenuHeaderEntry(std::string label) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 pos = window->DC.CursorPos; + const ImGuiID headerId = window->GetID(std::string(label + "##Header").c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + ImRect bb = { pos, pos + labelSize + style.FramePadding * 2 }; + ImGui::ItemSize(bb, style.FramePadding.y); + ImGui::ItemAdd(bb, headerId); + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, headerId, &hovered, &held); + window->DrawList->AddRectFilled(bb.Min, bb.Max, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive + : hovered ? ImGuiCol_ButtonHovered + : ImGuiCol_Button), + 3.0f); + pos += style.FramePadding; + UIWidgets::RenderText(pos, label.c_str(), ImGui::FindRenderedTextEnd(label.c_str()), true); + return pressed; +} + +uint32_t Menu::DrawSearchResults(std::string& menuSearchText) { + auto menuThemeIndex = static_cast<UIWidgets::Colors>(CVarGetInteger("gSettings.Menu.Theme", defaultThemeIndex)); + ImGui::BeginChild("Search Results"); + int searchCount = 0; + for (auto& menuLabel : menuOrder) { + auto& menuEntry = menuEntries.at(menuLabel); + for (auto& sidebarLabel : menuEntry.sidebarOrder) { + auto& sidebar = menuEntry.sidebars[sidebarLabel]; + for (int i = 0; i < sidebar.columnWidgets.size(); i++) { + auto& column = sidebar.columnWidgets.at(i); + for (auto& info : column) { + if (info.type == WIDGET_SEARCH || info.type == WIDGET_SEPARATOR || info.type == WIDGET_SEPARATOR_TEXT || + info.isHidden) { + continue; + } + const char* tooltip = info.options->tooltip; + std::string widgetStr = std::string(info.name) + std::string(tooltip != NULL ? tooltip : ""); + std::transform(menuSearchText.begin(), menuSearchText.end(), menuSearchText.begin(), ::tolower); + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), + menuSearchText.end()); + std::transform(widgetStr.begin(), widgetStr.end(), widgetStr.begin(), ::tolower); + widgetStr.erase(std::remove(widgetStr.begin(), widgetStr.end(), ' '), widgetStr.end()); + if (widgetStr.find(menuSearchText) != std::string::npos) { + MenuDrawItem(info, 90 / sidebar.columnCount, menuThemeIndex); + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(UIWidgets::Colors::Gray)); + std::string origin = fmt::format(" ({} -> {}, Col {})", menuEntry.label, sidebarLabel, i + 1); + ImGui::Text("%s", origin.c_str()); + ImGui::PopStyleColor(); + searchCount++; + } + } + } + } + } + return searchCount; +} + +void Menu::AddMenuEntry(std::string entryName, const char* entryCvar) { + menuEntries.emplace(entryName, MainMenuEntry{ entryName, entryCvar }); + menuOrder.push_back(entryName); +} + +std::unordered_map<uint32_t, disabledInfo>& Menu::GetDisabledMap() { + return disabledMap; +} + +void Menu::MenuDrawItem(WidgetInfo& widget, uint32_t width, UIWidgets::Colors menuThemeIndex) { + disabledTempTooltip = "This setting is disabled because: \n\n"; + disabledValue = false; + disabledTooltip = " "; + + if (widget.preFunc != nullptr) { + widget.ResetDisables(); + widget.preFunc(widget); + if (widget.isHidden) { + return; + } + if (!widget.activeDisables.empty()) { + widget.options->disabled = true; + for (auto option : widget.activeDisables) { + disabledTempTooltip += std::string("- ") + disabledMap.at(option).reason + std::string("\n"); + } + widget.options->disabledTooltip = disabledTempTooltip.c_str(); + } + } + + if (widget.sameLine) { + ImGui::SameLine(); + } + + try { + switch (widget.type) { + case WIDGET_CHECKBOX: { + bool* pointer = std::get<bool*>(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Checkbox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast<UIWidgets::CheckboxOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Checkbox(UIWidgets::WrappedText(widget.name.c_str(), width).c_str(), pointer, + *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CVAR_CHECKBOX: { + auto options = std::static_pointer_cast<UIWidgets::CheckboxOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarCheckbox(UIWidgets::WrappedText(widget.name.c_str(), width).c_str(), widget.cVar, + *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_AUDIO_BACKEND: { + auto currentAudioBackend = Ship::Context::GetInstance()->GetAudio()->GetCurrentAudioBackend(); + UIWidgets::ComboboxOptions options = {}; + options.color = menuThemeIndex; + options.tooltip = "Sets the audio API used by the game. Requires a relaunch to take effect."; + options.disabled = Ship::Context::GetInstance()->GetAudio()->GetAvailableAudioBackends()->size() <= 1; + options.disabledTooltip = "Only one audio API is available on this platform."; + if (UIWidgets::Combobox("Audio API", ¤tAudioBackend, audioBackendsMap, options)) { + Ship::Context::GetInstance()->GetAudio()->SetCurrentAudioBackend(currentAudioBackend); + } + } break; + case WIDGET_VIDEO_BACKEND: { + UIWidgets::ComboboxOptions options = {}; + options.color = menuThemeIndex; + options.tooltip = "Sets the renderer API used by the game."; + options.disabled = availableWindowBackends->size() <= 1; + options.disabledTooltip = "Only one renderer API is available on this platform."; + if (UIWidgets::Combobox("Renderer API (Needs reload)", &configWindowBackend, availableWindowBackendsMap, + options)) { + Ship::Context::GetInstance()->GetConfig()->SetInt("Window.Backend.Id", + (int32_t)(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->SetString("Window.Backend.Name", + windowBackendsMap.at(configWindowBackend)); + Ship::Context::GetInstance()->GetConfig()->Save(); + UpdateWindowBackendObjects(); + } + } break; + case WIDGET_SEPARATOR: { + ImGui::Separator(); + } break; + case WIDGET_SEPARATOR_TEXT: { + if (widget.options->color != UIWidgets::Colors::NoColor) { + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(widget.options->color)); + } + ImGui::SeparatorText(widget.name.c_str()); + if (widget.options->color != UIWidgets::Colors::NoColor) { + ImGui::PopStyleColor(); + } + } break; + case WIDGET_TEXT: { + if (widget.options->color != UIWidgets::Colors::NoColor) { + ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(widget.options->color)); + } + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", widget.name.c_str()); + if (widget.options->color != UIWidgets::Colors::NoColor) { + ImGui::PopStyleColor(); + } + } break; + case WIDGET_COMBOBOX: { + int32_t* pointer = std::get<int32_t*>(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("Combobox Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast<UIWidgets::ComboboxOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Combobox(widget.name.c_str(), pointer, options->comboMap, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_CVAR_COMBOBOX: { + auto options = std::static_pointer_cast<UIWidgets::ComboboxOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarCombobox(widget.name.c_str(), widget.cVar, options->comboMap, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_SLIDER_INT: { + int32_t* pointer = std::get<int32_t*>(widget.valuePointer); + if (pointer == nullptr) { + SPDLOG_ERROR("int32 Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast<UIWidgets::IntSliderOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::SliderInt(widget.name.c_str(), pointer, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_CVAR_SLIDER_INT: { + auto options = std::static_pointer_cast<UIWidgets::IntSliderOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarSliderInt(widget.name.c_str(), widget.cVar, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + }; + } break; + case WIDGET_SLIDER_FLOAT: { + float* pointer = std::get<float*>(widget.valuePointer); + + if (pointer == nullptr) { + SPDLOG_ERROR("float Slider Widget requires a value pointer, currently nullptr"); + assert(false); + return; + } + auto options = std::static_pointer_cast<UIWidgets::FloatSliderOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::SliderFloat(widget.name.c_str(), pointer, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CVAR_SLIDER_FLOAT: { + auto options = std::static_pointer_cast<UIWidgets::FloatSliderOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::CVarSliderFloat(widget.name.c_str(), widget.cVar, *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_BUTTON: { + auto options = std::static_pointer_cast<UIWidgets::ButtonOptions>(widget.options); + options->color = menuThemeIndex; + if (UIWidgets::Button(widget.name.c_str(), *options)) { + if (widget.callback != nullptr) { + widget.callback(widget); + } + } + } break; + case WIDGET_CUSTOM: { + if (widget.customFunction != nullptr) { + widget.customFunction(widget); + } + } break; + case WIDGET_WINDOW_BUTTON: { + if (widget.windowName == nullptr || widget.windowName[0] == '\0') { + std::string msg = + fmt::format("Error drawing window contents for {}: windowName not defined", widget.name); + SPDLOG_ERROR(msg.c_str()); + break; + } + auto window = Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow(widget.windowName); + if (!window) { + std::string msg = + fmt::format("Error drawing window contents: windowName {} does not exist", widget.windowName); + SPDLOG_ERROR(msg.c_str()); + break; + } + auto options = std::static_pointer_cast<UIWidgets::ButtonOptions>(widget.options); + options->color = menuThemeIndex; + UIWidgets::WindowButton(widget.name.c_str(), widget.cVar, window, *options); + if (!window->IsVisible()) { + window->DrawElement(); + } + } break; + case WIDGET_SEARCH: { + UIWidgets::PushStyleButton(menuThemeIndex); + if (ImGui::Button("Clear")) { + menuSearch.Clear(); + } + ImGui::SameLine(); + if (CVarGetInteger("gSettings.Menu.SearchAutofocus", 0) && + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() && + !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + UIWidgets::PushStyleCombobox(menuThemeIndex); + ImGui::PushStyleColor(ImGuiCol_Border, UIWidgets::ColorValues.at(menuThemeIndex)); + menuSearch.Draw(); + ImGui::PopStyleColor(); + UIWidgets::PopStyleCombobox(); + UIWidgets::PopStyleButton(); + std::string menuSearchText(menuSearch.InputBuf); + + if (menuSearchText == "") { + ImGui::Text("Start typing to see results."); + return; + } + DrawSearchResults(menuSearchText); + ImGui::EndChild(); + } break; + default: + break; + } + if (widget.postFunc != nullptr) { + widget.postFunc(widget); + } + } catch (const std::bad_variant_access& e) { + SPDLOG_ERROR("Failed to draw menu item \"{}\" due to: {}", widget.name, e.what()); + assert(false); + } +} + +void Menu::Draw() { + if (!IsVisible()) { + return; + } + DrawElement(); + // Sync up the IsVisible flag if it was changed by ImGui + SyncVisibilityConsoleVariable(); +} + +void Menu::DrawElement() { + for (auto& [reason, info] : disabledMap) { + info.active = info.evaluation(info); + } + auto menuThemeIndex = static_cast<UIWidgets::Colors>(CVarGetInteger("gSettings.Menu.Theme", defaultThemeIndex)); + + windowHeight = ImGui::GetMainViewport()->WorkSize.y; + windowWidth = ImGui::GetMainViewport()->WorkSize.x; + auto windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings; + bool popout = CVarGetInteger("gSettings.Menu.Popout", 0) && allowPopout; + if (popout) { + windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking; + } + if (popout != popped) { + if (popout) { + windowHeight = poppedSize.y; + windowWidth = poppedSize.x; + ImGui::SetNextWindowSize({ static_cast<float>(windowWidth), static_cast<float>(windowHeight) }, + ImGuiCond_Always); + ImGui::SetNextWindowPos(poppedPos, ImGuiCond_Always); + } else if (popped) { + CVarSetFloat("gSettings.Menu.PoppedWidth", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedHeight", poppedSize.y); + CVarSave(); + } + } + popped = popout; + auto windowCond = ImGuiCond_Always; + if (!popout) { + ImGui::SetNextWindowSize({ static_cast<float>(windowWidth), static_cast<float>(windowHeight) }, windowCond); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), windowCond, { 0.5f, 0.5f }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + } + if (!ImGui::Begin("Main Menu", NULL, windowFlags)) { + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::End(); + return; + } + if (popped != popout) { + if (!popout) { + ImGui::PopStyleVar(); + } + CVarSetInteger("gSettings.Menu.Popout", popped); + CVarSetFloat("gSettings.Menu.PoppedWidth", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedHeight", poppedSize.y); + CVarSetFloat("gSettings.Menu.PoppedPos.x", poppedSize.x); + CVarSetFloat("gSettings.Menu.PoppedPos.y", poppedSize.y); + CVarSave(); + ImGui::End(); + return; + } + ImGui::PushFont(GameEngine::Instance->fontStandardLargest); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = ImGui::GetStyle(); + windowHeight = window->WorkRect.GetHeight(); + windowWidth = window->WorkRect.GetWidth(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); + const char* headerCvar = "gSettings.Menu.ActiveHeader"; + std::string headerIndex = CVarGetString(headerCvar, "Settings"); + ImVec2 pos = window->DC.CursorPos; + float centerX = pos.x + windowWidth / 2 - (style.ItemSpacing.x * (menuEntries.size() + 1)); + std::vector<ImVec2> headerSizes; + float headerWidth = style.ItemSpacing.x; + bool headerSearch = !CVarGetInteger("gSettings.Menu.SidebarSearch", 0); + if (headerSearch) { + headerWidth += 200.0f + style.ItemSpacing.x + style.FramePadding.x; + } + for (auto& label : menuOrder) { + ImVec2 size = ImGui::CalcTextSize(label.c_str()); + headerSizes.push_back(size); + headerWidth += size.x + style.FramePadding.x * 2; + if (label == headerIndex) { + headerWidth += style.ItemSpacing.x; + } + } + ImVec2 menuSize = { std::fminf(1280, windowWidth), std::fminf(800, windowHeight) }; + pos += window->WorkRect.GetSize() / 2 - menuSize / 2; + ImGui::SetNextWindowPos(pos); + ImGui::BeginChild("Menu Block", menuSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar); + + std::unordered_map<std::string, SidebarEntry>* sidebar; + float headerHeight = headerSizes.at(0).y + style.FramePadding.y * 2; + ImVec2 buttonSize = ImGui::CalcTextSize(ICON_FA_TIMES_CIRCLE) + style.FramePadding * 2; + bool scrollbar = false; + if (headerWidth > menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3) { + headerHeight += style.ScrollbarSize; + scrollbar = true; + } + UIWidgets::ButtonOptions options = {}; + options.size = UIWidgets::Sizes::Inline; + options.tooltip = "Close Menu (Esc)"; + if (UIWidgets::Button(ICON_FA_TIMES_CIRCLE, options)) { + ToggleVisibility(); + + // Update gamepad navigation after close based on if other menus are still visible + auto mImGuiIo = &ImGui::GetIO(); + if (CVarGetInteger(CVAR_IMGUI_CONTROLLER_NAV, 0) && + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetMenuOrMenubarVisible()) { + mImGuiIo->ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + } else { + mImGuiIo->ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + } + } + ImGui::SameLine(); + ImGui::SetNextWindowSizeConstraints({ 0, headerHeight }, { headerWidth, headerHeight }); + ImVec2 headerSelSize = { menuSize.x - buttonSize.x * 3 - style.ItemSpacing.x * 3, headerHeight }; + if (scrollbar) { + headerSelSize.y += style.ScrollbarSize; + } + bool autoFocus = CVarGetInteger("gSettings.Menu.SearchAutofocus", 0); + ImGui::BeginChild("Header Selection", headerSelSize, + ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_HorizontalScrollbar); + uint8_t curIndex = 0; + for (auto& label : menuOrder) { + if (curIndex != 0) { + ImGui::SameLine(); + } + auto& entry = menuEntries.at(label); + std::string nextIndex = label; + UIWidgets::PushStyleButton(menuThemeIndex); + if (headerIndex != label) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuHeaderEntry(entry.label)) { + if (headerSearch) { + menuSearch.Clear(); + } + CVarSetString(headerCvar, label.c_str()); + CVarSave(); + nextIndex = label; + } + if (headerIndex != label) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (headerIndex == label) { + sidebar = &entry.sidebars; + } + if (nextIndex != label) { + headerIndex = nextIndex; + } + curIndex++; + } + std::string menuSearchText = ""; + if (headerSearch) { + ImGui::SameLine(); + if (autoFocus && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() && + !ImGui::IsMouseClicked(0)) { + ImGui::SetKeyboardFocusHere(0); + } + auto color = UIWidgets::ColorValues.at(menuThemeIndex); + color.w = 0.2f; + ImGui::PushStyleColor(ImGuiCol_FrameBg, color); + menuSearch.Draw("##search", 200.0f); + menuSearchText = menuSearch.InputBuf; + menuSearchText.erase(std::remove(menuSearchText.begin(), menuSearchText.end(), ' '), menuSearchText.end()); + if (menuSearchText.length() < 1) { + ImGui::SameLine(headerWidth - 200.0f + style.ItemSpacing.x); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "Search..."); + } + ImGui::PopStyleColor(); + } + ImGui::EndChild(); + ImGui::SameLine(menuSize.x - (buttonSize.x * 2) - style.ItemSpacing.x); + UIWidgets::ButtonOptions options2 = {}; + options2.color = UIWidgets::Colors::Red; + options2.size = UIWidgets::Sizes::Inline; + options2.tooltip = "Reset" +#ifdef __APPLE__ + " (Command-R)" +#elif !defined(__SWITCH__) && !defined(__WIIU__) + " (Ctrl+R)" +#else + "" +#endif + ; + if (UIWidgets::Button(ICON_FA_UNDO, options2)) { + ProcessReset(); + } + ImGui::SameLine(); + UIWidgets::ButtonOptions options3 = {}; + options3.color = UIWidgets::Colors::Red; + options3.size = UIWidgets::Sizes::Inline; + options3.tooltip = "Quit 2S2H"; + if (UIWidgets::Button(ICON_FA_POWER_OFF, options3)) { + if (!popped) { + ToggleVisibility(); + } + Ship::Context::GetInstance()->GetWindow()->Close(); + } + ImGui::PopStyleVar(); + + pos.y += headerHeight + style.ItemSpacing.y; + pos.x = centerX - menuSize.x / 2 + (style.ItemSpacing.x * (menuEntries.size() + 1)); + window->DrawList->AddRectFilled(pos, pos + ImVec2{ menuSize.x, 4 }, ImGui::GetColorU32({ 255, 255, 255, 255 }), + true, style.WindowRounding); + pos.y += style.ItemSpacing.y; + float sectionHeight = menuSize.y - headerHeight - 4 - style.ItemSpacing.y * 2; + float columnHeight = sectionHeight - style.ItemSpacing.y * 4; + ImGui::SetNextWindowPos(pos + style.ItemSpacing * 2); + float sidebarWidth = 200 - style.ItemSpacing.x; + + const char* sidebarCvar = menuEntries.at(headerIndex).sidebarCvar; + + std::string sectionIndex = CVarGetString(sidebarCvar, ""); + if (!sidebar->contains(sectionIndex)) { + sectionIndex = sidebar->begin()->first; + } + float sectionCenterX = pos.x + (sidebarWidth / 2); + float topY = pos.y; + ImGui::SetNextWindowSizeConstraints({ sidebarWidth, 0 }, { sidebarWidth, columnHeight }); + ImGui::BeginChild((menuEntries.at(headerIndex).label + " Section").c_str(), { sidebarWidth, columnHeight * 3 }, + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize, ImGuiWindowFlags_NoTitleBar); + for (auto& sidebarLabel : menuEntries.at(headerIndex).sidebarOrder) { + std::string nextIndex = ""; + UIWidgets::PushStyleButton(menuThemeIndex); + if (sectionIndex != sidebarLabel) { + ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); + } + if (ModernMenuSidebarEntry(sidebarLabel)) { + if (headerSearch) { + menuSearch.Clear(); + } + CVarSetString(sidebarCvar, sidebarLabel.c_str()); + CVarSave(); + nextIndex = sidebarLabel; + } + if (sectionIndex != sidebarLabel) { + ImGui::PopStyleColor(); + } + UIWidgets::PopStyleButton(); + if (nextIndex != "") { + sectionIndex = nextIndex; + } + } + ImGui::EndChild(); + + ImGui::PushFont(GameEngine::Instance->fontMonoLarger); + pos = ImVec2{ sectionCenterX + (sidebarWidth / 2), topY } + style.ItemSpacing * 2; + window->DrawList->AddRectFilled(pos, pos + ImVec2{ 4, sectionHeight - style.FramePadding.y * 2 }, + ImGui::GetColorU32({ 255, 255, 255, 255 }), true, style.WindowRounding); + pos.x += 4 + style.ItemSpacing.x; + ImGui::SetNextWindowPos(pos + style.ItemSpacing); + float sectionWidth = menuSize.x - sidebarWidth - 4 - style.ItemSpacing.x * 4; + std::string sectionMenuId = sectionIndex + " Settings"; + int columns = sidebar->at(sectionIndex).columnCount; + size_t columnFuncs = sidebar->at(sectionIndex).columnWidgets.size(); + if (windowWidth < 800) { + columns = 1; + } + float columnWidth = (sectionWidth - style.ItemSpacing.x * columns) / columns; + bool useColumns = columns > 1; + if (!useColumns || (headerSearch && menuSearchText.length() > 0)) { + ImGui::SameLine(); + ImGui::SetNextWindowSizeConstraints({ sectionWidth, 0 }, { sectionWidth, columnHeight }); + ImGui::BeginChild(sectionMenuId.c_str(), { sectionWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + if (headerSearch && menuSearchText.length() > 0) { + uint32_t searchCount = DrawSearchResults(menuSearchText); + if (searchCount == 0) { + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("No results found").x) / 2); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "No results found"); + } + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("Clear Search").x) / 2 - 10.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f); + UIWidgets::ButtonOptions clearBtnOpts = {}; + clearBtnOpts.size = UIWidgets::Sizes::Inline; + if (UIWidgets::Button("Clear Search", clearBtnOpts)) { + menuSearch.Clear(); + } + + ImGui::EndChild(); + } else { + std::string menuLabel = menuEntries.at(headerIndex).label; + if (MenuInit::GetUpdateFuncs().contains(menuLabel)) { + if (MenuInit::GetUpdateFuncs()[menuLabel].contains(sectionIndex)) { + for (auto& updateFunc : MenuInit::GetUpdateFuncs()[menuLabel][sectionIndex]) { + updateFunc(); + } + } + } + for (int i = 0; i < columnFuncs; i++) { + std::string sectionId = fmt::format("{} Column {}", sectionMenuId, i); + if (useColumns) { + ImGui::SetNextWindowSizeConstraints({ columnWidth, 0 }, { columnWidth, columnHeight }); + ImGui::BeginChild(sectionId.c_str(), { columnWidth, windowHeight * 4 }, ImGuiChildFlags_AutoResizeY, + ImGuiWindowFlags_NoTitleBar); + } + // for (auto& entryName : sidebar->at(sectionIndex).sidebarOrder) { + for (auto& entry : sidebar->at(sectionIndex).columnWidgets.at(i)) { + MenuDrawItem(entry, 90 / sidebar->at(sectionIndex).columnCount, menuThemeIndex); + } + //} + if (useColumns) { + ImGui::EndChild(); + } + if (i < columns - 1) { + ImGui::SameLine(); + } + } + } + if (!useColumns || menuSearchText.length() > 0) { + ImGui::EndChild(); + } + ImGui::PopFont(); + ImGui::PopFont(); + + if (!popout) { + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + if (popout) { + poppedSize = ImGui::GetWindowSize(); + poppedPos = ImGui::GetWindowPos(); + } + ImGui::End(); +} +} // namespace Ship diff --git a/src/port/ui/Menu.h b/src/port/ui/Menu.h new file mode 100644 index 000000000..e4aa3ac00 --- /dev/null +++ b/src/port/ui/Menu.h @@ -0,0 +1,66 @@ +#ifndef MENU_H +#define MENU_H + +#include <libultraship/libultraship.h> +#include "UIWidgets.h" +#include "graphic/Fast3D/gfx_rendering_api.h" +#include "MenuTypes.h" + +namespace Ship { +uint32_t GetVectorIndexOf(std::vector<std::string>& vector, std::string value); +class Menu : public GuiWindow { + public: + using GuiWindow::GuiWindow; + + Menu(const std::string& cVar, const std::string& name, uint8_t searchSidebarIndex_ = 0, + UIWidgets::Colors menuThemeIndex_ = UIWidgets::Colors::LightBlue); + virtual ~Menu() {} + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + void Draw() override; + void InsertSidebarSearch(); + void RemoveSidebarSearch(); + void UpdateWindowBackendObjects(); + + void MenuDrawItem(WidgetInfo& widget, uint32_t width, UIWidgets::Colors menuThemeIndex); + void AddMenuEntry(std::string entryName, const char* entryCvar); + std::unordered_map<uint32_t, disabledInfo>& GetDisabledMap(); + + protected: + ImVec2 mOriginalSize; + std::string mName; + uint32_t mWindowFlags; + std::unordered_map<std::string, MainMenuEntry> menuEntries; + std::vector<std::string> menuOrder; + uint32_t DrawSearchResults(std::string& menuSearchText); + ImGuiTextFilter menuSearch; + uint8_t searchSidebarIndex; + UIWidgets::Colors defaultThemeIndex; + std::shared_ptr<std::vector<Ship::WindowBackend>> availableWindowBackends; + std::unordered_map<Ship::WindowBackend, const char*> availableWindowBackendsMap; + Ship::WindowBackend configWindowBackend; + + std::unordered_map<uint32_t, disabledInfo> disabledMap; + std::vector<disabledInfo> disabledVector; + const SidebarEntry searchSidebarEntry = { + .columnCount = 1, + .columnWidgets = { { { .name = "Sidebar Search", + .type = WIDGET_SEARCH, + .options = std::make_shared<UIWidgets::WidgetOptions>(UIWidgets::WidgetOptions{}.Tooltip( + "Searches all menus for the given text, including tooltips.")) } } } + }; + virtual void ProcessReset() {} + + private: + bool allowPopout = true; // PortNote: should be set to false on small screen ports + bool popped; + ImVec2 poppedSize; + ImVec2 poppedPos; + float windowHeight; + float windowWidth; +}; +} // namespace Ship + +#endif // MENU_H diff --git a/src/port/ui/MenuTypes.h b/src/port/ui/MenuTypes.h new file mode 100644 index 000000000..72ac48846 --- /dev/null +++ b/src/port/ui/MenuTypes.h @@ -0,0 +1,303 @@ +#ifndef MENUTYPES_H +#define MENUTYPES_H + +#include <libultraship/libultraship.h> +#include "UIWidgets.h" + +typedef enum { + DISABLE_FOR_FREE_CAM_ON, + DISABLE_FOR_FREE_CAM_OFF, + DISABLE_FOR_DEBUG_MODE_OFF, + DISABLE_FOR_NO_VSYNC, + DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + DISABLE_FOR_NO_MULTI_VIEWPORT, + DISABLE_FOR_NOT_DIRECTX, + DISABLE_FOR_DIRECTX, + DISABLE_FOR_MATCH_REFRESH_RATE_ON, + DISABLE_FOR_ADVANCED_RESOLUTION_ON, + DISABLE_FOR_VERTICAL_RES_TOGGLE_ON, + DISABLE_FOR_LOW_RES_MODE_ON, + //DISABLE_FOR_MULTIPLAYER_CONNECTED, +} DisableOption; + +struct WidgetInfo; +struct disabledInfo; +using VoidFunc = void (*)(); +using DisableInfoFunc = bool (*)(disabledInfo&); +using DisableVec = std::vector<DisableOption>; +using WidgetFunc = void (*)(WidgetInfo&); + +typedef enum { + WIDGET_CHECKBOX, + WIDGET_COMBOBOX, + WIDGET_SLIDER_INT, + WIDGET_SLIDER_FLOAT, + WIDGET_CVAR_CHECKBOX, + WIDGET_CVAR_COMBOBOX, + WIDGET_CVAR_SLIDER_INT, + WIDGET_CVAR_SLIDER_FLOAT, + WIDGET_BUTTON, + WIDGET_COLOR_24, // color picker without alpha + WIDGET_COLOR_32, // color picker with alpha + WIDGET_SEARCH, + WIDGET_SEPARATOR, + WIDGET_SEPARATOR_TEXT, + WIDGET_TEXT, + WIDGET_WINDOW_BUTTON, + WIDGET_AUDIO_BACKEND, // needed for special operations that can't be handled easily with the normal combobox widget + WIDGET_VIDEO_BACKEND, // same as above + WIDGET_CUSTOM, +} WidgetType; + +typedef enum { + SECTION_COLUMN_1, + SECTION_COLUMN_2, + SECTION_COLUMN_3, +} SectionColumns; + +typedef enum { + MOTION_BLUR_DYNAMIC, + MOTION_BLUR_ALWAYS_OFF, + MOTION_BLUR_ALWAYS_ON, +} MotionBlurOption; + +typedef enum { + DEBUG_LOG_TRACE, + DEBUG_LOG_DEBUG, + DEBUG_LOG_INFO, + DEBUG_LOG_WARN, + DEBUG_LOG_ERROR, + DEBUG_LOG_CRITICAL, + DEBUG_LOG_OFF, +} DebugLogOption; + +// holds the widget values for a widget, contains all CVar types available from LUS. int32_t is used for boolean +// evaluation +using CVarVariant = std::variant<int32_t, const char*, float, Color_RGBA8, Color_RGB8>; +using OptionsVariant = + std::variant<UIWidgets::ButtonOptions, UIWidgets::CheckboxOptions, UIWidgets::ComboboxOptions, + UIWidgets::FloatSliderOptions, UIWidgets::IntSliderOptions, UIWidgets::WidgetOptions>; + +// All the info needed for display and search of all widgets in the menu. +// `name` is the label displayed, +// `cVar` is the string representation of the CVar used to store the widget value +// `tooltip` is what is displayed when hovering (except when disabled, more on that later) +// `type` is the WidgetType for the widget, which is what determines how the information is used in the draw func +// `options` is a variant that holds the UIWidgetsOptions struct for the widget type +// blank objects need to be initialized with specific typing matching the expected Options struct for the widget +// `callback` is a lambda used for running code on widget change. may need `BenGui::GetMenu()` for specific menu actions +// `preFunc` is a lambda called before drawing code starts. It can be used to determine a widget's status, +// whether disabled or hidden, as well as update pointers for non-CVar widget types. +// `postFunc` is a lambda called after all drawing code is finished, for reacting to states other than +// widgets having been changed, like holding buttons. +// All three lambdas accept a `widgetInfo` reference in case it needs information on the widget for these operations +// `activeDisables` is a vector of DisableOptions for specifying what reasons a widget is disabled, which are displayed +// in the disabledTooltip for the widget. Can display multiple reasons. Handling the reasons is done in `preFunc`. +// It is recommended to utilize `disabledInfo`/`DisableReason` to list out all reasons for disabling and isHidden so +// the info can be shown. +// `windowName` is what is displayed and searched for `windowButton` type and window interactions +// `isHidden` just prevents the widget from being drawn under whatever circumstances you specify in the `preFunc` +// `sameLine` allows for specifying that the widget should be on the same line as the previous widget +struct WidgetInfo { + std::string name; // Used by all widgets + const char* cVar; // Used by all widgets except + WidgetType type; + std::shared_ptr<UIWidgets::WidgetOptions> options; + std::variant<bool*, int32_t*, float*> valuePointer; + WidgetFunc callback = nullptr; + WidgetFunc preFunc = nullptr; + WidgetFunc postFunc = nullptr; + WidgetFunc customFunction = nullptr; + DisableVec activeDisables = {}; + const char* windowName = ""; + bool isHidden = false; + bool sameLine = false; + + WidgetInfo& CVar(const char* cVar_) { + cVar = cVar_; + return *this; + } + WidgetInfo& Options(OptionsVariant options_) { + switch (type) { + case WIDGET_AUDIO_BACKEND: + case WIDGET_VIDEO_BACKEND: + case WIDGET_COMBOBOX: + case WIDGET_CVAR_COMBOBOX: + options = std::make_shared<UIWidgets::ComboboxOptions>(std::get<UIWidgets::ComboboxOptions>(options_)); + break; + case WIDGET_CHECKBOX: + case WIDGET_CVAR_CHECKBOX: + options = std::make_shared<UIWidgets::CheckboxOptions>(std::get<UIWidgets::CheckboxOptions>(options_)); + break; + case WIDGET_SLIDER_FLOAT: + case WIDGET_CVAR_SLIDER_FLOAT: + options = + std::make_shared<UIWidgets::FloatSliderOptions>(std::get<UIWidgets::FloatSliderOptions>(options_)); + break; + case WIDGET_SLIDER_INT: + case WIDGET_CVAR_SLIDER_INT: + options = + std::make_shared<UIWidgets::IntSliderOptions>(std::get<UIWidgets::IntSliderOptions>(options_)); + break; + case WIDGET_BUTTON: + case WIDGET_WINDOW_BUTTON: + options = std::make_shared<UIWidgets::ButtonOptions>(std::get<UIWidgets::ButtonOptions>(options_)); + break; + case WIDGET_CUSTOM: + case WIDGET_TEXT: + case WIDGET_SEPARATOR_TEXT: + case WIDGET_SEPARATOR: + default: + options = std::make_shared<UIWidgets::WidgetOptions>(std::get<UIWidgets::WidgetOptions>(options_)); + } + return *this; + } + void ResetDisables() { + isHidden = false; + options->disabled = false; + options->disabledTooltip = ""; + activeDisables.clear(); + } + WidgetInfo& Options(std::shared_ptr<UIWidgets::WidgetOptions> options_) { + options = options_; + return *this; + } + WidgetInfo& Callback(WidgetFunc callback_) { + callback = callback_; + return *this; + } + WidgetInfo& PreFunc(WidgetFunc preFunc_) { + preFunc = preFunc_; + return *this; + } + WidgetInfo& PostFunc(WidgetFunc postFunc_) { + postFunc = postFunc_; + return *this; + } + WidgetInfo& WindowName(const char* windowName_) { + windowName = windowName_; + return *this; + } + WidgetInfo& ValuePointer(std::variant<bool*, int32_t*, float*> valuePointer_) { + valuePointer = valuePointer_; + return *this; + } + WidgetInfo& SameLine(bool sameLine_) { + sameLine = sameLine_; + return *this; + } + WidgetInfo& CustomFunction(WidgetFunc customFunction_) { + customFunction = customFunction_; + return *this; + } +}; + +struct WidgetPath { + std::string sectionName; + std::string sidebarName; + SectionColumns column; +}; + +// `disabledInfo` holds information on reasons for hiding or disabling a widget, as well as an evaluation lambda that +// is run once per frame to update its status (this is done to prevent dozens of redundant CVarGets in each frame loop) +// `evaluation` returns a bool which can be determined by whatever code you want that changes its status +// `reason` is the text displayed in the disabledTooltip when a widget is disabled by a particular DisableReason +// `active` is what's referenced when determining disabled status for a widget that uses this This can also be used to +// hold reasons to hide widgets so that their evaluations are also only run once per frame +struct disabledInfo { + DisableInfoFunc evaluation; + const char* reason; + bool active = false; + int32_t value = 0; +}; + +// struct Sidebar { +// //std::unordered_map<std::string, SidebarEntry> entries; +// uint32_t columnCount; +// std::vector<std::vector<WidgetInfo>> columnWidgets; +// +// void Insert(std::string entryName, WidgetInfo& entry, int32_t index = -1) { +// if (index == -1 || index >= entryOrder.size()) { +// entryOrder.push_back(entryName); +// } else { +// entryOrder.insert(entryOrder.begin() + index, entryName); +// } +// columnWidgets[entryName].push_back({entry}); +// } +// +// void Erase(std::string entryName) { +// if (columnWidgets.contains(entryName)) { +// columnWidgets.erase(entryName); +// } +// std::erase_if(entryOrder, [entryName](std::string name) { return name == entryName; }); +// } +// }; + +// Contains the name displayed in the sidebar (label), the number of columns to use in drawing (columnCount; for visual +// separation, 1-3), and nested vectors of the widgets, grouped by column (columnWidgets). The number of widget vectors +// added to the column groups does not need to match the specified columnCount, e.g. you can have one vector added to +// the sidebar, but still separate the window into 3 columns and display only in the first column +struct SidebarEntry { + uint32_t columnCount; + std::vector<std::vector<WidgetInfo>> columnWidgets; +}; + +// Contains entries for what's listed in the header at the top, including the name displayed on the top bar (label), +// a vector of the SidebarEntries for that header entry, and the name of the cvar used to track what sidebar entry is +// the last viewed for that header. +struct MainMenuEntry { + std::string label; + const char* sidebarCvar; + std::unordered_map<std::string, SidebarEntry> sidebars = {}; + std::vector<std::string> sidebarOrder = {}; +}; + +static const std::unordered_map<Ship::AudioBackend, const char*> audioBackendsMap = { + { Ship::AudioBackend::WASAPI, "Windows Audio Session API" }, + { Ship::AudioBackend::SDL, "SDL" }, +}; + +static const std::unordered_map<Ship::WindowBackend, const char*> windowBackendsMap = { + { Ship::WindowBackend::FAST3D_DXGI_DX11, "DirectX" }, + { Ship::WindowBackend::FAST3D_SDL_OPENGL, "OpenGL" }, + { Ship::WindowBackend::FAST3D_SDL_METAL, "Metal" }, +}; + +struct MenuInit { + static std::vector<std::function<void()>>& GetInitFuncs() { + static std::vector<std::function<void()>> menuInitFuncs; + return menuInitFuncs; + } + + static std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::function<void()>>>>& + GetUpdateFuncs() { + static std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::function<void()>>>> + menuUpdateFuncs; + return menuUpdateFuncs; + } + + static void InitAll() { + auto& menuInitFuncs = MenuInit::GetInitFuncs(); + for (const auto& initFunc : menuInitFuncs) { + initFunc(); + } + } +}; + +struct RegisterMenuInitFunc { + RegisterMenuInitFunc(std::function<void()> initFunc) { + auto& menuInitFuncs = MenuInit::GetInitFuncs(); + + menuInitFuncs.push_back(initFunc); + } +}; + +struct RegisterMenuUpdateFunc { + RegisterMenuUpdateFunc(std::function<void()> updateFunc, std::string sectionName, std::string sidebarName) { + auto& menuUpdateFuncs = MenuInit::GetUpdateFuncs(); + + menuUpdateFuncs[sectionName][sidebarName].push_back(updateFunc); + } +}; + +#endif // MENUTYPES_H diff --git a/src/port/ui/MultiplayerWindow.cpp b/src/port/ui/MultiplayerWindow.cpp index 0345dbaa5..51b6fbf11 100644 --- a/src/port/ui/MultiplayerWindow.cpp +++ b/src/port/ui/MultiplayerWindow.cpp @@ -1,5 +1,6 @@ #include "MultiplayerWindow.h" #include "UIWidgets.h" +#include "PortMenu.h" #include "libultraship/src/Context.h" #include <imgui.h> @@ -9,7 +10,7 @@ #include <defines.h> extern "C" { -#include "../../../src/main.h" +#include "main.h" #include "networking/networking.h" #include "actor_types.h" #include "code_800029B0.h" @@ -21,135 +22,145 @@ extern Camera cameras[]; extern UnkStruct_800DC5EC* D_800DC5EC; } +namespace GameUI { +extern std::shared_ptr<PortMenu> mPortMenu; namespace Multiplayer { -MultiplayerWindow::~MultiplayerWindow() { - SPDLOG_TRACE("destruct multiplayer window"); -} - -void MultiplayerWindow::InitElement() { -} static s32 sConnectedBool = false; static s32 sReadyUpBool = false; static s32 sCharacter = 0; static s32 sCup = 0; -void MultiplayerWindow::DrawElement() { - const float framerate = ImGui::GetIO().Framerate; - const float deltatime = ImGui::GetIO().DeltaTime; - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); +void DrawColumn1(WidgetInfo& info) { + if (gNetwork.isConnected) { + ImGui::BeginDisabled(); + } - if (!gNetwork.isConnected) { + static char username[NETWORK_USERNAME_LENGTH] = "TestUser"; - static char username[NETWORK_USERNAME_LENGTH] = "TestUser"; + ImGui::Text("Username:"); + ImGui::InputText("##edit", username, IM_ARRAYSIZE(username)); - ImGui::Text("Username:"); - ImGui::InputText("##edit", username, IM_ARRAYSIZE(username)); + static char address[32] = "127.0.0.1:64010"; + static char addressCopy[32]; + strncpy(addressCopy, address, sizeof(addressCopy)); + addressCopy[sizeof(addressCopy) - 1] = '\0'; // Ensure null termination - static char address[32] = "127.0.0.1:64010"; - static char addressCopy[32]; - strncpy(addressCopy, address, sizeof(addressCopy)); - addressCopy[sizeof(addressCopy) - 1] = '\0'; // Ensure null termination + ImGui::Text("Host:"); + ImGui::InputText("##edit3", addressCopy, IM_ARRAYSIZE(addressCopy)); - ImGui::Text("Host:"); - ImGui::InputText("##edit3", addressCopy, IM_ARRAYSIZE(addressCopy)); + if (ImGui::Button("Connect!")) { + char* ip = NULL; + uint16_t port = 0; - if (ImGui::Button("Connect!")) { - char* ip = NULL; - uint16_t port = 0; + char* token = strtok(addressCopy, ":"); + ip = token; - char* token = strtok(addressCopy, ":"); - ip = token; + token = strtok(NULL, ":"); + if (token != NULL) { + port = (uint16_t) atoi(token); + } - token = strtok(NULL, ":"); - if (token != NULL) { - port = (uint16_t) atoi(token); - } + gNetwork.enabled = true; + ConnectToServer(ip, port, username); + } + if (gNetwork.isConnected) { + ImGui::EndDisabled(); + } +} - gNetwork.enabled = true; - ConnectToServer(ip, port, username); - } +void DrawColumn2(WidgetInfo& info) { + if (!gNetwork.isConnected) { + ImGui::BeginDisabled(); + } - } else { + ImGui::Text("Select Character:"); + ImGui::Spacing(); - ImGui::Spacing(); - ImGui::Text("Select Character:"); - ImGui::Spacing(); - - for (size_t i = 0; i < NUM_PLAYERS; i++) { - // These are euc-jp characters that look sort of like a hyphen - - if (D_800E76A8[i] == NULL || D_800E76A8[i] == "\xA1\xBC\xA1\xBC\xA1\xBC\xA1\xBC") { - break; - } - ImGui::SameLine(); - if (ImGui::Button(D_800E76A8[i])) { - network_character_vote(i); - sCharacter = i; - } + for (size_t i = 0; i < NUM_PLAYERS; i++) { + // These are euc-jp characters that look sort of like a hyphen - + if (D_800E76A8[i] == NULL || D_800E76A8[i] == "\xA1\xBC\xA1\xBC\xA1\xBC\xA1\xBC") { + break; } - ImGui::Spacing(); - ImGui::Text("-- %s --", D_800E76A8[sCharacter]); - - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::Text("Course Vote:"); - - for (size_t i = 0; i < NUM_CUPS - 1; i++) { - if (ImGui::Button(gCupNames[i])) { - network_cup_vote(i); - sCup = i; - } - ImGui::SameLine(); + ImGui::SameLine(); + if (ImGui::Button(D_800E76A8[i])) { + network_character_vote(i); + sCharacter = i; } - ImGui::Spacing(); - ImGui::Text("-- %s --", gCupNames[sCup]); + } + ImGui::Spacing(); + ImGui::Text("-- %s --", D_800E76A8[sCharacter]); - if (ImGui::Button("test", ImVec2(-1, 0))) { - // Add disconnect logic here - cameras[0].playerId = 2; - D_800DC5EC->player = &gPlayers[2]; - } + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Text("Course Vote:"); - for (size_t i = 0; i < NETWORK_MAX_PLAYERS; i++) { - ImGui::Text("Slot %d: char: %d, hasAuthority: %d", clients[i].slot, clients[i].character, - gPlayers[i].nHasAuthority); + for (size_t i = 0; i < NUM_CUPS - 1; i++) { + if (ImGui::Button(gCupNames[i])) { + network_cup_vote(i); + sCup = i; } + ImGui::SameLine(); + } + ImGui::Spacing(); + ImGui::Text("-- %s --", gCupNames[sCup]); - /* Stick UI to the bottom of the panel */ + if (ImGui::Button("test", ImVec2(-1, 0))) { + // Add disconnect logic here + cameras[0].playerId = 2; + D_800DC5EC->player = &gPlayers[2]; + } - float windowHeight = ImGui::GetWindowHeight(); + for (size_t i = 0; i < NETWORK_MAX_PLAYERS; i++) { + ImGui::Text("Slot %d: char: %d, hasAuthority: %d", clients[i].slot, clients[i].character, + gPlayers[i].nHasAuthority); + } - // Move the cursor to a position relative to the bottom of the window - ImGui::SetCursorPosY(windowHeight - - ImGui::GetFrameHeightWithSpacing() * 2); // Adjust Y position for two buttons + /* Stick UI to the bottom of the panel */ - char buttonLabel[32]; + float windowHeight = ImGui::GetWindowHeight(); - if (sReadyUpBool) { - strcpy(buttonLabel, "Lets Go!"); - } else { - strcpy(buttonLabel, "Ready Up!"); - } + // Move the cursor to a position relative to the bottom of the window + ImGui::SetCursorPosY(windowHeight - + ImGui::GetFrameHeightWithSpacing() * 2); // Adjust Y position for two buttons - // First button (Ready Up / Let's Go) - if (ImGui::Button(buttonLabel, ImVec2(-1, 0))) { - sReadyUpBool = !sReadyUpBool; - networking_ready_up(sReadyUpBool); - } + char buttonLabel[32]; - // Add some vertical spacing between the buttons - ImGui::Spacing(); + if (sReadyUpBool) { + strcpy(buttonLabel, "Lets Go!"); + } else { + strcpy(buttonLabel, "Ready Up!"); + } - // Second button (Disconnect) - if (ImGui::Button("Disconnect", ImVec2(-1, 0))) { - // Add disconnect logic here - networking_disconnect(); - } + // First button (Ready Up / Let's Go) + if (ImGui::Button(buttonLabel, ImVec2(-1, 0))) { + sReadyUpBool = !sReadyUpBool; + networking_ready_up(sReadyUpBool); } - ImGui::PopStyleColor(); + // Add some vertical spacing between the buttons + ImGui::Spacing(); + + // Second button (Disconnect) + if (ImGui::Button("Disconnect", ImVec2(-1, 0))) { + // Add disconnect logic here + networking_disconnect(); + } + if (!gNetwork.isConnected) { + ImGui::EndDisabled(); + } } -void MultiplayerWindow::UpdateElement() { +void RegisterMultiplayerWidgets() { + mPortMenu->AddSidebarEntry("Enhancements", "Multiplayer", 2); + WidgetPath path = { "Enhancements", "Multiplayer", SECTION_COLUMN_1 }; + mPortMenu->AddWidget(path, "Multi Custom 1", WIDGET_CUSTOM) + .CustomFunction(DrawColumn1); + path.column = SECTION_COLUMN_2; + mPortMenu->AddWidget(path, "Multi Custom 2", WIDGET_CUSTOM) + .CustomFunction(DrawColumn2); } + +static RegisterMenuInitFunc initFunc(RegisterMultiplayerWidgets); } // namespace Multiplayer +} // namespace GameUI diff --git a/src/port/ui/MultiplayerWindow.h b/src/port/ui/MultiplayerWindow.h index aad42ba2b..d196c33c0 100644 --- a/src/port/ui/MultiplayerWindow.h +++ b/src/port/ui/MultiplayerWindow.h @@ -3,14 +3,4 @@ #include <libultraship/libultraship.h> namespace Multiplayer { -class MultiplayerWindow : public Ship::GuiWindow { - public: - using Ship::GuiWindow::GuiWindow; - ~MultiplayerWindow(); - - private: - void InitElement() override; - void DrawElement() override; - void UpdateElement() override; -}; } // namespace Multiplayer diff --git a/src/port/ui/PortMenu.cpp b/src/port/ui/PortMenu.cpp new file mode 100644 index 000000000..b1e9cfde2 --- /dev/null +++ b/src/port/ui/PortMenu.cpp @@ -0,0 +1,497 @@ +#include "PortMenu.h" +#include "UIWidgets.h" +#include "port/Game.h" +#include "window/gui/GuiMenuBar.h" +#include "window/gui/GuiElement.h" +#include <variant> +#include "StringHelper.h" +#include <spdlog/fmt/fmt.h> +#include <variant> +#include <tuple> +#include "ResolutionEditor.h" + +extern "C" { + extern s32 gGamestateNext; + extern s32 gMenuSelection; + #include "audio/external.h" + #include "defines.h" +} + +namespace GameUI { +extern std::shared_ptr<PortMenu> mPortMenu; + +using namespace UIWidgets; + +void PortMenu::AddSidebarEntry(std::string sectionName, std::string sidebarName, uint32_t columnCount) { + assert(!sectionName.empty()); + assert(!sidebarName.empty()); + menuEntries.at(sectionName).sidebars.emplace(sidebarName, SidebarEntry{ .columnCount = columnCount }); + menuEntries.at(sectionName).sidebarOrder.push_back(sidebarName); +} + +WidgetInfo& PortMenu::AddWidget(WidgetPath& pathInfo, std::string widgetName, WidgetType widgetType) { + assert(!widgetName.empty()); // Must be unique + assert(menuEntries.contains(pathInfo.sectionName)); // Section/header must already exist + assert(menuEntries.at(pathInfo.sectionName).sidebars.contains(pathInfo.sidebarName)); // Sidebar must already exist + std::unordered_map<std::string, SidebarEntry>& sidebar = menuEntries.at(pathInfo.sectionName).sidebars; + uint8_t column = pathInfo.column; + if (sidebar.contains(pathInfo.sidebarName)) { + while (sidebar.at(pathInfo.sidebarName).columnWidgets.size() < column + 1) { + sidebar.at(pathInfo.sidebarName).columnWidgets.push_back({}); + } + } + SidebarEntry& entry = sidebar.at(pathInfo.sidebarName); + entry.columnWidgets.at(column).push_back({ .name = widgetName, .type = widgetType }); + WidgetInfo& widget = entry.columnWidgets.at(column).back(); + switch (widgetType) { + case WIDGET_CHECKBOX: + case WIDGET_CVAR_CHECKBOX: + widget.options = std::make_shared<CheckboxOptions>(); + break; + case WIDGET_SLIDER_FLOAT: + case WIDGET_CVAR_SLIDER_FLOAT: + widget.options = std::make_shared<FloatSliderOptions>(); + break; + case WIDGET_SLIDER_INT: + case WIDGET_CVAR_SLIDER_INT: + widget.options = std::make_shared<IntSliderOptions>(); + break; + case WIDGET_COMBOBOX: + case WIDGET_CVAR_COMBOBOX: + case WIDGET_AUDIO_BACKEND: + case WIDGET_VIDEO_BACKEND: + widget.options = std::make_shared<ComboboxOptions>(); + break; + case WIDGET_BUTTON: + widget.options = std::make_shared<ButtonOptions>(); + break; + case WIDGET_WINDOW_BUTTON: + widget.options = std::make_shared<ButtonOptions>(ButtonOptions{ .size = Sizes::Inline }); + break; + case WIDGET_COLOR_24: + case WIDGET_COLOR_32: + break; + case WIDGET_SEARCH: + case WIDGET_SEPARATOR: + case WIDGET_SEPARATOR_TEXT: + case WIDGET_TEXT: + default: + widget.options = std::make_shared<WidgetOptions>(); + } + return widget; +} + +void PortMenu::AddSettings() { + // Add Settings menu + AddMenuEntry("Settings", "gSettings.Menu.SettingsSidebarSection"); + // General Settings + AddSidebarEntry("Settings", "General", 3); + WidgetPath path = { "Settings", "General", SECTION_COLUMN_1 }; + AddWidget(path, "Menu Theme", WIDGET_CVAR_COMBOBOX) + .CVar("gSettings.Menu.Theme") + .Options(ComboboxOptions() + .Tooltip("Changes the Theme of the Menu Widgets.") + .ComboMap(menuThemeOptions) + .DefaultIndex(Colors::LightBlue)); +#if not defined(__SWITCH__) and not defined(__WIIU__) + AddWidget(path, "Menu Controller Navigation", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_IMGUI_CONTROLLER_NAV) + .Options(CheckboxOptions().Tooltip( + "Allows controller navigation of the Spaghetti menu (Settings, Enhancements,...)\nCAUTION: " + "This will disable game inputs while the menu is visible.\n\nD-pad to move between " + "items, A to select, B to move up in scope.")); + AddWidget(path, "Cursor Always Visible", WIDGET_CVAR_CHECKBOX) + .CVar("gSettings.CursorVisibility") + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetForceCursorVisibility( + CVarGetInteger("gSettings.CursorVisibility", 0)); + }) + .Options(CheckboxOptions().Tooltip("Makes the cursor always visible, even in full screen.")); +#endif + AddWidget(path, "Search In Sidebar", WIDGET_CVAR_CHECKBOX) + .CVar("gSettings.Menu.SidebarSearch") + .Callback([](WidgetInfo& info) { + if (CVarGetInteger("gSettings.Menu.SidebarSearch", 0)) { + mPortMenu->InsertSidebarSearch(); + } else { + mPortMenu->RemoveSidebarSearch(); + } + }) + .Options(CheckboxOptions().Tooltip( + "Displays the Search menu as a sidebar entry in Settings instead of in the header.")); + AddWidget(path, "Search Input Autofocus", WIDGET_CVAR_CHECKBOX) + .CVar("gSettings.Menu.SearchAutofocus") + .Options(CheckboxOptions().Tooltip( + "Search input box gets autofocus when visible. Does not affect using other widgets.")); + AddWidget(path, "Alt Assets Tab hotkey", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Mods.AlternateAssetsHotkey") + .Options( + CheckboxOptions().Tooltip("Allows pressing the Tab key to toggle alternate assets").DefaultValue(true)); + AddWidget(path, "Open App Files Folder", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { + std::string filesPath = Ship::Context::GetInstance()->GetAppDirectoryPath(); + SDL_OpenURL(std::string("file:///" + std::filesystem::absolute(filesPath).string()).c_str()); + }) + .Options(ButtonOptions().Tooltip("Opens the folder that contains the save and mods folders, etc.")); + + // Audio Settings + path.sidebarName = "Audio"; + AddSidebarEntry("Settings", "Audio", 3); + AddWidget(path, "Master Volume: %.0f%%", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gGameMasterVolume") + .Options(FloatSliderOptions() + .Tooltip("Adjust the overall sound volume.") + .ShowButtons(false) + .Format("") + .IsPercentage()); + AddWidget(path, "Main Music Volume: %.0f%%", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gMainMusicVolume") + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_LEVEL, CVarGetFloat("gMainMusicVolume", 1.0f)); + }) + .Options(FloatSliderOptions() + .Tooltip("Adjust the background music volume.") + .ShowButtons(false) + .Format("") + .IsPercentage()); + AddWidget(path, "Sound Effects Volume: %.0f%%", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gSFXMusicVolume") + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_SFX, CVarGetFloat("gSFXMusicVolume", 1.0f)); + }) + .Options( + FloatSliderOptions().Tooltip("Adjust the sound effects volume.").ShowButtons(false).Format("").IsPercentage()); + AddWidget(path, "Sound Effects Volume: %.0f%%", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gEnvironmentVolume") + .Callback([](WidgetInfo& info) { + audio_set_player_volume(SEQ_PLAYER_ENV, CVarGetFloat("gEnvironmentVolume", 1.0f)); + }) + .Options(FloatSliderOptions() + .Tooltip("Adjust the environment volume.") + .ShowButtons(false) + .Format("") + .IsPercentage()); + AddWidget(path, "Audio API", WIDGET_AUDIO_BACKEND); + + // Graphics Settings + static int32_t maxFps; + static bool isFullscreen = false; + const char* tooltip = ""; + if (Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == Ship::WindowBackend::FAST3D_DXGI_DX11) { + maxFps = 360; + tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc.\n\nA higher target " + "FPS than your monitor's refresh rate will waste resources, and might give a worse result."; + } else { + maxFps = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); + tooltip = "Uses Matrix Interpolation to create extra frames, resulting in smoother graphics. This is " + "purely visual and does not impact game logic, execution of glitches etc."; + } + path.sidebarName = "Graphics"; + AddSidebarEntry("Settings", "Graphics", 3); + AddWidget(path, "Toggle Fullscreen", WIDGET_CHECKBOX) + .ValuePointer(&isFullscreen) + .PreFunc([](WidgetInfo& info) { isFullscreen = Ship::Context::GetInstance()->GetWindow()->IsFullscreen(); }) + .Callback([](WidgetInfo& info) { Ship::Context::GetInstance()->GetWindow()->ToggleFullscreen(); }) + .Options(CheckboxOptions().Tooltip("Toggles Fullscreen On/Off.")); +#ifndef __APPLE__ + AddWidget(path, "Internal Resolution: %.0f%%", WIDGET_CVAR_SLIDER_FLOAT) + .CVar(CVAR_INTERNAL_RESOLUTION) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier( + CVarGetFloat(CVAR_INTERNAL_RESOLUTION, 1)); + }) + .PreFunc([](WidgetInfo& info) { + if (mPortMenu->disabledMap.at(DISABLE_FOR_ADVANCED_RESOLUTION_ON).active && + mPortMenu->disabledMap.at(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_ADVANCED_RESOLUTION_ON); + info.activeDisables.push_back(DISABLE_FOR_VERTICAL_RES_TOGGLE_ON); + } else if (mPortMenu->disabledMap.at(DISABLE_FOR_LOW_RES_MODE_ON).active) { + info.activeDisables.push_back(DISABLE_FOR_LOW_RES_MODE_ON); + } + }) + .Options( + FloatSliderOptions() + .Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but effective " + "form of anti-aliasing.") + .ShowButtons(false) + .IsPercentage() + .Format("") + .Min(0.5f) + .Max(2.0f)); +#endif +#ifndef __WIIU__ + AddWidget(path, "Anti-aliasing (MSAA): %d", WIDGET_CVAR_SLIDER_INT) + .CVar(CVAR_MSAA_VALUE) + .Callback([](WidgetInfo& info) { + Ship::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger(CVAR_MSAA_VALUE, 1)); + }) + .Options( + IntSliderOptions() + .Tooltip("Activates MSAA (multi-sample anti-aliasing) from 2x up to 8x, to smooth the edges of " + "rendered geometry.\n" + "Higher sample count will result in smoother edges on models, but may reduce performance.") + .Min(1) + .Max(8) + .DefaultValue(1)); +#endif + + AddWidget(path, "Current FPS: %d", WIDGET_CVAR_SLIDER_INT) + .CVar("gInterpolationFPS") + .Callback([](WidgetInfo& info) { + int32_t defaultValue = std::static_pointer_cast<IntSliderOptions>(info.options)->defaultValue; + if (CVarGetInteger(info.cVar, defaultValue) == defaultValue) { + info.name = "Current FPS: Original (%d)"; + } else { + info.name = "Current FPS: %d"; + } + }) + .PreFunc([](WidgetInfo& info) { + if (mPortMenu->disabledMap.at(DISABLE_FOR_MATCH_REFRESH_RATE_ON).active) + info.activeDisables.push_back(DISABLE_FOR_MATCH_REFRESH_RATE_ON); + }) + .Options(IntSliderOptions().Tooltip(tooltip).Min(20).Max(maxFps).DefaultValue(20)); + AddWidget(path, "Match Refresh Rate", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { + int hz = Ship::Context::GetInstance()->GetWindow()->GetCurrentRefreshRate(); + if (hz >= 20 && hz <= 360) { + CVarSetInteger("gInterpolationFPS", hz); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + }) + .PreFunc([](WidgetInfo& info) { info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_NOT_DIRECTX).active; }) + .Options(ButtonOptions().Tooltip("Matches interpolation value to the current game's window refresh rate.")); + AddWidget(path, "Match Refresh Rate", WIDGET_CVAR_CHECKBOX) + .CVar("gMatchRefreshRate") + .PreFunc([](WidgetInfo& info) { info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_DIRECTX).active; }) + .Options(CheckboxOptions().Tooltip("Matches interpolation value to the current game's window refresh rate.")); + AddWidget(path, "Jitter fix : >= % d FPS", WIDGET_CVAR_SLIDER_INT) + .CVar("gExtraLatencyThreshold") + .PreFunc([](WidgetInfo& info) { info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_NOT_DIRECTX).active; }) + .Options(IntSliderOptions() + .Tooltip("When Interpolation FPS setting is at least this threshold, add one frame of input " + "lag (e.g. 16.6 ms for 60 FPS) in order to avoid jitter. This setting allows the " + "CPU to work on one frame while GPU works on the previous frame.\nThis setting " + "should be used when your computer is too slow to do CPU + GPU work in time.") + .Min(0) + .Max(360) + .DefaultValue(80)); + AddWidget(path, "Renderer API (Needs reload)", WIDGET_VIDEO_BACKEND); + AddWidget(path, "Enable Vsync", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_VSYNC_ENABLED) + .PreFunc([](WidgetInfo& info) { info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_NO_VSYNC).active; }) + .Options(CheckboxOptions().Tooltip("Enables Vsync.")); + AddWidget(path, "Windowed Fullscreen", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SDL_WINDOWED_FULLSCREEN) + .PreFunc([](WidgetInfo& info) { + info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_NO_WINDOWED_FULLSCREEN).active; + }) + .Options(CheckboxOptions().Tooltip("Enables Windowed Fullscreen Mode.")); + AddWidget(path, "Allow multi-windows", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENABLE_MULTI_VIEWPORTS) + .PreFunc( + [](WidgetInfo& info) { info.isHidden = mPortMenu->disabledMap.at(DISABLE_FOR_NO_MULTI_VIEWPORT).active; }) + .Options(CheckboxOptions().Tooltip( + "Allows multiple windows to be opened at once. Requires a reload to take effect.")); + AddWidget(path, "Texture Filter (Needs reload)", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_TEXTURE_FILTER) + .Options(ComboboxOptions().Tooltip("Sets the applied Texture Filtering.").ComboMap(textureFilteringMap)); + + path.sidebarName = "Controls"; + AddSidebarEntry("Settings", "Controls", 1); + AddWidget(path, + "This interface can be a little daunting. Please bear with us as we work to improve the experience " + "and address some known issues.\n" + "\n" + "At first glance, you may notice several input devices displayed below the 'Clear All' button. " + "Some of these might be other controllers connected to your computer, while others may be " + "duplicated controllers (a known issue). We recommend clicking on the box with the " ICON_FA_EYE + " icon and the name of any disconnected or unused controllers to hide their inputs. Make sure the " + "target controller remains visible.\n" + "\n" + "If you encounter issues connecting your controller or registering inputs, try closing Steam or " + "any other external input software. Alternatively, test a different controller to determine if " + "it's a compatibility issue.\n", + WIDGET_TEXT); + AddWidget(path, "Bindings", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Popout Bindings Window", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_CONTROLLER_CONFIGURATION_WINDOW_OPEN) + .WindowName("Input Editor") + .Options(ButtonOptions().Tooltip("Enables the separate Bindings Window.").Size(Sizes::Inline)); +} +int32_t motionBlurStrength; + +void PortMenu::AddEnhancements() { + AddMenuEntry("Enhancements", "gSettings.Menu.EnhancementsSidebarSection"); + WidgetPath path = { "Enhancements", "General", SECTION_COLUMN_1 }; + AddSidebarEntry("Enhancements", "General", 3); + //UIWidgets::WindowButton("Multiplayer", "gMultiplayerWindowEnabled", GameUI::mMultiplayerWindow, + // { .tooltip = "Shows the multiplayer window" }); + // UIWidgets::WindowButton("Freecam", "gFreecam", GameUI::mFreecamWindow, + // { .tooltip = "Allows you to fly around the course" }); + AddWidget(path, "No multiplayer feature cuts", WIDGET_CVAR_CHECKBOX) + .CVar("gMultiplayerNoFeatureCuts") + .Options(CheckboxOptions().Tooltip("Allows full train and jumbotron in multiplayer, etc.")); + AddWidget(path, "General Improvements", WIDGET_CVAR_CHECKBOX) + .CVar("gImprovements").Options(CheckboxOptions().Tooltip("General improvements to the game experience.")); + AddWidget(path, "No Level of Detail (LOD)", WIDGET_CVAR_CHECKBOX) + .CVar("gDisableLod") + .Options(CheckboxOptions().Tooltip("Disable Level of Detail (LOD) to avoid models using lower poly versions at a distance")); + AddWidget(path, "Disable Culling", WIDGET_CVAR_CHECKBOX) + .CVar("gNoCulling").Options(CheckboxOptions().Tooltip("Disable original culling of mk64")); + AddWidget(path, "Far Frustrum", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gFarFrustrum") + .Options(FloatSliderOptions().Min(0.0f).Max(10000.0f).DefaultValue(10000.0f) + .Tooltip("Say how Far the Frustrum are when 'Disable Culling' are enable").Step(10.0f)); + + path = { "Enhancements", "Cheats", SECTION_COLUMN_1 }; + AddSidebarEntry("Enhancements", "Cheats", 3); + AddWidget(path, "Moon Jump", WIDGET_CVAR_CHECKBOX) + .CVar("gEnableMoonJump"); + AddWidget(path, "Enable Custom CC", WIDGET_CVAR_CHECKBOX) + .CVar("gEnableCustomCC"); + AddWidget(path, "Custom CC", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gCustomCC") + .Options(FloatSliderOptions().Min(0.0f).Max(1000.0f).DefaultValue(150.0f).Step(10.0f)); + AddWidget(path, "Disable Wall Collision", WIDGET_CVAR_CHECKBOX) + .CVar("gNoWallColision") + .Options(CheckboxOptions().Tooltip("Disable wall collision.")); + AddWidget(path, "Min Height", WIDGET_CVAR_SLIDER_FLOAT) + .CVar("gMinHeight") + .Options(FloatSliderOptions().Min(-50.0f).Max(50.0f).DefaultValue(0.0f) + .Tooltip("When Disable Wall Collision are enable what is the minimal height you can get.")); +} + +void PortMenu::AddDevTools() { + AddMenuEntry("Developer", "gSettings.Menu.DevToolsSidebarSection"); + AddSidebarEntry("Developer", "General", 3); + WidgetPath path = { "Developer", "General", SECTION_COLUMN_1 }; + AddWidget(path, "Popout Menu", WIDGET_CVAR_CHECKBOX) + .CVar("gSettings.Menu.Popout") + .Options(CheckboxOptions().Tooltip("Changes the menu display from overlay to windowed.")); + AddWidget(path, "Debug Mode", WIDGET_CVAR_CHECKBOX) + .CVar("gEnableDebugMode") + .Options(CheckboxOptions().Tooltip("Enables Debug Mode.")); + AddWidget(path, "Render Collision", WIDGET_CVAR_CHECKBOX) + .CVar("gRenderCollisionMesh") + .Options(CheckboxOptions().Tooltip("Renders the collision mesh instead of the course mesh")); + + path = { "Developer", "Gfx Debugger", SECTION_COLUMN_1 }; + AddSidebarEntry("Developer", "Gfx Debugger", 1); + AddWidget(path, "Popout Gfx Debugger", WIDGET_WINDOW_BUTTON) + .CVar("gGfxDebuggerEnabled") + .Options(ButtonOptions().Tooltip( + "Enables the Gfx Debugger window, allowing you to input commands. Type help for some examples")) + .WindowName("GfxDebuggerWindow"); + + path = { "Developer", "Game Info", SECTION_COLUMN_1 }; + AddSidebarEntry("Developer", "Game Info", 1); + AddWidget(path, "Popout Game Info", WIDGET_WINDOW_BUTTON) + .CVar("gGameInfoEnabled") + .Options(ButtonOptions().Tooltip( + "Shows the game info window, contains player and actor information")) + .WindowName("GfxDebuggerWindow"); + + path = { "Developer", "Stats", SECTION_COLUMN_1 }; + AddSidebarEntry("Developer", "Stats", 1); + AddWidget(path, "Popout Stats", WIDGET_WINDOW_BUTTON) + .CVar("gStatsEnabled") + .Options(ButtonOptions().Tooltip("Shows the stats window, with your FPS and frametimes, and the OS you're playing on")) + .WindowName("Stats"); + + path = { "Developer", "Console", SECTION_COLUMN_1 }; + AddSidebarEntry("Developer", "Console", 1); + AddWidget(path, "Popout Console", WIDGET_WINDOW_BUTTON) + .CVar("gConsoleEnabled") + .Options(ButtonOptions().Tooltip( + "Enables the console window, allowing you to input commands. Type help for some examples")) + .WindowName("Console"); +} + +PortMenu::PortMenu(const std::string& consoleVariable, const std::string& name) + : Menu(consoleVariable, name, 0, UIWidgets::Colors::LightBlue) { +} + +//bool CheckNetworkConnected(disabledInfo& info) { +// return gNetwork.isConnected; +//} + +void PortMenu::InitElement() { + Ship::Menu::InitElement(); + AddSettings(); + AddEnhancements(); + AddDevTools(); + + if (CVarGetInteger("gSettings.Menu.SidebarSearch", 0)) { + InsertSidebarSearch(); + } + + for (auto& initFunc : MenuInit::GetInitFuncs()) { + initFunc(); + } + + disabledMap = { + { DISABLE_FOR_FREE_CAM_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger("gFreecam", 0); }, "Free Cam is Enabled" } }, + { DISABLE_FOR_FREE_CAM_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gFreecam", 0); }, "Free Cam is Disabled" } }, + { DISABLE_FOR_DEBUG_MODE_OFF, + { [](disabledInfo& info) -> bool { return !CVarGetInteger("gEnableDebugMode", 0); }, + "Debug Mode is Disabled" } }, + { DISABLE_FOR_NO_VSYNC, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->CanDisableVerticalSync(); + }, + "Disabling VSync not supported" } }, + { DISABLE_FOR_NO_WINDOWED_FULLSCREEN, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->SupportsWindowedFullscreen(); + }, + "Windowed Fullscreen not supported" } }, + { DISABLE_FOR_NO_MULTI_VIEWPORT, + { [](disabledInfo& info) -> bool { + return !Ship::Context::GetInstance()->GetWindow()->GetGui()->SupportsViewports(); + }, + "Multi-viewports not supported" } }, + { DISABLE_FOR_NOT_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() != + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Available Only on DirectX" } }, + { DISABLE_FOR_DIRECTX, + { [](disabledInfo& info) -> bool { + return Ship::Context::GetInstance()->GetWindow()->GetWindowBackend() == + Ship::WindowBackend::FAST3D_DXGI_DX11; + }, + "Not Available on DirectX" } }, + { DISABLE_FOR_MATCH_REFRESH_RATE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger("gMatchRefreshRate", 0); }, + "Match Refresh Rate is Enabled" } }, + { DISABLE_FOR_ADVANCED_RESOLUTION_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); }, + "Advanced Resolution Enabled" } }, + { DISABLE_FOR_VERTICAL_RES_TOGGLE_ON, + { [](disabledInfo& info) -> bool { + return CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); + }, + "Vertical Resolution Toggle Enabled" } }, + { DISABLE_FOR_LOW_RES_MODE_ON, + { [](disabledInfo& info) -> bool { return CVarGetInteger(CVAR_LOW_RES_MODE, 0); }, "N64 Mode Enabled" } }, + //{ DISABLE_FOR_MULTIPLAYER_CONNECTED, + // { CheckNetworkConnected, "Multiplayer Connected"}}, + }; +} + +void PortMenu::UpdateElement() { + Ship::Menu::UpdateElement(); +} + +void PortMenu::Draw() { + Ship::Menu::Draw(); +} + +void PortMenu::DrawElement() { + Ship::Menu::DrawElement(); +} +} // namespace BenGui diff --git a/src/port/ui/PortMenu.h b/src/port/ui/PortMenu.h new file mode 100644 index 000000000..209166a9e --- /dev/null +++ b/src/port/ui/PortMenu.h @@ -0,0 +1,64 @@ +#ifndef PORTMENU_H +#define PORTMENU_H + +#include <libultraship/libultraship.h> +#include "UIWidgets.h" +#include "Menu.h" +#include "graphic/Fast3D/gfx_rendering_api.h" + +namespace GameUI { + +static const std::unordered_map<int32_t, const char*> menuThemeOptions = { + { UIWidgets::Colors::Red, "Red" }, + { UIWidgets::Colors::DarkRed, "Dark Red" }, + { UIWidgets::Colors::Orange, "Orange" }, + { UIWidgets::Colors::Green, "Green" }, + { UIWidgets::Colors::DarkGreen, "Dark Green" }, + { UIWidgets::Colors::LightBlue, "Light Blue" }, + { UIWidgets::Colors::Blue, "Blue" }, + { UIWidgets::Colors::DarkBlue, "Dark Blue" }, + { UIWidgets::Colors::Indigo, "Indigo" }, + { UIWidgets::Colors::Violet, "Violet" }, + { UIWidgets::Colors::Purple, "Purple" }, + { UIWidgets::Colors::Brown, "Brown" }, + { UIWidgets::Colors::Gray, "Gray" }, + { UIWidgets::Colors::DarkGray, "Dark Gray" }, +}; + +static const std::unordered_map<int32_t, const char*> textureFilteringMap = { + { FILTER_THREE_POINT, "Three-Point" }, + { FILTER_LINEAR, "Linear" }, + { FILTER_NONE, "None" }, +}; + +static const std::unordered_map<int32_t, const char*> motionBlurOptions = { + { MOTION_BLUR_DYNAMIC, "Dynamic (default)" }, + { MOTION_BLUR_ALWAYS_OFF, "Always Off" }, + { MOTION_BLUR_ALWAYS_ON, "Always On" }, +}; + +static const std::unordered_map<int32_t, const char*> logLevels = { + { DEBUG_LOG_TRACE, "Trace" }, { DEBUG_LOG_DEBUG, "Debug" }, { DEBUG_LOG_INFO, "Info" }, + { DEBUG_LOG_WARN, "Warn" }, { DEBUG_LOG_ERROR, "Error" }, { DEBUG_LOG_CRITICAL, "Critical" }, + { DEBUG_LOG_OFF, "Off" }, +}; + +class PortMenu : public Ship::Menu { + public: + PortMenu(const std::string& consoleVariable, const std::string& name); + ~PortMenu() {} + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; + void Draw() override; + + void AddSidebarEntry(std::string sectionName, std::string sidbarName, uint32_t columnCount); + WidgetInfo& AddWidget(WidgetPath& pathInfo, std::string widgetName, WidgetType widgetType); + void AddSettings(); + void AddEnhancements(); + void AddDevTools(); +}; +} // namespace BenGui + +#endif // PORTMENU_H diff --git a/src/port/ui/ResolutionEditor.cpp b/src/port/ui/ResolutionEditor.cpp index dbc9259d7..c1fa6f263 100644 --- a/src/port/ui/ResolutionEditor.cpp +++ b/src/port/ui/ResolutionEditor.cpp @@ -1,422 +1,542 @@ #include "ResolutionEditor.h" -#include "UIWidgets.h" -#include "libultraship/src/Context.h" - #include <imgui.h> #include <libultraship/libultraship.h> + +#include "UIWidgets.h" #include <graphic/Fast3D/gfx_pc.h> +#include "port/Engine.h" +#include "PortMenu.h" -/* Console Variables are grouped under gAdvancedResolution. (e.g. "gAdvancedResolution.Enabled") +/* Console Variables are grouped under gAdvancedResolution. (e.g. CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled") - The following CVars are used in Libultraship and can be edited here: + The following cvars are used in Libultraship and can be edited here: - Enabled - Turns Advanced Resolution Mode on. - AspectRatioX, AspectRatioY - Aspect ratio controls. To toggle off, set either to zero. - VerticalPixelCount, VerticalResolutionToggle - Resolution controls. - PixelPerfectMode, IntegerScale.Factor - Pixel Perfect Mode a.k.a. integer scaling controls. - (Waiting on a second PR merge on LUS for this to fully function.): - IntegerScale.FitAutomatically - Automatic resizing for Pixel Perfect Mode. - IntegerScale.NeverExceedBounds - Prevents manual resizing from exceeding screen bounds. - The following CVars are also implemented in LUS for niche use cases: + The following cvars are also implemented in LUS for niche use cases: - IgnoreAspectCorrection - Stretch framebuffer to fill screen. This is something of a power-user setting for niche setups that most people won't need or care about, but may be useful if playing the Switch/Wii U ports on a 4:3 television. - IntegerScale.ExceedBoundsBy - Offset the max screen bounds, usually by +1. - This isn't that useful at the moment. + This isn't that useful at the moment, so it's unused here. */ -namespace AdvancedResolutionSettings { +namespace GameUI { +extern std::shared_ptr<PortMenu> mPortMenu; enum setting { UPDATE_aspectRatioX, UPDATE_aspectRatioY, UPDATE_verticalPixelCount }; -const char* aspectRatioPresetLabels[] = { - "Off", "Custom", "Original (4:3)", "Widescreen (16:9)", "Nintendo 3DS (5:3)", "16:10 (8:5)", "Ultrawide (21:9)" -}; -const float aspectRatioPresetsX[] = { 0.0f, 12.0f, 4.0f, 16.0f, 5.0f, 16.0f, 21.0f }; +std::unordered_map<int32_t, const char*> aspectRatioPresetLabels = { { 0, "Off" }, + { 1, "Custom" }, + { 2, "Original (4:3)" }, + { 3, "Widescreen (16:9)" }, + { 4, "Nintendo 3DS (5:3)" }, + { 5, "16:10 (8:5)" }, + { 6, "Ultrawide (21:9)" } }; +const float aspectRatioPresetsX[] = { 0.0f, 16.0f, 4.0f, 16.0f, 5.0f, 16.0f, 21.0f }; const float aspectRatioPresetsY[] = { 0.0f, 9.0f, 3.0f, 9.0f, 3.0f, 10.0f, 9.0f }; const int default_aspectRatio = 1; // Default combo list option const char* pixelCountPresetLabels[] = { "Custom", "Native N64 (240p)", "2x (480p)", "3x (720p)", "4x (960p)", "5x (1200p)", "6x (1440p)", "Full HD (1080p)", "4K (2160p)" }; -const int pixelCountPresets[] = { 480, 240, 480, 720, 960, 1200, 1440, 1080, 2160, 480 }; +const int pixelCountPresets[] = { 480, 240, 480, 720, 960, 1200, 1440, 1080, 2160 }; const int default_pixelCount = 0; // Default combo list option -const uint32_t minVerticalPixelCount = 240; // see: Ship::AdvancedResolution() -const uint32_t maxVerticalPixelCount = 4320; +// Resolution clamp values as hardcoded in LUS::Gui::ApplyResolutionChanges() +const uint32_t minVerticalPixelCount = SCREEN_HEIGHT; +const uint32_t maxVerticalPixelCount = 4320; // 18x native, or 8K TV resolution const unsigned short default_maxIntegerScaleFactor = 6; // Default size of Integer scale factor slider. -const float enhancementSpacerHeight = 19.0f; -// This will need to be determined more intelligently when Hi-DPI UI support is added. - -void AdvancedResolutionSettingsWindow::InitElement() { -} - -void AdvancedResolutionSettingsWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(497, 532), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Advanced Resolution Settings", &mIsVisible)) { - // Initialise update flags. - bool update[sizeof(setting)]; - for (unsigned short i = 0; i < sizeof(setting); i++) - update[i] = false; - static short updateCountdown = 0; - short countdownStartingValue = CVarGetInteger("gInterpolationFPS", 30) / 2; // half of a second, in frames. - - // Initialise integer scale bounds. - short max_integerScaleFactor = default_maxIntegerScaleFactor; // default value, which may or may not get - // overridden depending on viewport res - - short integerScale_maximumBounds = 1; // can change when window is resized - // This is mostly just for UX purposes, as Fit Automatically logic is part of LUS. - if (((float) gfx_current_game_window_viewport.width / gfx_current_game_window_viewport.height) > - ((float) gfx_current_dimensions.width / gfx_current_dimensions.height)) { - // Scale to window height - integerScale_maximumBounds = gfx_current_game_window_viewport.height / gfx_current_dimensions.height; - } else { - // Scale to window width - integerScale_maximumBounds = gfx_current_game_window_viewport.width / gfx_current_dimensions.width; - } - // Lower-clamping maximum bounds value to 1 is no-longer necessary as that's accounted for in LUS. - // Letting it go below 1 in this Editor will even allow for checking if screen bounds are being exceeded. - if (default_maxIntegerScaleFactor < integerScale_maximumBounds) { - max_integerScaleFactor = - integerScale_maximumBounds + CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); - } - - // Stored Values for non-UIWidgets elements - static float aspectRatioX = CVarGetFloat("gAdvancedResolution.AspectRatioX", 16.0f); - static float aspectRatioY = CVarGetFloat("gAdvancedResolution.AspectRatioY", 9.0f); - static int verticalPixelCount = CVarGetInteger("gAdvancedResolution.VerticalPixelCount", 480); - // Combo List defaults - static int item_aspectRatio = default_aspectRatio; - static int item_pixelCount = default_pixelCount; - // Additional settings - static bool showHorizontalResField = false; - static int horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - +enum messageType { MESSAGE_ERROR, MESSAGE_WARNING, MESSAGE_QUESTION, MESSAGE_INFO, MESSAGE_GRAY_75 }; +const ImVec4 messageColor[]{ + { 0.85f, 0.0f, 0.0f, 1.0f }, // MESSAGE_ERROR + { 0.85f, 0.85f, 0.0f, 1.0f }, // MESSAGE_WARNING + { 0.0f, 0.85f, 0.85f, 1.0f }, // MESSAGE_QUESTION + { 0.0f, 0.85f, 0.55f, 1.0f }, // MESSAGE_INFO + { 0.75f, 0.75f, 0.75f, 1.0f } // MESSAGE_GRAY_75 +}; +static const float enhancementSpacerHeight = 19.0f; +// Initialise update flags. +static bool update[3]; + +// Initialise integer scale bounds. +static short max_integerScaleFactor = default_maxIntegerScaleFactor; // default value, which may or may not get + // overridden depending on viewport res + +static short integerScale_maximumBounds = 1; // can change when window is resized + +// Combo List defaults +static int32_t item_aspectRatio; +static int32_t item_pixelCount; +// Stored Values for non-UIWidgets elements +static float aspectRatioX; +static float aspectRatioY; +static int32_t verticalPixelCount; +// Additional settings +static bool showHorizontalResField; +static int32_t horizontalPixelCount; +// Disabling flags +static bool disabled_everything; +static bool disabled_pixelCount; + +using namespace UIWidgets; + +void RegisterResolutionWidgets() { + WidgetPath path = { "Settings", "Graphics", SECTION_COLUMN_2 }; #ifdef __APPLE__ - ImGui::Text("Note: these settings may behave incorrectly on Apple Retina Displays."); - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + mPortMenu + ->AddWidget(path, ICON_FA_INFO_CIRCLE " These settings may behave incorrectly on Retina displays.", WIDGET_TEXT) + .Options(WidgetOptions().Color(Colors::Green)); #endif - // The original resolution slider (for convenience) - if (UIWidgets::EnhancementSliderFloat("Internal Resolution: %d %%", "##IMul", "gInternalResolution", 0.5f, 2.0f, - "", 1.0f, true, true, - (CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0) && - CVarGetInteger("gAdvancedResolution.Enabled", 0)) || - CVarGetInteger("gLowResMode", 0))) { - Ship::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); - } - UIWidgets::Tooltip("Multiplies your output resolution by the value entered, as a more intensive but effective " - "form of anti-aliasing"); // Description pulled from SohMenuBar.cpp - - // N64 Mode toggle (again for convenience) - // UIWidgets::PaddedEnhancementCheckbox("(Enhancements>Graphics) N64 Mode", "gLowResMode", false, false, false, - // "", UIWidgets::CheckboxGraphics::Cross, false); - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - // Activator - UIWidgets::PaddedEnhancementCheckbox("Enable advanced settings.", "gAdvancedResolution.Enabled", false, false, - false, "", UIWidgets::CheckboxGraphics::Cross, false); - // Error/Warning display - if (!CVarGetInteger("gLowResMode", 0)) { - if (IsDroppingFrames()) { // Significant frame drop warning - ImGui::TextColored({ 0.85f, 0.85f, 0.0f, 1.0f }, - ICON_FA_EXCLAMATION_TRIANGLE " Significant frame rate (FPS) drops may be occuring."); - UIWidgets::Spacer(2); - } else { // No warnings - UIWidgets::Spacer(enhancementSpacerHeight); - } - } else { // N64 Mode warning - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - ICON_FA_QUESTION_CIRCLE " \"N64 Mode\" is overriding these settings."); - ImGui::SameLine(); - if (ImGui::Button("Click to disable")) { - CVarSetInteger("gLowResMode", (int) false); - CVarSave(); - } - } - // Resolution visualiser - ImGui::Text("Viewport dimensions: %d x %d", gfx_current_game_window_viewport.width, - gfx_current_game_window_viewport.height); - ImGui::Text("Internal resolution: %d x %d", gfx_current_dimensions.width, gfx_current_dimensions.height); - - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - // Aspect Ratio - ImGui::Text("Force aspect ratio:"); - ImGui::SameLine(); - ImGui::TextColored({ 0.75f, 0.75f, 0.75f, 1.0f }, "(Select \"Off\" to disable.)"); - if (ImGui::Combo(" ", &item_aspectRatio, aspectRatioPresetLabels, - IM_ARRAYSIZE(aspectRatioPresetLabels)) && - item_aspectRatio != default_aspectRatio) { // don't change anything if "Custom" is selected. - aspectRatioX = aspectRatioPresetsX[item_aspectRatio]; - aspectRatioY = aspectRatioPresetsY[item_aspectRatio]; - update[UPDATE_aspectRatioX] = true; - update[UPDATE_aspectRatioY] = true; + // Resolution visualiser + mPortMenu->AddWidget(path, "Viewport dimensions: {} x {}", WIDGET_TEXT).PreFunc([](WidgetInfo& info) { + info.name = fmt::format("Viewport dimensions: {} x {}", gfx_current_game_window_viewport.width, + gfx_current_game_window_viewport.height); + }); + mPortMenu->AddWidget(path, "Internal resolution: {} x {}", WIDGET_TEXT).PreFunc([](WidgetInfo& info) { + info.name = + fmt::format("Internal resolution: {} x {}", gfx_current_dimensions.width, gfx_current_dimensions.height); + }); + + // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + // Activator + mPortMenu->AddWidget(path, "Enable advanced settings.", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled"); + /*UIWidgets::PaddedEnhancementCheckbox(, , false, false, + false, "", UIWidgets::CheckboxGraphics::Cross, false);*/ + // Error/Warning display + mPortMenu + ->AddWidget(path, ICON_FA_EXCLAMATION_TRIANGLE " Significant frame rate (FPS) drops may be occuring.", + WIDGET_TEXT) + .PreFunc( + [](WidgetInfo& info) { info.isHidden = !(!CVarGetInteger(CVAR_LOW_RES_MODE, 0) && IsDroppingFrames()); }) + .Options(WidgetOptions().Color(Colors::Orange)); + mPortMenu->AddWidget(path, ICON_FA_QUESTION_CIRCLE " \"N64 Mode\" is overriding these settings.", WIDGET_TEXT) + .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_LOW_RES_MODE, 0); }) + .Options(WidgetOptions().Color(Colors::LightBlue)); + mPortMenu->AddWidget(path, "Click to disable N64 mode", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_LOW_RES_MODE, 0); }) + .Callback([](WidgetInfo& info) { + CVarSetInteger(CVAR_LOW_RES_MODE, 0); + CVarSave(); + }); + + // if (disabled_everything) { // Hide aspect ratio controls. + // UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + // } + + // Aspect Ratio + mPortMenu->AddWidget(path, "AspectSep", WIDGET_SEPARATOR); + mPortMenu->AddWidget(path, "Force aspect ratio:", WIDGET_TEXT); + mPortMenu->AddWidget(path, "(Select \"Off\" to disable.)", WIDGET_TEXT) + .SameLine(true) + .Options(WidgetOptions().Color(Colors::Gray)); + // Presets + mPortMenu->AddWidget(path, "Aspect Ratio", WIDGET_COMBOBOX) + .ValuePointer(&item_aspectRatio) + .PreFunc([](WidgetInfo& info) { + info.isHidden = !mPortMenu->GetDisabledMap().at(DISABLE_FOR_ADVANCED_RESOLUTION_ON).active; + }) + .Callback([](WidgetInfo& info) { + if (item_aspectRatio != default_aspectRatio) { // don't change anything if "Custom" is selected. + aspectRatioX = aspectRatioPresetsX[item_aspectRatio]; + aspectRatioY = aspectRatioPresetsY[item_aspectRatio]; + + if (showHorizontalResField) { + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } - if (showHorizontalResField) { - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); } - } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", item_aspectRatio); + CVarSave(); + }) + .Options(ComboboxOptions().ComboMap(aspectRatioPresetLabels)); + mPortMenu->AddWidget(path, "AspectRationCustom", WIDGET_CUSTOM).CustomFunction([](WidgetInfo& info) { + // Hide aspect ratio input fields if using one of the presets. if (item_aspectRatio == default_aspectRatio && !showHorizontalResField) { - // Declaring the Y input interaction in particular as a variable beforehand - // will prevent a bug where the Y field would disappear when modifying X. - bool inputX = ImGui::InputFloat("X", &aspectRatioX, 0.1f, 1.0f, "%.3f"); - bool inputY = ImGui::InputFloat("Y", &aspectRatioY, 0.1f, 1.0f, "%.3f"); - if (inputX || inputY) { + // Declare input interaction bools outside of IF statement to prevent Y field from disappearing. + const bool input_X = ImGui::InputFloat("X", &aspectRatioX, 0.1f, 1.0f, "%.3f"); + const bool input_Y = ImGui::InputFloat("Y", &aspectRatioY, 0.1f, 1.0f, "%.3f"); + if (input_X || input_Y) { item_aspectRatio = default_aspectRatio; update[UPDATE_aspectRatioX] = true; update[UPDATE_aspectRatioY] = true; } } else if (showHorizontalResField) { // Show calculated aspect ratio if (item_aspectRatio) { - UIWidgets::Spacer(2); - float resolvedAspectRatio = (float) gfx_current_dimensions.height / gfx_current_dimensions.width; - ImGui::Text("Aspect ratio: %.4f", resolvedAspectRatio); - } else { - UIWidgets::Spacer(enhancementSpacerHeight); + ImGui::Dummy({ 0, 2 }); + const float resolvedAspectRatio = (float)gfx_current_dimensions.width / gfx_current_dimensions.height; + ImGui::Text("Aspect ratio: %.2f:1", resolvedAspectRatio); } } + }); + // + // if (disabled_everything) { // Hide aspect ratio controls. + // UIWidgets::ReEnableComponent("disabledTooltipText"); + // } + // UIWidgets::Spacer(0); + // + // // Vertical Resolution + // UIWidgets::PaddedEnhancementCheckbox("Set fixed vertical resolution (disables Resolution slider)", + // CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", true, + // false, disabled_everything, "", UIWidgets::CheckboxGraphics::Cross, + // false); + // UIWidgets::Tooltip( + // "Override the resolution scale slider and use the settings below, irrespective of window size."); + // if (disabled_pixelCount || disabled_everything) { // Hide pixel count controls. + // UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + // } + // if (ImGui::Combo("Pixel Count Presets", &item_pixelCount, pixelCountPresetLabels, + // IM_ARRAYSIZE(pixelCountPresetLabels)) && + // item_pixelCount != default_pixelCount) { // don't change anything if "Custom" is selected. + // verticalPixelCount = pixelCountPresets[item_pixelCount]; + // + // if (showHorizontalResField) { + // horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // } + // + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + // CVarSave(); + // } + // // Horizontal Resolution, if visibility is enabled for it. + // if (showHorizontalResField) { + // // Only show the field if Aspect Ratio is being enforced. + // if ((aspectRatioX > 0.0f) && (aspectRatioY > 0.0f)) { + // // So basically we're "faking" this one by setting aspectRatioX instead. + // if (ImGui::InputInt("Horiz. Pixel Count", &horizontalPixelCount, 8, 320)) { + // item_aspectRatio = default_aspectRatio; + // if (horizontalPixelCount < SCREEN_WIDTH) { + // horizontalPixelCount = SCREEN_WIDTH; + // } + // aspectRatioX = horizontalPixelCount; + // aspectRatioY = verticalPixelCount; + // update[UPDATE_aspectRatioX] = true; + // update[UPDATE_aspectRatioY] = true; + // } + // } else { // Display a notice instead. + // ImGui::TextColored(messageColor[MESSAGE_QUESTION], + // ICON_FA_QUESTION_CIRCLE " \"Force aspect ratio\" required."); + // // ImGui::Text(" "); + // ImGui::SameLine(); + // if (ImGui::Button("Click to resolve")) { + // item_aspectRatio = default_aspectRatio; // Set it to Custom + // aspectRatioX = aspectRatioPresetsX[2]; // but use the 4:3 defaults + // aspectRatioY = aspectRatioPresetsY[2]; + // update[UPDATE_aspectRatioX] = true; + // update[UPDATE_aspectRatioY] = true; + // horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // } + // } + // } + // // Vertical Resolution part 2 + // if (ImGui::InputInt("Vertical Pixel Count", &verticalPixelCount, 8, 240)) { + // item_pixelCount = default_pixelCount; + // update[UPDATE_verticalPixelCount] = true; + // + // // Account for the natural instinct to enter horizontal first. + // // Ignore vertical resolutions that are below the lower clamp constant. + // if (showHorizontalResField && !(verticalPixelCount < minVerticalPixelCount)) { + // item_aspectRatio = default_aspectRatio; + // aspectRatioX = horizontalPixelCount; + // aspectRatioY = verticalPixelCount; + // update[UPDATE_aspectRatioX] = true; + // update[UPDATE_aspectRatioY] = true; + // } + // } + // if (disabled_pixelCount || disabled_everything) { // Hide pixel count controls. + // UIWidgets::ReEnableComponent("disabledTooltipText"); + // } + // + // UIWidgets::Spacer(0); + // + // // Integer scaling settings group (Pixel-perfect Mode) + // static const ImGuiTreeNodeFlags IntegerScalingResolvedImGuiFlag = + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) ? ImGuiTreeNodeFlags_DefaultOpen + // : ImGuiTreeNodeFlags_None; + // if (ImGui::CollapsingHeader("Integer Scaling Settings", IntegerScalingResolvedImGuiFlag)) { + // const bool disabled_pixelPerfectMode = + // !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) || disabled_everything; + // // Pixel-perfect Mode + // UIWidgets::PaddedEnhancementCheckbox("Pixel-perfect Mode", CVAR_PREFIX_ADVANCED_RESOLUTION + // ".PixelPerfectMode", true, + // true, disabled_pixelCount || disabled_everything, "", + // UIWidgets::CheckboxGraphics::Cross, false); + // UIWidgets::Tooltip("Don't scale image to fill window."); + // if (disabled_pixelCount && CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0)) { + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0); + // CVarSave(); + // } + // + // // Integer Scaling + // UIWidgets::EnhancementSliderInt( + // "Integer scale factor: %d", "##ARSIntScale", CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", + // 1, max_integerScaleFactor, "%d", 1, true, disabled_pixelPerfectMode || + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", 0)); + // UIWidgets::Tooltip("Integer scales the image. Only available in pixel-perfect mode."); + // // Display warning if size is being clamped or if framebuffer is larger than viewport. + // if (!disabled_pixelPerfectMode && + // (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.NeverExceedBounds", 1) && + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", 1) > + // integerScale_maximumBounds)) { + // ImGui::SameLine(); + // ImGui::TextColored(messageColor[MESSAGE_WARNING], ICON_FA_EXCLAMATION_TRIANGLE " Window exceeded."); + // } + // + // UIWidgets::PaddedEnhancementCheckbox( + // "Automatically scale image to fit viewport", CVAR_PREFIX_ADVANCED_RESOLUTION + // ".IntegerScale.FitAutomatically", true, true, disabled_pixelPerfectMode, "", + // UIWidgets::CheckboxGraphics::Cross, false); + // UIWidgets::Tooltip("Automatically sets scale factor to fit window. Only available in pixel-perfect + // mode."); if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", 0)) { + // // This is just here to update the value shown on the slider. + // // The function in LUS to handle this setting will ignore IntegerScaleFactor while active. + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.Factor", integerScale_maximumBounds); + // // CVarSave(); + // } + // } // End of integer scaling settings + // + // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + // + // // Collapsible panel for additional settings + // if (ImGui::CollapsingHeader("Additional Settings")) { + // UIWidgets::Spacer(0); + // + //#if defined(__SWITCH__) || defined(__WIIU__) + // // Disable aspect correction, stretching the framebuffer to fill the viewport. + // // This option is only really needed on systems limited to 16:9 TV resolutions, such as consoles. + // // The associated cvar is still functional on PC platforms if you want to use it though. + // UIWidgets::PaddedEnhancementCheckbox("Disable aspect correction and stretch the output image.\n" + // "(Might be useful for 4:3 televisions!)\n" + // "Not available in Pixel Perfect Mode.", + // CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", false, + // true, CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION + // ".PixelPerfectMode", 0) || + // disabled_everything, + // "", UIWidgets::CheckboxGraphics::Cross, false); + //#else + // if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", 0)) { + // // This setting is intentionally not exposed on PC platforms, + // // but may be accidentally activated for varying reasons. + // // Having this button should hopefully prevent support headaches. + // ImGui::TextColored(messageColor[MESSAGE_QUESTION], ICON_FA_QUESTION_CIRCLE + // " If the image is stretched and you don't know why, click this."); + // if (ImGui::Button("Click to reenable aspect correction.")) { + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IgnoreAspectCorrection", 0); + // CVarSave(); + // } + // UIWidgets::Spacer(2); + // } + //#endif + // + // // A requested addition; an alternative way of displaying the resolution field. + // if (ImGui::Checkbox("Show a horizontal resolution field, instead of aspect ratio.", + // &showHorizontalResField)) { + // if (!showHorizontalResField && (aspectRatioX > 0.0f)) { // when turning this setting off + // // Refresh relevant values + // aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + // horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // } else { // when turning this setting on + // item_aspectRatio = default_aspectRatio; + // if (aspectRatioX > 0.0f) { + // // Refresh relevant values in the opposite order + // horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + // } + // } + // update[UPDATE_aspectRatioX] = true; + // } + // + // // Beginning of Integer Scaling additional settings. + // { + // // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + // + // // Integer Scaling - Never Exceed Bounds. + // const bool disabled_neverExceedBounds = + // !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) || + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.FitAutomatically", 0) || + // disabled_everything; + // const bool checkbox_neverExceedBounds = + // UIWidgets::PaddedEnhancementCheckbox("Prevent integer scaling from exceeding screen bounds.\n" + // "(Makes screen bounds take priority over specified + // factor.)", CVAR_PREFIX_ADVANCED_RESOLUTION + // ".IntegerScale.NeverExceedBounds", true, false, + // disabled_neverExceedBounds, "", + // UIWidgets::CheckboxGraphics::Cross, true); + // UIWidgets::Tooltip( + // "Prevents integer scaling factor from exceeding screen bounds.\n\n" + // "Enabled: Will clamp the scaling factor and display a gentle warning in the resolution editor.\n" + // "Disabled: Will allow scaling to exceed screen bounds, for users who want to crop overscan.\n\n" + // " " ICON_FA_INFO_CIRCLE + // " Please note that exceeding screen bounds may show a scroll bar on-screen."); + // + // // Initialise the (currently unused) "Exceed Bounds By" cvar if it's been changed. + // if (checkbox_neverExceedBounds && + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0); + // CVarSave(); + // } + // + // // Integer Scaling - Exceed Bounds By 1x/Offset. + // // A popular feature in some retro frontends/upscalers, sometimes called "crop overscan" or "1080p + // 5x". + // /* + // UIWidgets::PaddedEnhancementCheckbox("Allow integer scale factor to go +1 above maximum screen + // bounds.", CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", false, false, + // !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".PixelPerfectMode", 0) || disabled_everything, "", + // UIWidgets::CheckboxGraphics::Cross, false); + // */ + // // It does actually function as expected, but exceeding the bottom of the screen shows a scroll bar. + // // I've ended up commenting this one out because of the scroll bar, and for simplicity. + // + // // Display an info message about the scroll bar. + // if (!CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.NeverExceedBounds", 1) || + // CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + // if (disabled_neverExceedBounds) { // Dim this help text accordingly + // UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + // } + // ImGui::TextColored(messageColor[MESSAGE_INFO], + // " " ICON_FA_INFO_CIRCLE + // " A scroll bar may become visible if screen bounds are exceeded."); + // if (disabled_neverExceedBounds) { // Dim this help text accordingly + // UIWidgets::ReEnableComponent("disabledTooltipText"); + // } + // + // // Another support helper button, to disable the unused "Exceed Bounds By" cvar. + // // (Remove this button if uncommenting the checkbox.) + // if (CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0)) { + // if (ImGui::Button("Click to reset a console variable that may be causing this.")) { + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0); + // CVarSave(); + // } + // } + // } else { + // ImGui::Text(" "); + // } + // // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + // } // End of Integer Scaling additional settings. + // + // } // End of additional settings + // + // // Clamp and update the cvars that don't use UIWidgets + // if (update[UPDATE_aspectRatioX] || update[UPDATE_aspectRatioY] || update[UPDATE_verticalPixelCount]) { + // if (update[UPDATE_aspectRatioX]) { + // if (aspectRatioX < 0.0f) { + // aspectRatioX = 0.0f; + // } + // CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); + // } + // if (update[UPDATE_aspectRatioY]) { + // if (aspectRatioY < 0.0f) { + // aspectRatioY = 0.0f; + // } + // CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); + // } + // if (update[UPDATE_verticalPixelCount]) { + // // There's a upper and lower clamp on the Libultraship side too, + // // so clamping it here is entirely visual, so the vertical resolution field reflects it. + // if (verticalPixelCount < minVerticalPixelCount) { + // verticalPixelCount = minVerticalPixelCount; + // } + // if (verticalPixelCount > maxVerticalPixelCount) { + // verticalPixelCount = maxVerticalPixelCount; + // } + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); + // } + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", item_aspectRatio); + // CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + // CVarSave(); + // } +} - UIWidgets::Spacer(0); - // Vertical Resolution - UIWidgets::PaddedEnhancementCheckbox("Set fixed vertical resolution (disables Resolution slider)", - "gAdvancedResolution.VerticalResolutionToggle", true, false, false, "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip( - "Override the resolution scale slider and use the settings below, irrespective of window size."); - if (ImGui::Combo("Pixel Count Presets", &item_pixelCount, pixelCountPresetLabels, - IM_ARRAYSIZE(pixelCountPresetLabels)) && - item_pixelCount != default_pixelCount) { // don't change anything if "Custom" is selected. - verticalPixelCount = pixelCountPresets[item_pixelCount]; - update[UPDATE_verticalPixelCount] = true; - - if (showHorizontalResField) { - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; +void UpdateResolutionVars() { + // Clamp and update the cvars that don't use UIWidgets + if (update[UPDATE_aspectRatioX] || update[UPDATE_aspectRatioY] || update[UPDATE_verticalPixelCount]) { + if (update[UPDATE_aspectRatioX]) { + if (aspectRatioX < 0.0f) { + aspectRatioX = 0.0f; } + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioX); } - // Horizontal Resolution, if visibility is enabled for it. - if (showHorizontalResField) { - // Only show the field if Aspect Ratio is being enforced. - if ((aspectRatioX > 0.0f) && (aspectRatioY > 0.0f)) { - // So basically we're "faking" this one by setting aspectRatioX instead. - if (ImGui::InputInt("Horiz. Pixel Count", &horizontalPixelCount, 8, 320)) { - item_aspectRatio = default_aspectRatio; - if (horizontalPixelCount < (minVerticalPixelCount / 3.0f) * 4.0f) { - horizontalPixelCount = (minVerticalPixelCount / 3.0f) * 4.0f; - } - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - update[UPDATE_aspectRatioX] = true; - } - } else { // Display a notice instead. - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - ICON_FA_QUESTION_CIRCLE " \"Force aspect ratio\" required."); - // ImGui::Text(" "); - ImGui::SameLine(); - if (ImGui::Button("Click to resolve")) { - item_aspectRatio = default_aspectRatio; // Set it to Custom - aspectRatioX = aspectRatioPresetsX[2]; // but use the 4:3 defaults - aspectRatioY = aspectRatioPresetsY[2]; - update[UPDATE_aspectRatioX] = true; - update[UPDATE_aspectRatioY] = true; - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - } - } - } - // Vertical Resolution part 2 - if (ImGui::InputInt("Vertical Pixel Count", &verticalPixelCount, 8, 240)) { - item_pixelCount = default_pixelCount; - update[UPDATE_verticalPixelCount] = true; - - // Account for the natural instinct to enter horizontal first. - // Ignore vertical resolutions that are below the lower clamp constant. - if (showHorizontalResField && !(verticalPixelCount < minVerticalPixelCount)) { - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - update[UPDATE_aspectRatioX] = true; + if (update[UPDATE_aspectRatioY]) { + if (aspectRatioY < 0.0f) { + aspectRatioY = 0.0f; } + CVarSetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioY); } - - UIWidgets::Spacer(0); - // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - // Integer scaling settings group - if (ImGui::CollapsingHeader("Integer Scaling Settings")) { - // Pixel-perfect Mode - UIWidgets::PaddedEnhancementCheckbox("Pixel-perfect Mode", "gAdvancedResolution.PixelPerfectMode", true, - true, - !CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip("Don't scale image to fill window."); - if (!CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0)) { - CVarSetInteger("gAdvancedResolution.PixelPerfectMode", (int) false); - CVarSave(); + if (update[UPDATE_verticalPixelCount]) { + // There's a upper and lower clamp on the Libultraship side too, + // so clamping it here is entirely visual, so the vertical resolution field reflects it. + if (verticalPixelCount < minVerticalPixelCount) { + verticalPixelCount = minVerticalPixelCount; } - - // Integer Scaling - UIWidgets::EnhancementSliderInt("Integer scale factor: %d", "##ARSIntScale", - "gAdvancedResolution.IntegerScale.Factor", 1, max_integerScaleFactor, "%d", - 1, true, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || - CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)); - UIWidgets::Tooltip("Integer scales the image. Only available in pixel-perfect mode."); - // Display warning if size is being clamped or if framebuffer is larger than viewport. - if (CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) && - (CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) && - CVarGetInteger("gAdvancedResolution.IntegerScale.Factor", 1) > integerScale_maximumBounds)) { - ImGui::SameLine(); - ImGui::TextColored({ 0.85f, 0.85f, 0.0f, 1.0f }, ICON_FA_EXCLAMATION_TRIANGLE " Window exceeded."); - } - - UIWidgets::PaddedEnhancementCheckbox("Automatically scale image to fit viewport", - "gAdvancedResolution.IntegerScale.FitAutomatically", true, true, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - UIWidgets::Tooltip("Automatically sets scale factor to fit window. Only available in pixel-perfect mode."); - if (CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)) { - // This is just here to update the value shown on the slider. - // The function in LUS to handle this setting will ignore IntegerScaleFactor while active. - CVarSetInteger("gAdvancedResolution.IntegerScale.Factor", integerScale_maximumBounds); - // CVarSave(); + if (verticalPixelCount > maxVerticalPixelCount) { + verticalPixelCount = maxVerticalPixelCount; } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", verticalPixelCount); } + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", item_aspectRatio); + CVarSetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", item_pixelCount); + CVarSave(); + } + // Initialise update flags. + for (uint8_t i = 0; i < sizeof(update); i++) { + update[i] = false; + } - UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); - - // Collapsible panel for additional settings - if (ImGui::CollapsingHeader("Additional Settings")) { - UIWidgets::Spacer(0); -#if defined(__SWITCH__) || defined(__WIIU__) - // Disable aspect correction, stretching the framebuffer to fill the viewport. - // This option is only really needed on systems limited to 16:9 TV resolutions, such as consoles. - // The associated CVar is still functional on PC platforms if you want to use it though. - UIWidgets::PaddedEnhancementCheckbox("Disable aspect correction and stretch the output image.\n" - "(Might be useful for 4:3 televisions!)\n" - "Not available in Pixel Perfect Mode.", - "gAdvancedResolution.IgnoreAspectCorrection", false, true, - CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); -#else - if (CVarGetInteger("gAdvancedResolution.IgnoreAspectCorrection", 0)) { - // This setting is intentionally not exposed on PC platforms, - // but may be accidentally activated for varying reasons. - // Having this button should hopefully prevent support headaches. - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, ICON_FA_QUESTION_CIRCLE - " If the image is stretched and you don't know why, click this."); - if (ImGui::Button("Click to reenable aspect correction.")) { - CVarSetInteger("gAdvancedResolution.IgnoreAspectCorrection", (int) false); - CVarSave(); - } - UIWidgets::Spacer(2); - } -#endif - - if (ImGui::Checkbox("Show a horizontal resolution field.", &showHorizontalResField)) { - if (!showHorizontalResField && (aspectRatioX > 0.0f)) { // when turning this setting off - // Refresh relevant values - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - } else { // when turning this setting on - item_aspectRatio = default_aspectRatio; - if (aspectRatioX > 0.0f) { - // Refresh relevant values in the opposite order - horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; - aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; - } - } - update[UPDATE_aspectRatioX] = true; - } - - UIWidgets::PaddedEnhancementCheckbox( - "Don't allow integer scaling to exceed screen bounds.\n" - "(Makes screen bounds take priority over specified factor.)", - "gAdvancedResolution.IntegerScale.NeverExceedBounds", true, false, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || - CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0), - "", UIWidgets::CheckboxGraphics::Cross, true); - - if (!CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) || - CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - " " ICON_FA_QUESTION_CIRCLE - " A scroll bar may become visible if screen bounds are exceeded."); - // Another helpful button for an unused CVar. - if (CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - if (ImGui::Button("Click to reset an unused CVar that may be causing this.")) { - CVarSetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); - CVarSave(); - } - } - } else { - UIWidgets::Spacer(enhancementSpacerHeight); - } - - // I've ended up dummying this one out because it doesn't function in a satisfactory way. - // Consider this idea on the table, but I don't deem it an important enough feature to push for. - /* - UIWidgets::PaddedEnhancementCheckbox("Allow integer scale factor to go 1x above maximum screen bounds.", - "gAdvancedResolution.IntegerScale.ExceedBoundsBy", false, false, - !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0), "", - UIWidgets::CheckboxGraphics::Cross, false); - if (CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { - ImGui::TextColored({ 0.0f, 0.85f, 0.85f, 1.0f }, - " " ICON_FA_QUESTION_CIRCLE - " A scroll bar may become visible if screen bounds are exceeded."); - }*/ - - } // end of Additional Settings - - // Clamp and update the CVars that don't use UIWidgets - if (IsBoolArrayTrue(update)) { - if (update[UPDATE_aspectRatioX]) { - if (aspectRatioX < 0.0f) { - aspectRatioX = 0.0f; - } - CVarSetFloat("gAdvancedResolution.AspectRatioX", aspectRatioX); - } - if (update[UPDATE_aspectRatioY]) { - if (aspectRatioY < 0.0f) { - aspectRatioY = 0.0f; - } - CVarSetFloat("gAdvancedResolution.AspectRatioY", aspectRatioY); - } - if (update[UPDATE_verticalPixelCount]) { - // There's a upper and lower clamp on the Libultraship side too, - // so clamping it here is purely visual, so the vertical resolution field reflects it. - if (verticalPixelCount < minVerticalPixelCount) { - verticalPixelCount = minVerticalPixelCount; - } - if (verticalPixelCount > maxVerticalPixelCount) { - verticalPixelCount = maxVerticalPixelCount; - } - CVarSetInteger("gAdvancedResolution.VerticalPixelCount", verticalPixelCount); - } - // Delay saving this set of CVars by a predetermined length of time, in frames. - updateCountdown = countdownStartingValue; - } - if (updateCountdown > 0) { - updateCountdown--; - } else { - CVarSave(); - } + // Initialise integer scale bounds. + short max_integerScaleFactor = default_maxIntegerScaleFactor; // default value, which may or may not get + // overridden depending on viewport res + + short integerScale_maximumBounds = 1; // can change when window is resized + // This is mostly just for UX purposes, as Fit Automatically logic is part of LUS. + if (((float)gfx_current_game_window_viewport.width / gfx_current_game_window_viewport.height) > + ((float)gfx_current_dimensions.width / gfx_current_dimensions.height)) { + // Scale to window height + integerScale_maximumBounds = gfx_current_game_window_viewport.height / gfx_current_dimensions.height; + } else { + // Scale to window width + integerScale_maximumBounds = gfx_current_game_window_viewport.width / gfx_current_dimensions.width; + } + // Lower-clamping maximum bounds value to 1 is no-longer necessary as that's accounted for in LUS. + // Letting it go below 1 in this Editor will even allow for checking if screen bounds are being exceeded. + if (default_maxIntegerScaleFactor < integerScale_maximumBounds) { + max_integerScaleFactor = integerScale_maximumBounds + + CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".IntegerScale.ExceedBoundsBy", 0); } - ImGui::End(); -} -void AdvancedResolutionSettingsWindow::UpdateElement() { + // Combo List defaults + item_aspectRatio = CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.AspectRatio", 3); + item_pixelCount = CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".UIComboItem.PixelCount", default_pixelCount); + // Stored Values for non-UIWidgets elements + aspectRatioX = CVarGetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioX", aspectRatioPresetsX[item_aspectRatio]); + aspectRatioY = CVarGetFloat(CVAR_PREFIX_ADVANCED_RESOLUTION ".AspectRatioY", aspectRatioPresetsY[item_aspectRatio]); + verticalPixelCount = + CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalPixelCount", pixelCountPresets[item_pixelCount]); + // Additional settings + showHorizontalResField = false; + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // Disabling flags + disabled_everything = !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".Enabled", 0); + disabled_pixelCount = !CVarGetInteger(CVAR_PREFIX_ADVANCED_RESOLUTION ".VerticalResolutionToggle", 0); } -bool AdvancedResolutionSettingsWindow::IsDroppingFrames() { +bool IsDroppingFrames() { // a rather imprecise way of checking for frame drops. // but it's mostly there to inform the player of large drops. - const short targetFPS = CVarGetInteger("gInterpolationFPS", 30); + const short targetFPS = CVarGetInteger("gInterpolationFPS", 20); const float threshold = targetFPS / 20.0f + 4.1f; return ImGui::GetIO().Framerate < targetFPS - threshold; } -bool AdvancedResolutionSettingsWindow::IsBoolArrayTrue(bool* foo) { - for (unsigned short i = 0; i < sizeof(&foo); i++) - if (&foo[i]) - return true; - return false; -} -} // namespace AdvancedResolutionSettings +static RegisterMenuUpdateFunc updateFunc(UpdateResolutionVars, "Settings", "Graphics"); +static RegisterMenuInitFunc initFunc(RegisterResolutionWidgets); + +} // namespace BenGui diff --git a/src/port/ui/ResolutionEditor.h b/src/port/ui/ResolutionEditor.h index 8894422c1..a287bb282 100644 --- a/src/port/ui/ResolutionEditor.h +++ b/src/port/ui/ResolutionEditor.h @@ -1,19 +1,12 @@ -#pragma once -#include <libultraship/libultraship.h> - -namespace AdvancedResolutionSettings { -class AdvancedResolutionSettingsWindow : public Ship::GuiWindow { - private: - bool IsDroppingFrames(); +#ifndef RESOLUTIONEDITOR_H +#define RESOLUTIONEDITOR_H - protected: - bool IsBoolArrayTrue(bool*); +#include <libultraship/libultraship.h> - public: - using Ship::GuiWindow::GuiWindow; +namespace GameUI { +bool IsDroppingFrames(); +void RegisterResolutionWidgets(); +void UpdateResolutionVars(); +} // namespace BenGui - void InitElement() override; - void DrawElement() override; - void UpdateElement() override; -}; -} // namespace AdvancedResolutionSettings +#endif // RESOLUTIONEDITOR_H diff --git a/src/port/ui/UIWidgets.cpp b/src/port/ui/UIWidgets.cpp index b97256765..1368cf420 100644 --- a/src/port/ui/UIWidgets.cpp +++ b/src/port/ui/UIWidgets.cpp @@ -1,27 +1,19 @@ -// -// UIWidgets.cpp -// soh -// -// Created by David Chavez on 25.08.22. -// - #include "UIWidgets.h" - -#include "libultraship/src/Context.h" - -#include <imgui.h> +#define IMGUI_DEFINE_MATH_OPERATORS #include <imgui_internal.h> +#include <sstream> #include <libultraship/libultraship.h> +#include <string> +#include <unordered_map> #include <libultraship/libultra/types.h> +#include <spdlog/fmt/fmt.h> namespace UIWidgets { -// MARK: - Layout Helper - // Automatically adds newlines to break up text longer than a specified number of characters // Manually included newlines will still be respected and reset the line length // If line is midword when it hits the limit, text should break at the last encountered space -char* WrappedText(const char* text, unsigned int charactersPerLine) { +std::string WrappedText(const char* text, unsigned int charactersPerLine) { std::string newText(text); const size_t tipLength = newText.length(); int lastSpace = -1; @@ -43,615 +35,41 @@ char* WrappedText(const char* text, unsigned int charactersPerLine) { currentLineLength++; } - return strdup(newText.c_str()); + return newText; } -char* WrappedText(const std::string& text, unsigned int charactersPerLine) { +std::string WrappedText(const std::string& text, unsigned int charactersPerLine) { return WrappedText(text.c_str(), charactersPerLine); } -void SetLastItemHoverText(const std::string& text) { - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -void SetLastItemHoverText(const char* text) { - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -// Adds a "?" next to the previous ImGui item with a custom tooltip -void InsertHelpHoverText(const std::string& text) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "?"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -void InsertHelpHoverText(const char* text) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "?"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", WrappedText(text, 60)); - ImGui::EndTooltip(); - } -} - -// MARK: - UI Elements - void Tooltip(const char* text) { if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", WrappedText(text)); - } -} - -void Spacer(float height) { - ImGui::Dummy(ImVec2(0.0f, height)); -} - -void PaddedSeparator(bool padTop, bool padBottom, float extraVerticalTopPadding, float extraVerticalBottomPadding) { - if (padTop) { - Spacer(extraVerticalTopPadding); - } - ImGui::Separator(); - if (padBottom) { - Spacer(extraVerticalBottomPadding); - } -} - -void RenderCross(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) { - float thickness = ImMax(sz / 5.0f, 1.0f); - sz -= thickness * 0.5f; - pos += ImVec2(thickness * 0.25f, thickness * 0.25f); - - draw_list->PathLineTo(ImVec2(pos.x, pos.y)); - draw_list->PathLineTo(ImVec2(pos.x + sz, pos.y + sz)); - draw_list->PathStroke(col, 0, thickness); - - draw_list->PathLineTo(ImVec2(pos.x + sz, pos.y)); - draw_list->PathLineTo(ImVec2(pos.x, pos.y + sz)); - draw_list->PathStroke(col, 0, thickness); -} - -bool CustomCheckbox(const char* label, bool* v, bool disabled, CheckboxGraphics disabledGraphic) { - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) { - return false; - } - - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); - - const float square_sz = ImGui::GetFrameHeight(); - const ImVec2 pos = window->DC.CursorPos; - const ImRect total_bb( - pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), - label_size.y + style.FramePadding.y * 2.0f)); - ImGui::ItemSize(total_bb, style.FramePadding.y); - if (!ImGui::ItemAdd(total_bb, id)) { - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, - g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | - (*v ? ImGuiItemStatusFlags_Checked : 0)); - return false; - } - - bool hovered, held; - bool pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) { - *v = !(*v); - ImGui::MarkItemEdited(id); - } - - const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); - ImGui::RenderNavHighlight(total_bb, id); - ImGui::RenderFrame(check_bb.Min, check_bb.Max, - ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive - : hovered ? ImGuiCol_FrameBgHovered - : ImGuiCol_FrameBg), - true, style.FrameRounding); - ImU32 check_col = ImGui::GetColorU32(ImGuiCol_CheckMark); - ImU32 cross_col = ImGui::GetColorU32(ImVec4(0.50f, 0.50f, 0.50f, 1.00f)); - bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0; - if (mixed_value) { - // Undocumented tristate/mixed/indeterminate checkbox (#2644) - // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all - // widgets (not just checkbox) - ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(1.0f, IM_FLOOR(square_sz / 3.6f))); - window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); - } else if ((!disabled && *v) || (disabled && disabledGraphic == CheckboxGraphics::Checkmark)) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); - ImGui::RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); - } else if (disabled && disabledGraphic == CheckboxGraphics::Cross) { - const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); - RenderCross(window->DrawList, check_bb.Min + ImVec2(pad, pad), cross_col, square_sz - pad * 2.0f); - } - - ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); - if (g.LogEnabled) { - ImGui::LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); - } - if (label_size.x > 0.0f) { - ImGui::RenderText(label_pos, label); - } - - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, - g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | - (*v ? ImGuiItemStatusFlags_Checked : 0)); - return pressed; -} - -void ReEnableComponent(const char* disabledTooltipText) { - // End of disable region of previous component - ImGui::PopStyleVar(1); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(disabledTooltipText, "") != 0) { - ImGui::SetTooltip("%s", disabledTooltipText); - } - ImGui::PopItemFlag(); -} - -void DisableComponent(const float alpha) { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); -} - -bool EnhancementCheckbox(const char* text, const char* cvarName, bool disabled, const char* disabledTooltipText, - CheckboxGraphics disabledGraphic, bool defaultValue) { - bool changed = false; - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - bool val = (bool) CVarGetInteger(cvarName, defaultValue); - if (CustomCheckbox(text, &val, disabled, disabledGraphic)) { - CVarSetInteger(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - changed = true; - } - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - return changed; -} - -bool PaddedEnhancementCheckbox(const char* text, const char* cvarName, bool padTop, bool padBottom, bool disabled, - const char* disabledTooltipText, CheckboxGraphics disabledGraphic, bool defaultValue) { - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - bool changed = EnhancementCheckbox(text, cvarName, disabled, disabledTooltipText, disabledGraphic, defaultValue); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool EnhancementCombobox(const char* cvarName, std::span<const char*, std::dynamic_extent> comboArray, - uint8_t defaultIndex, bool disabled, const char* disabledTooltipText, uint8_t disabledValue) { - bool changed = false; - if (defaultIndex <= 0) { - defaultIndex = 0; - } - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - uint8_t selected = CVarGetInteger(cvarName, defaultIndex); - std::string comboName = std::string("##") + std::string(cvarName); - if (ImGui::BeginCombo(comboName.c_str(), comboArray[selected])) { - for (uint8_t i = 0; i < comboArray.size(); i++) { - if (strlen(comboArray[i]) > 1) { - if (ImGui::Selectable(comboArray[i], i == selected)) { - CVarSetInteger(cvarName, i); - selected = i; - changed = true; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - } - ImGui::EndCombo(); - } - - if (disabled) { - ReEnableComponent(disabledTooltipText); - - if (disabledValue >= 0 && selected != disabledValue) { - CVarSetInteger(cvarName, disabledValue); - changed = true; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - } - - return changed; -} - -bool LabeledRightAlignedEnhancementCombobox(const char* label, const char* cvarName, - std::span<const char*, std::dynamic_extent> comboArray, - uint8_t defaultIndex, bool disabled, const char* disabledTooltipText, - uint8_t disabledValue) { - ImGui::Text(label); - s32 currentValue = CVarGetInteger(cvarName, defaultIndex); - -#ifdef __WIIU__ - ImGui::SameLine(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize(comboArray[currentValue]).x + 40.0f)); - ImGui::PushItemWidth(ImGui::CalcTextSize(comboArray[currentValue]).x + 60.0f); -#else - ImGui::SameLine(ImGui::GetContentRegionAvail().x - (ImGui::CalcTextSize(comboArray[currentValue]).x + 20.0f)); - ImGui::PushItemWidth(ImGui::CalcTextSize(comboArray[currentValue]).x + 30.0f); -#endif - - bool changed = - EnhancementCombobox(cvarName, comboArray, defaultIndex, disabled, disabledTooltipText, disabledValue); - - ImGui::PopItemWidth(); - return changed; -} - -void PaddedText(const char* text, bool padTop, bool padBottom) { - if (padTop) - Spacer(0); - - ImGui::Text("%s", text); - - if (padBottom) - Spacer(0); -} - -bool EnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, - int defaultValue, bool PlusMinusButton, bool disabled, const char* disabledTooltipText) { - bool changed = false; - int val = CVarGetInteger(cvarName, defaultValue); - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - ImGui::Text(text, val); - Spacer(0); - - ImGui::BeginGroup(); - if (PlusMinusButton) { - std::string MinusBTNName = " - ##" + std::string(cvarName); - if (ImGui::Button(MinusBTNName.c_str())) { - val--; - changed = true; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - } - - ImGui::PushItemWidth( - std::min((ImGui::GetContentRegionAvail().x - (PlusMinusButton ? sliderButtonWidth : 0.0f)), maxSliderWidth)); - if (ImGui::SliderInt(id, &val, min, max, format, ImGuiSliderFlags_AlwaysClamp)) { - changed = true; - } - ImGui::PopItemWidth(); - - if (PlusMinusButton) { - std::string PlusBTNName = " + ##" + std::string(cvarName); - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(PlusBTNName.c_str())) { - val++; - changed = true; - } - } - ImGui::EndGroup(); - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - - if (val < min) { - val = min; - changed = true; - } - - if (val > max) { - val = max; - changed = true; - } - - if (changed) { - CVarSetInteger(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - - return changed; -} - -bool EnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton, - bool disabled, const char* disabledTooltipText) { - bool changed = false; - float val = CVarGetFloat(cvarName, defaultValue); - - if (disabled) { - DisableComponent(ImGui::GetStyle().Alpha * 0.5f); - } - - if (!isPercentage) { - ImGui::Text(text, val); - } else { - ImGui::Text(text, static_cast<int>(100 * val)); - } - Spacer(0); - - ImGui::BeginGroup(); - if (PlusMinusButton) { - std::string MinusBTNName = " - ##" + std::string(cvarName); - if (ImGui::Button(MinusBTNName.c_str())) { - if (isPercentage) { - val -= 0.01f; - } else { - val -= 0.1f; - } - changed = true; - } - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - } - - ImGui::PushItemWidth( - std::min((ImGui::GetContentRegionAvail().x - (PlusMinusButton ? sliderButtonWidth : 0.0f)), maxSliderWidth)); - if (ImGui::SliderFloat(id, &val, min, max, format, ImGuiSliderFlags_AlwaysClamp)) { - if (isPercentage) { - val = roundf(val * 100) / 100; - } - changed = true; - } - ImGui::PopItemWidth(); - - if (PlusMinusButton) { - std::string PlusBTNName = " + ##" + std::string(cvarName); - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 7.0f); - if (ImGui::Button(PlusBTNName.c_str())) { - if (isPercentage) { - val += 0.01f; - } else { - val += 0.1f; - } - changed = true; - } - } - ImGui::EndGroup(); - - if (disabled) { - ReEnableComponent(disabledTooltipText); - } - - if (val < min) { - val = min; - changed = true; - } - - if (val > max) { - val = max; - changed = true; - } - - if (changed) { - CVarSetFloat(cvarName, val); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - } - - return changed; -} - -bool PaddedEnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, - const char* format, int defaultValue, bool PlusMinusButton, bool padTop, bool padBottom, - bool disabled, const char* disabledTooltipText) { - bool changed = false; - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - changed = EnhancementSliderInt(text, id, cvarName, min, max, format, defaultValue, PlusMinusButton, disabled, - disabledTooltipText); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool PaddedEnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton, - bool padTop, bool padBottom, bool disabled, const char* disabledTooltipText) { - bool changed = false; - ImGui::BeginGroup(); - if (padTop) - Spacer(0); - - changed = EnhancementSliderFloat(text, id, cvarName, min, max, format, defaultValue, isPercentage, PlusMinusButton, - disabled, disabledTooltipText); - - if (padBottom) - Spacer(0); - ImGui::EndGroup(); - return changed; -} - -bool EnhancementRadioButton(const char* text, const char* cvarName, int id) { - /*Usage : - EnhancementRadioButton("My Visible Name","gMyCVarName", MyID); - First arg is the visible name of the Radio button - Second is the cvar name where MyID will be saved. - Note: the CVar name should be the same to each Buddies. - Example : - EnhancementRadioButton("English", "gLanguages", LANGUAGE_ENG); - EnhancementRadioButton("German", "gLanguages", LANGUAGE_GER); - EnhancementRadioButton("French", "gLanguages", LANGUAGE_FRA); - */ - std::string make_invisible = "##" + std::string(text) + std::string(cvarName); - - bool ret = false; - int val = CVarGetInteger(cvarName, 0); - if (ImGui::RadioButton(make_invisible.c_str(), id == val)) { - CVarSetInteger(cvarName, id); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - ret = true; - } - ImGui::SameLine(); - ImGui::Text("%s", text); - - return ret; -} - -bool DrawResetColorButton(const char* cvarName, ImVec4* colors, ImVec4 defaultcolors, bool has_alpha) { - bool changed = false; - std::string Cvar_RBM = std::string(cvarName) + "RBM"; - std::string MakeInvisible = "Reset##" + std::string(cvarName) + "Reset"; - if (ImGui::Button(MakeInvisible.c_str())) { - colors->x = defaultcolors.x; - colors->y = defaultcolors.y; - colors->z = defaultcolors.z; - colors->w = has_alpha ? defaultcolors.w : 255.0f; - - Color_RGBA8 colorsRGBA; - colorsRGBA.r = defaultcolors.x; - colorsRGBA.g = defaultcolors.y; - colorsRGBA.b = defaultcolors.z; - colorsRGBA.a = has_alpha ? defaultcolors.w : 255.0f; - - CVarSetColor(cvarName, colorsRGBA); - CVarSetInteger(Cvar_RBM.c_str(), 0); // On click disable rainbow mode. - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - changed = true; - } - Tooltip("Revert colors to the game's original colors (GameCube version)\nOverwrites previously chosen color"); - return changed; -} - -void DrawLockColorCheckbox(const char* cvarName) { - std::string Cvar_Lock = std::string(cvarName) + "Lock"; - s32 lock = CVarGetInteger(Cvar_Lock.c_str(), 0); - std::string FullName = "Lock##" + Cvar_Lock; - EnhancementCheckbox(FullName.c_str(), Cvar_Lock.c_str()); - Tooltip("Prevents this color from being changed upon selecting \"Randomize all\""); -} - -void RainbowColor(const char* cvarName, ImVec4* colors) { - std::string Cvar_RBM = std::string(cvarName) + "RBM"; - std::string MakeInvisible = "Rainbow##" + std::string(cvarName) + "Rainbow"; - - EnhancementCheckbox(MakeInvisible.c_str(), Cvar_RBM.c_str()); - Tooltip("Cycles through colors on a timer\nOverwrites previously chosen color"); -} - -void LoadPickersColors(ImVec4& ColorArray, const char* cvarname, const ImVec4& default_colors, bool has_alpha) { - Color_RGBA8 defaultColors; - defaultColors.r = default_colors.x; - defaultColors.g = default_colors.y; - defaultColors.b = default_colors.z; - defaultColors.a = default_colors.w; - - Color_RGBA8 cvarColor = CVarGetColor(cvarname, defaultColors); - - ColorArray.x = cvarColor.r / 255.0; - ColorArray.y = cvarColor.g / 255.0; - ColorArray.z = cvarColor.b / 255.0; - ColorArray.w = cvarColor.a / 255.0; -} - -void DrawFlagArray32(const std::string& name, uint32_t& flags) { - ImGui::PushID(name.c_str()); - for (int32_t flagIndex = 0; flagIndex < 32; flagIndex++) { - if ((flagIndex % 8) != 0) { - ImGui::SameLine(); - } - ImGui::PushID(flagIndex); - uint32_t bitMask = 1 << flagIndex; - bool flag = (flags & bitMask) != 0; - if (ImGui::Checkbox("##check", &flag)) { - if (flag) { - flags |= bitMask; - } else { - flags &= ~bitMask; - } - } - ImGui::PopID(); - } - ImGui::PopID(); -} - -void DrawFlagArray16(const std::string& name, uint16_t& flags) { - ImGui::PushID(name.c_str()); - for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { - if ((flagIndex % 8) != 0) { - ImGui::SameLine(); - } - ImGui::PushID(flagIndex); - uint16_t bitMask = 1 << flagIndex; - bool flag = (flags & bitMask) != 0; - if (ImGui::Checkbox("##check", &flag)) { - if (flag) { - flags |= bitMask; - } else { - flags &= ~bitMask; - } - } - ImGui::PopID(); - } - ImGui::PopID(); -} - -void DrawFlagArray8(const std::string& name, uint8_t& flags) { - ImGui::PushID(name.c_str()); - for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { - if ((flagIndex % 8) != 0) { - ImGui::SameLine(); - } - ImGui::PushID(flagIndex); - uint8_t bitMask = 1 << flagIndex; - bool flag = (flags & bitMask) != 0; - if (ImGui::Checkbox("##check", &flag)) { - if (flag) { - flags |= bitMask; - } else { - flags &= ~bitMask; - } - } - ImGui::PopID(); + ImGui::SetTooltip("%s", WrappedText(text).c_str()); } - ImGui::PopID(); } -// V2 - void PushStyleMenu(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_PopupBg, UIWidgets::Colors::DarkGray); - ImGui::PushStyleColor(ImGuiCol_Border, UIWidgets::Colors::DarkGray); + ImGui::PushStyleColor(ImGuiCol_PopupBg, UIWidgets::ColorValues.at(Colors::DarkGray)); + ImGui::PushStyleColor(ImGuiCol_Border, UIWidgets::ColorValues.at(Colors::DarkGray)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 15.0f)); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 3.0f); } +void PushStyleMenu(Colors color) { + PushStyleMenu(UIWidgets::ColorValues.at(color)); +} + void PopStyleMenu() { ImGui::PopStyleVar(2); ImGui::PopStyleColor(4); } -bool BeginMenu(const char* label, const ImVec4& color) { +bool BeginMenu(const char* label, Colors color) { bool dirty = false; PushStyleMenu(color); + ImGui::SetNextWindowSizeConstraints(ImVec2(200.0f, 0.0f), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::BeginMenu(label)) { dirty = true; } @@ -660,16 +78,20 @@ bool BeginMenu(const char* label, const ImVec4& color) { } void PushStyleMenuItem(const ImVec4& color) { - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0f, 15.0f)); } +void PushStyleMenuItem(Colors color) { + PushStyleMenuItem(ColorValues.at(color)); +} + void PopStyleMenuItem() { ImGui::PopStyleVar(1); ImGui::PopStyleColor(1); } -bool MenuItem(const char* label, const char* shortcut, const ImVec4& color) { +bool MenuItem(const char* label, const char* shortcut, Colors color) { bool dirty = false; PushStyleMenuItem(color); if (ImGui::MenuItem(label, shortcut)) { @@ -689,6 +111,10 @@ void PushStyleButton(const ImVec4& color) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } +void PushStyleButton(Colors color) { + PushStyleButton(UIWidgets::ColorValues.at(color)); +} + void PopStyleButton() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(4); @@ -701,10 +127,10 @@ bool Button(const char* label, const ButtonOptions& options) { PopStyleButton(); ImGui::EndDisabled(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } return dirty; } @@ -738,71 +164,131 @@ void PushStyleCheckbox(const ImVec4& color) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } +void PushStyleCheckbox(Colors color) { + PushStyleCheckbox(UIWidgets::ColorValues.at(color)); +} + void PopStyleCheckbox() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(5); } -bool Checkbox(const char* label, bool* value, const CheckboxOptions& options) { - ImGui::PushID(label); - bool dirty = false; - float startX = ImGui::GetCursorPosX(); - std::string invisibleLabelStr = "##" + std::string(label); - const char* invisibleLabel = invisibleLabelStr.c_str(); +void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Hide anything after a '##' string + const char* text_display_end; + if (hide_text_after_hash) { + text_display_end = ImGui::FindRenderedTextEnd(text, text_end); + } else { + if (!text_end) + text_end = text + strlen(text); // FIXME-OPT + text_display_end = text_end; + } + + if (text != text_display_end) { + window->DrawList->AddText(g.Font, g.FontSize, pos, ImGui::GetColorU32(ImGuiCol_Text), text, text_display_end); + if (g.LogEnabled) + ImGui::LogRenderedText(&pos, text, text_display_end); + } +} + +bool Checkbox(const char* _label, bool* value, const CheckboxOptions& options) { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + ImGui::BeginDisabled(options.disabled); - PushStyleCheckbox(options.color); - if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far || - options.labelPosition == LabelPosition::None) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2 - - ImGui::GetStyle().ItemSpacing.x); - } else if (options.labelPosition == LabelPosition::Above) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - ImGui::NewLine(); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2 - - ImGui::GetStyle().ItemSpacing.x); - } - } else if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label); + + bool above = options.labelPosition == LabelPosition::Above; + bool lpFar = options.labelPosition == LabelPosition::Far; + bool right = options.alignment == ComponentAlignment::Right; + bool none = options.labelPosition == LabelPosition::None; + + std::string labelStr = (none ? "##" : ""); + labelStr.append(_label); + + const char* label = labelStr.c_str(); + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + const float square_sz = ImGui::GetFrameHeight(); + ImVec2 pos = window->DC.CursorPos; + + if (right) { + float labelOffsetX = (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz); + if (!lpFar) { + pos.x += ImGui::GetContentRegionAvail().x - (label_size.x + labelOffsetX); } } - dirty = ImGui::Checkbox(invisibleLabel, value); + float bbAboveX = lpFar ? ImGui::GetContentRegionAvail().x + : (label_size.x + (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz)); + float bbAboveY = label_size.y + (above ? square_sz : 0) + (style.FramePadding.y * 2.0f); + const ImRect total_bb(pos, pos + ImVec2(bbAboveX, bbAboveY)); + + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, id)) { + ImGui::EndDisabled(); + return false; + } + bool hovered, held, pressed; + pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) { + *value = !(*value); + ImGui::MarkItemEdited(id); + } + PushStyleCheckbox(options.color); + ImVec2 checkPos = pos; + ImVec2 labelPos = pos; + if (options.labelPosition == LabelPosition::Above) { + checkPos.y += label_size.y + (style.ItemInnerSpacing.y * 2.0f); + } else { + labelPos.y += (square_sz / 2) - (label_size.y / 2); + } if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x * 2); - ImGui::Text(label); - } else if (options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(); - ImGui::SetCursorPosX(startX); - ImGui::Text(label); - } - } else if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(); - ImGui::Text(label); - } else if (options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - } + checkPos.x = total_bb.Max.x - square_sz; + } else { + float labelFarOffset = ImGui::GetContentRegionAvail().x - label_size.x; + float labelOffsetX = above ? 0 : (lpFar ? labelFarOffset : (style.ItemInnerSpacing.x * 2.0f) + square_sz); + labelPos.x += labelOffsetX; } + const ImRect check_bb(checkPos, checkPos + ImVec2(square_sz, square_sz)); + ImGui::RenderNavHighlight(total_bb, id); + ImGui::RenderFrame(check_bb.Min, check_bb.Max, + ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive + : hovered ? ImGuiCol_FrameBgHovered + : ImGuiCol_FrameBg), + true, style.FrameRounding); + ImU32 check_col = ImGui::GetColorU32(ImGuiCol_CheckMark); + bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0; + if (mixed_value) { + // Undocumented tristate/mixed/indeterminate checkbox (#2644) + // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all + // widgets (not just checkbox) + ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); + window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); + } else if (*value) { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + ImGui::RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); + } + RenderText(labelPos, label, ImGui::FindRenderedTextEnd(label), true); PopStyleCheckbox(); ImGui::EndDisabled(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } - ImGui::PopID(); - return dirty; + return pressed; } bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options) { bool dirty = false; - bool value = (bool) CVarGetInteger(cvarName, options.defaultValue); + bool value = (bool)CVarGetInteger(cvarName, options.defaultValue); if (Checkbox(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); @@ -827,100 +313,17 @@ void PushStyleCombobox(const ImVec4& color) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); } +void PushStyleCombobox(Colors color) { + PushStyleCombobox(UIWidgets::ColorValues.at(color)); +} + void PopStyleCombobox() { ImGui::PopStyleVar(4); ImGui::PopStyleColor(9); } -bool Combobox(const char* label, uint8_t* value, std::span<const char*, std::dynamic_extent> comboArray, - const ComboboxOptions& options) { - bool dirty = false; - float startX = ImGui::GetCursorPosX(); - std::string invisibleLabelStr = "##" + std::string(label); - const char* invisibleLabel = invisibleLabelStr.c_str(); - ImGui::PushID(label); - ImGui::BeginGroup(); - ImGui::BeginDisabled(options.disabled); - PushStyleCombobox(options.color); - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Near) { - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - - ImGui::GetStyle().ItemSpacing.x * 2); - } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { - ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboArray[*value]).x + ImGui::GetStyle().FramePadding.x * 4 + - ImGui::GetStyle().ItemSpacing.x); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::NewLine(); - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { - float width = ImGui::CalcTextSize(comboArray[*value]).x + ImGui::GetStyle().FramePadding.x * 4; - ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - } - } - if (ImGui::BeginCombo(invisibleLabel, comboArray[*value], options.flags)) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); - for (uint8_t i = 0; i < comboArray.size(); i++) { - if (strlen(comboArray[i]) > 1) { - if (ImGui::Selectable(comboArray[i], i == *value)) { - *value = i; - dirty = true; - } - } - } - ImGui::PopStyleVar(); - ImGui::EndCombo(); - } - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Near) { - ImGui::SameLine(); - ImGui::Text(label); - } else if (options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); - ImGui::Text(label); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { - ImGui::SameLine(startX); - ImGui::Text(label); - } - } - PopStyleCombobox(); - ImGui::EndDisabled(); - ImGui::EndGroup(); - if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); - } - ImGui::PopID(); - return dirty; -} - -bool CVarCombobox(const char* label, const char* cvarName, std::span<const char*, std::dynamic_extent> comboArray, - const ComboboxOptions& options) { - bool dirty = false; - uint8_t value = (uint8_t) CVarGetInteger(cvarName, options.defaultIndex); - if (Combobox(label, &value, comboArray, options)) { - CVarSetInteger(cvarName, value); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); - dirty = true; - } - return dirty; -} - -void PushStyleSlider(const ImVec4& color) { +void PushStyleSlider(Colors color_) { + const ImVec4& color = UIWidgets::ColorValues.at(color_); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 1.0f)); @@ -938,7 +341,7 @@ void PopStyleSlider() { ImGui::PopStyleColor(6); } -bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, const IntSliderOptions& options) { +bool SliderInt(const char* label, int32_t* value, const IntSliderOptions& options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); const char* invisibleLabel = invisibleLabelStr.c_str(); @@ -958,11 +361,10 @@ bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, cons } } if (options.showButtons) { - if (Button("-", { .color = options.color, .size = Sizes::Inline }) && *value > min) { + if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value > options.min) { *value -= options.step; - if (*value < min) - *value = min; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (*value < options.min) + *value = options.min; dirty = true; } ImGui::SameLine(0, 3.0f); @@ -970,18 +372,17 @@ bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, cons } else { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); } - if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_S32, value, &min, &max, options.format, options.flags)) { - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_S32, value, &options.min, &options.max, options.format, + options.flags)) { dirty = true; } if (options.showButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (Button("+", { .color = options.color, .size = Sizes::Inline }) && *value < max) { + if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value < options.max) { *value += options.step; - if (*value > max) - *value = max; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + if (*value > options.max) + *value = options.max; dirty = true; } } @@ -989,20 +390,19 @@ bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, cons ImGui::EndDisabled(); ImGui::EndGroup(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } -bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t max, const int32_t defaultValue, - const IntSliderOptions& options) { +bool CVarSliderInt(const char* label, const char* cvarName, const IntSliderOptions& options) { bool dirty = false; - int32_t value = CVarGetInteger(cvarName, defaultValue); - if (SliderInt(label, &value, min, max, options)) { + int32_t value = CVarGetInteger(cvarName, options.defaultValue); + if (SliderInt(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); dirty = true; @@ -1010,13 +410,51 @@ bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t return dirty; } -bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options) { +void ClampFloat(float* value, float min, float max, float step) { + int ticks = 0; + float increment = 1.0f; + if (step < 1.0f) { + ticks++; + increment = 0.1f; + } + if (step < 0.1f) { + ticks++; + increment = 0.01f; + } + if (step < 0.01f) { + ticks++; + increment = 0.001f; + } + if (step < 0.001f) { + ticks++; + increment = 0.0001f; + } + if (step < 0.0001f) { + ticks++; + increment = 0.00001f; + } + if (step < 0.00001f) { + ticks++; + increment = 0.000001f; + } + int factor = 1 * std::pow(10, ticks); + if (*value < min) { + *value = min; + } else if (*value > max) { + *value = max; + } else { + int trunc = (int)std::round(*value * factor); + *value = (float)trunc / factor; + } +} + +bool SliderFloat(const char* label, float* value, const FloatSliderOptions& options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); const char* invisibleLabel = invisibleLabelStr.c_str(); float valueToDisplay = options.isPercentage ? *value * 100.0f : *value; - float maxToDisplay = options.isPercentage ? max * 100.0f : max; - float minToDisplay = options.isPercentage ? min * 100.0f : min; + float maxToDisplay = options.isPercentage ? options.max * 100.0f : options.max; + float minToDisplay = options.isPercentage ? options.min * 100.0f : options.min; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); @@ -1033,11 +471,9 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl } } if (options.showButtons) { - if (Button("-", { .color = options.color, .size = Sizes::Inline }) && *value > min) { + if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value > options.min) { *value -= options.step; - if (*value < min) - *value = min; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ClampFloat(value, options.min, options.max, options.step); dirty = true; } ImGui::SameLine(0, 3.0f); @@ -1048,17 +484,15 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_Float, &valueToDisplay, &minToDisplay, &maxToDisplay, options.format, options.flags)) { *value = options.isPercentage ? valueToDisplay / 100.0f : valueToDisplay; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ClampFloat(value, options.min, options.max, options.step); dirty = true; } if (options.showButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (Button("+", { .color = options.color, .size = Sizes::Inline }) && *value < max) { + if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value < options.max) { *value += options.step; - if (*value > max) - *value = max; - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ClampFloat(value, options.min, options.max, options.step); dirty = true; } } @@ -1066,24 +500,133 @@ bool SliderFloat(const char* label, float* value, float min, float max, const Fl ImGui::EndDisabled(); ImGui::EndGroup(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && - strcmp(options.disabledTooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip)); - } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { - ImGui::SetTooltip("%s", WrappedText(options.tooltip)); + !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } -bool CVarSliderFloat(const char* label, const char* cvarName, float min, float max, const float defaultValue, - const FloatSliderOptions& options) { +bool CVarSliderFloat(const char* label, const char* cvarName, const FloatSliderOptions& options) { bool dirty = false; - float value = CVarGetFloat(cvarName, defaultValue); - if (SliderFloat(label, &value, min, max, options)) { + float value = CVarGetFloat(cvarName, options.defaultValue); + if (SliderFloat(label, &value, options)) { CVarSetFloat(cvarName, value); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); dirty = true; } return dirty; } + +bool CVarColorPicker(const char* label, const char* cvarName, Color_RGBA8 defaultColor) { + Color_RGBA8 color = CVarGetColor(cvarName, defaultColor); + ImVec4 colorVec = ImVec4(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f); + bool changed = false; + PushStyleCombobox(Colors::Gray); + if (ImGui::ColorEdit3(label, (float*)&colorVec, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoBorder)) { + color.r = (uint8_t)(colorVec.x * 255.0f); + color.g = (uint8_t)(colorVec.y * 255.0f); + color.b = (uint8_t)(colorVec.z * 255.0f); + color.a = (uint8_t)(colorVec.w * 255.0f); + CVarSetColor(cvarName, color); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + changed = true; + } + PopStyleCombobox(); + return changed; +} + +void DrawFlagArray32(const std::string& name, uint32_t& flags) { + ImGui::PushID(name.c_str()); + for (int32_t flagIndex = 0; flagIndex < 32; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint32_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + std::string label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); + if (UIWidgets::Checkbox(label.c_str(), &flag, + CheckboxOptions{ { .tooltip = label.c_str() } }.LabelPosition(LabelPosition::None))) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray16(const std::string& name, uint16_t& flags) { + ImGui::PushID(name.c_str()); + for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint16_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + std::string label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); + if (UIWidgets::Checkbox(label.c_str(), &flag, + CheckboxOptions{ { .tooltip = label.c_str() } }.LabelPosition(LabelPosition::None))) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray8(const std::string& name, uint8_t& flags) { + ImGui::PushID(name.c_str()); + for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint8_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + std::string label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); + if (UIWidgets::Checkbox(label.c_str(), &flag, + CheckboxOptions{ { .tooltip = label.c_str() } }.LabelPosition(LabelPosition::None))) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopID(); + } + ImGui::PopID(); +} + +void DrawFlagArray8Mask(const std::string& name, uint8_t& flags) { + ImGui::PushID(name.c_str()); + for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { + if ((flagIndex % 8) != 0) { + ImGui::SameLine(); + } + ImGui::PushID(flagIndex); + uint8_t bitMask = 1 << flagIndex; + bool flag = (flags & bitMask) != 0; + std::string label = fmt::format("0x{:02X} ({})", bitMask, flagIndex); + if (UIWidgets::Checkbox(label.c_str(), &flag, + CheckboxOptions{ { .tooltip = label.c_str() } }.LabelPosition(LabelPosition::None))) { + if (flag) { + flags |= bitMask; + } else { + flags &= ~bitMask; + } + } + ImGui::PopID(); + } + ImGui::PopID(); +} } // namespace UIWidgets diff --git a/src/port/ui/UIWidgets.h b/src/port/ui/UIWidgets.h index 93a7a9b2d..079c66869 100644 --- a/src/port/ui/UIWidgets.h +++ b/src/port/ui/UIWidgets.h @@ -1,231 +1,711 @@ -#pragma once +#ifndef UIWidgets_hpp +#define UIWidgets_hpp #include <string> #include <vector> #include <span> -#include <cstdint> +#include <stdint.h> #define IMGUI_DEFINE_MATH_OPERATORS #include <imgui.h> #include <libultraship/libultraship.h> +#include <unordered_map> +#include "port/ShipUtils.h" namespace UIWidgets { -struct TextFilters { - static int FilterNumbers(ImGuiInputTextCallbackData* data) { - if (data->EventChar < 256 && strchr("1234567890", (char) data->EventChar)) { - return 0; + using SectionFunc = void(*)(); + + struct TextFilters { + static int FilterNumbers(ImGuiInputTextCallbackData* data) { + if (data->EventChar < 256 && strchr("1234567890", (char)data->EventChar)) { + return 0; + } + return 1; + } + + static int FilterAlphaNum(ImGuiInputTextCallbackData* data) { + const char* alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ0123456789"; + if (data->EventChar < 256 && strchr(alphanum, (char)data->EventChar)) { + return 0; + } + return 1; + } + }; + + std::string WrappedText(const char* text, unsigned int charactersPerLine = 60); + std::string WrappedText(const std::string& text, unsigned int charactersPerLine = 60); + void Tooltip(const char* text); + + // mostly in order for colors usable by the menu without custom text color + enum Colors { + Red, + DarkRed, + Orange, + Green, + DarkGreen, + LightBlue, + Blue, + DarkBlue, + Indigo, + Violet, + Purple, + Brown, + Gray, + DarkGray, + // not suitable for menu theme use + Pink, + Yellow, + Cyan, + Black, + LightGray, + White, + NoColor + }; + + const std::unordered_map<Colors, ImVec4> ColorValues = { + { Colors::Pink, ImVec4(0.87f, 0.3f, 0.87f, 1.0f) }, + { Colors::Red, ImVec4(0.55f, 0.0f, 0.0f, 1.0f) }, + { Colors::DarkRed, ImVec4(0.3f, 0.0f, 0.0f, 1.0f) }, + { Colors::Orange, ImVec4(0.85f, 0.55f, 0.0f, 1.0f) }, + { Colors::Yellow, ImVec4(0.95f, 0.95f, 0.0f, 1.0f) }, + { Colors::Green, ImVec4(0.0f, 0.55f, 0.0f, 1.0f) }, + { Colors::DarkGreen, ImVec4(0.0f, 0.3f, 0.0f, 1.0f) }, + { Colors::Cyan, ImVec4(0.0f, 0.9f, 0.9f, 1.0f) }, + { Colors::LightBlue, ImVec4(0.0f, 0.24f, 0.8f, 1.0f) }, + { Colors::Blue, ImVec4(0.08f, 0.03f, 0.65f, 1.0f) }, + { Colors::DarkBlue, ImVec4(0.03f, 0.0f, 0.5f, 1.0f) }, + { Colors::Indigo, ImVec4(0.35f, 0.0f, 0.87f, 1.0f) }, + { Colors::Violet, ImVec4(0.5f, 0.0f, 0.9f, 1.0f) }, + { Colors::Purple, ImVec4(0.31f, 0.0f, 0.67f, 1.0f) }, + { Colors::Brown, ImVec4(0.37f, 0.18f, 0.0f, 1.0f) }, + { Colors::LightGray, ImVec4(0.75f, 0.75f, 0.75f, 1.0f) }, + { Colors::Gray, ImVec4(0.45f, 0.45f, 0.45f, 1.0f) }, + { Colors::DarkGray, ImVec4(0.15f, 0.15f, 0.15f, 1.0f) }, + { Colors::Black, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)}, + { Colors::White, ImVec4(1.0f, 1.0f, 1.0f, 1.0f) }, + { Colors::NoColor, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)}, + }; + + namespace Sizes { + const ImVec2 Inline = ImVec2(0.0f, 0.0f); + const ImVec2 Fill = ImVec2(-1.0f, 0.0f); + } + + enum LabelPosition { + Near, + Far, + Above, + None, + Within, + }; + + enum ComponentAlignment { + Left, + Right, + }; + + struct WidgetOptions{ + const char* tooltip = ""; + bool disabled = false; + const char* disabledTooltip = ""; + Colors color = Colors::NoColor; + + WidgetOptions& Color(Colors color_) { + color = color = color_; + return *this; + } + WidgetOptions& Tooltip(const char* tooltip_) { + tooltip = tooltip_; + return *this; + } + WidgetOptions& Disabled(bool disabled_) { + disabled = disabled_; + return *this; + } + }; + + struct ButtonOptions : WidgetOptions { + ImVec2 size = Sizes::Fill; + Colors color = Colors::Gray; + + ButtonOptions& Size(ImVec2 size_) { + size = size_; + return *this; + } + ButtonOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + ButtonOptions& Color(Colors color_) { + WidgetOptions::color = color = color_; + return *this; + } + }; + + struct CheckboxOptions : WidgetOptions { + bool defaultValue = false; // Only applicable to CVarCheckbox + ComponentAlignment alignment = ComponentAlignment::Left; + LabelPosition labelPosition = LabelPosition::Near; + Colors color = WidgetOptions::color = Colors::LightBlue; + + CheckboxOptions& DefaultValue(bool defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + CheckboxOptions& ComponentAlignment(ComponentAlignment alignment_) { + alignment = alignment_; + return *this; + } + CheckboxOptions& LabelPosition(LabelPosition labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + CheckboxOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + CheckboxOptions& Color(Colors color_) { + WidgetOptions::color = color = color_; + return *this; + } + }; + + struct ComboboxOptions : WidgetOptions { + std::unordered_map<int32_t, const char*> comboMap = {}; + uint32_t defaultIndex = 0; // Only applicable to CVarCombobox + ComponentAlignment alignment = ComponentAlignment::Left; + LabelPosition labelPosition = LabelPosition::Above; + ImGuiComboFlags flags = 0; + Colors color = Colors::LightBlue; + + ComboboxOptions& ComboMap(std::unordered_map<int32_t, const char*> comboMap_) { + comboMap = comboMap_; + return *this; + } + ComboboxOptions& DefaultIndex(uint32_t defaultIndex_) { + defaultIndex = defaultIndex_; + return *this; + } + ComboboxOptions& ComponentAlignment(ComponentAlignment alignment_) { + alignment = alignment_; + return *this; + } + ComboboxOptions& LabelPosition(LabelPosition labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + ComboboxOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + ComboboxOptions& Color(Colors color_) { + WidgetOptions::color = color = color_; + return *this; + } + }; + + struct IntSliderOptions : WidgetOptions { + bool showButtons = true; + const char* format = "%d"; + int32_t step = 1; + int32_t min = 1; + int32_t max = 10; + int32_t defaultValue = 1; + ComponentAlignment alignment = ComponentAlignment::Left; + LabelPosition labelPosition = LabelPosition::Above; + Colors color = Colors::Gray; + ImGuiSliderFlags flags = 0; + + IntSliderOptions& ShowButtons(bool showButtons_) { + showButtons = showButtons_; + return *this; + } + IntSliderOptions& Format(const char* format_) { + format = format_; + return *this; + } + IntSliderOptions& Step(int32_t step_) { + step = step_; + return *this; + } + IntSliderOptions& Min(int32_t min_) { + min = min_; + return *this; + } + IntSliderOptions& Max(int32_t max_) { + max = max_; + return *this; + } + IntSliderOptions& DefaultValue(int32_t defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + IntSliderOptions& ComponentAlignment(ComponentAlignment alignment_) { + alignment = alignment_; + return *this; + } + IntSliderOptions& LabelPosition(LabelPosition labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + IntSliderOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + IntSliderOptions& Color(Colors color_) { + WidgetOptions::color = color = color_; + return *this; + } + }; + + struct FloatSliderOptions : WidgetOptions { + bool showButtons = true; + const char* format = "%f"; + float step = 0.01f; + float min = 0.01f; + float max = 10.0f; + float defaultValue = 1.0f; + bool isPercentage = false; // Multiplies visual value by 100 + ComponentAlignment alignment = ComponentAlignment::Left; + LabelPosition labelPosition = LabelPosition::Above; + Colors color = Colors::Gray; + ImGuiSliderFlags flags = 0; + + FloatSliderOptions& ShowButtons(bool showButtons_) { + showButtons = showButtons_; + return *this; + } + FloatSliderOptions& Format(const char* format_) { + format = format_; + return *this; + } + FloatSliderOptions& Step(float step_) { + step = step_; + return *this; } - return 1; + FloatSliderOptions& Min(float min_) { + min = min_; + return *this; + } + FloatSliderOptions& Max(float max_) { + max = max_; + return *this; + } + FloatSliderOptions& DefaultValue(float defaultValue_) { + defaultValue = defaultValue_; + return *this; + } + FloatSliderOptions& ComponentAlignment(ComponentAlignment alignment_) { + alignment = alignment_; + return *this; + } + FloatSliderOptions& LabelPosition(LabelPosition labelPosition_) { + labelPosition = labelPosition_; + return *this; + } + FloatSliderOptions& IsPercentage(bool isPercentage_ = true) { + isPercentage = isPercentage_; + format = "%.0f"; + min = 0.0f; + max = 1.0f; + return *this; + } + FloatSliderOptions& Tooltip(const char* tooltip_) { + WidgetOptions::tooltip = tooltip_; + return *this; + } + FloatSliderOptions& Color(Colors color_) { + WidgetOptions::color = color = color_; + return *this; + } + }; + + void PushStyleMenu(const ImVec4& color); + void PushStyleMenu(Colors color = Colors::LightBlue); + void PopStyleMenu(); + bool BeginMenu(const char* label, Colors color = Colors::LightBlue); + + void PushStyleMenuItem(const ImVec4& color); + void PushStyleMenuItem(Colors color = Colors::LightBlue); + void PopStyleMenuItem(); + bool MenuItem(const char* label, const char* shortcut = NULL, Colors color = Colors::LightBlue); + + void PushStyleButton(const ImVec4& color); + void PushStyleButton(Colors color = Colors::Gray); + void PopStyleButton(); + bool Button(const char* label, const ButtonOptions& options = {}); + bool WindowButton(const char* label, const char* cvarName, std::shared_ptr<Ship::GuiWindow> windowPtr, const ButtonOptions& options = {}); + + void PushStyleCheckbox(const ImVec4& color); + void PushStyleCheckbox(Colors color = Colors::LightBlue); + void PopStyleCheckbox(); + void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash); + bool Checkbox(const char* label, bool* v, const CheckboxOptions& options = {}); + bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options = {}); + + void PushStyleCombobox(const ImVec4& color); + void PushStyleCombobox(Colors color = Colors::LightBlue); + void PopStyleCombobox(); + + /*using ComboVariant = std::variant<const std::unordered_map<int32_t, const char*>&, const std::vector<const char*>&>; + + bool Combobox(const char* label, int32_t* value, ComboVariant comboSource, const ComboboxOptions& options = {}) { + bool dirty = false; + float startX = ImGui::GetCursorPosX(); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - ImGui::GetStyle().ItemSpacing.x * 2); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboMap.at(*value)).x + ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + float width = ImGui::CalcTextSize(comboMap.at(*value)).x + ImGui::GetStyle().FramePadding.x * 4; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } + } + if (ImGui::BeginCombo(invisibleLabel, comboMap.at(*value), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (const auto& pair : comboMap) { + if (strlen(pair.second) > 1) { + if (ImGui::Selectable(pair.second, pair.first == *value)) { + *value = pair.first; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(startX); + ImGui::Text("%s", label); + } + } + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; } - static int FilterAlphaNum(ImGuiInputTextCallbackData* data) { - const char* alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ0123456789"; - if (data->EventChar < 256 && strchr(alphanum, (char) data->EventChar)) { - return 0; + bool CVarCombobox(const char* label, const char* cvarName, ComboVariant comboSource, const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox(label, &value, comboSource, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init(cvarName); + dirty = true; + } + return dirty; + }*/ + + template <typename T> + bool Combobox(const char* label, T* value, const std::unordered_map<T, const char*>& comboMap, const ComboboxOptions& options = {}) { + bool dirty = false; + float startX = ImGui::GetCursorPosX(); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - ImGui::GetStyle().ItemSpacing.x * 2); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboMap.at(*value)).x + ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + float width = ImGui::CalcTextSize(comboMap.at(*value)).x + ImGui::GetStyle().FramePadding.x * 4; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } + } + if (ImGui::BeginCombo(invisibleLabel, comboMap.at(*value), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (const auto& pair : comboMap) { + if (strlen(pair.second) > 1) { + if (ImGui::Selectable(pair.second, pair.first == *value)) { + *value = pair.first; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(startX); + ImGui::Text("%s", label); + } } - return 1; + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; } -}; - -// MARK: - Enums - -enum class CheckboxGraphics { Cross, Checkmark, None }; -constexpr float maxSliderWidth = 260.0f; -#ifdef __SWITCH__ -constexpr float sliderButtonWidth = 42.0f; -#elif defined(__WIIU__) -constexpr float sliderButtonWidth = 60.0f; -#else -constexpr float sliderButtonWidth = 30.0f; -#endif - -char* WrappedText(const char* text, unsigned int charactersPerLine = 60); -char* WrappedText(const std::string& text, unsigned int charactersPerLine); - -void SetLastItemHoverText(const std::string& text); -void SetLastItemHoverText(const char* text); - -void InsertHelpHoverText(const std::string& text); -void InsertHelpHoverText(const char* text); - -void Tooltip(const char* text); -void Spacer(float height); -void PaddedSeparator(bool padTop = true, bool padBottom = true, float extraVerticalTopPadding = 0.0f, - float extraVerticalBottomPadding = 0.0f); - -void RenderCross(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); -bool CustomCheckbox(const char* label, bool* v, bool disabled, CheckboxGraphics disabledGraphic); - -void ReEnableComponent(const char* disabledTooltipText); -void DisableComponent(const float alpha); - -bool EnhancementCheckbox(const char* text, const char* cvarName, bool disabled = false, - const char* disabledTooltipText = "", - CheckboxGraphics disabledGraphic = CheckboxGraphics::Cross, bool defaultValue = false); -bool PaddedEnhancementCheckbox(const char* text, const char* cvarName, bool padTop = true, bool padBottom = true, - bool disabled = false, const char* disabledTooltipText = "", - CheckboxGraphics disabledGraphic = CheckboxGraphics::Cross, bool defaultValue = false); - -bool EnhancementCombobox(const char* cvarName, std::span<const char*, std::dynamic_extent> comboArray, - uint8_t defaultIndex, bool disabled = false, const char* disabledTooltipText = "", - uint8_t disabledValue = -1); -bool LabeledRightAlignedEnhancementCombobox(const char* label, const char* cvarName, - std::span<const char*, std::dynamic_extent> comboArray, - uint8_t defaultIndex, bool disabled = false, - const char* disabledTooltipText = "", uint8_t disabledValue = -1); - -void PaddedText(const char* text, bool padTop = true, bool padBottom = true); - -bool EnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, const char* format, - int defaultValue = 0, bool PlusMinusButton = true, bool disabled = false, - const char* disabledTooltipText = ""); -bool PaddedEnhancementSliderInt(const char* text, const char* id, const char* cvarName, int min, int max, - const char* format, int defaultValue = 0, bool PlusMinusButton = true, - bool padTop = true, bool padBottom = true, bool disabled = false, - const char* disabledTooltipText = ""); -bool EnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, bool PlusMinusButton = true, - bool disabled = false, const char* disabledTooltipText = ""); -bool PaddedEnhancementSliderFloat(const char* text, const char* id, const char* cvarName, float min, float max, - const char* format, float defaultValue, bool isPercentage, - bool PlusMinusButton = true, bool padTop = true, bool padBottom = true, - bool disabled = false, const char* disabledTooltipText = ""); - -bool EnhancementRadioButton(const char* text, const char* cvarName, int id); - -bool DrawResetColorButton(const char* cvarName, ImVec4* colors, ImVec4 defaultcolors, bool has_alpha); -bool DrawRandomizeColorButton(const char* cvarName, ImVec4* colors); -void DrawLockColorCheckbox(const char* cvarName); -void RainbowColor(const char* cvarName, ImVec4* colors); - -void LoadPickersColors(ImVec4& ColorArray, const char* cvarname, const ImVec4& default_colors, bool has_alpha); -bool EnhancementColor(const char* text, const char* cvarName, ImVec4 ColorRGBA, ImVec4 default_colors, - bool allow_rainbow = true, bool has_alpha = false, bool TitleSameLine = false); - -void DrawFlagArray32(const std::string& name, uint32_t& flags); -void DrawFlagArray16(const std::string& name, uint16_t& flags); -void DrawFlagArray8(const std::string& name, uint8_t& flags); - -// V2 -namespace Colors { -const ImVec4 White = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); -const ImVec4 Gray = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); -const ImVec4 DarkGray = ImVec4(0.1f, 0.1f, 0.1f, 1.0f); -const ImVec4 Indigo = ImVec4(0.24f, 0.31f, 0.71f, 1.0f); -const ImVec4 Red = ImVec4(0.5f, 0.0f, 0.0f, 1.0f); -const ImVec4 DarkRed = ImVec4(0.3f, 0.0f, 0.0f, 1.0f); -const ImVec4 LightGreen = ImVec4(0.0f, 0.7f, 0.0f, 1.0f); -const ImVec4 Green = ImVec4(0.0f, 0.5f, 0.0f, 1.0f); -const ImVec4 DarkGreen = ImVec4(0.0f, 0.3f, 0.0f, 1.0f); -const ImVec4 Yellow = ImVec4(1.0f, 0.627f, 0.0f, 1.0f); -}; // namespace Colors - -namespace Sizes { -const ImVec2 Inline = ImVec2(0.0f, 0.0f); -const ImVec2 Fill = ImVec2(-1.0f, 0.0f); -} // namespace Sizes - -enum LabelPosition { - Near, - Far, - Above, - None, - Within, -}; - -enum ComponentAlignment { - Left, - Right, -}; - -void PushStyleMenu(const ImVec4& color = Colors::Indigo); -void PopStyleMenu(); -bool BeginMenu(const char* label, const ImVec4& color = Colors::Indigo); - -void PushStyleMenuItem(const ImVec4& color = Colors::Indigo); -void PopStyleMenuItem(); -bool MenuItem(const char* label, const char* shortcut = NULL, const ImVec4& color = Colors::Indigo); - -struct ButtonOptions { - const ImVec4 color = Colors::Gray; - const ImVec2 size = Sizes::Fill; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; -}; - -void PushStyleButton(const ImVec4& color = Colors::Gray); -void PopStyleButton(); -bool Button(const char* label, const ButtonOptions& options = {}); -bool WindowButton(const char* label, const char* cvarName, std::shared_ptr<Ship::GuiWindow> windowPtr, - const ButtonOptions& options = {}); - -struct CheckboxOptions { - const ImVec4 color = Colors::Indigo; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool defaultValue = false; // Only applicable to CVarCheckbox - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Near; -}; - -void PushStyleCheckbox(const ImVec4& color = Colors::Indigo); -void PopStyleCheckbox(); -bool Checkbox(const char* label, bool* v, const CheckboxOptions& options = {}); -bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options = {}); - -struct ComboboxOptions { - const ImVec4 color = Colors::Indigo; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - uint32_t defaultIndex = 0; // Only applicable to CVarCombobox - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; - ImGuiComboFlags flags = 0; -}; - -void PushStyleCombobox(const ImVec4& color = Colors::Indigo); -void PopStyleCombobox(); -bool Combobox(const char* label, uint8_t* value, std::span<const char*, std::dynamic_extent> comboArray, - const ComboboxOptions& options = {}); -bool CVarCombobox(const char* label, const char* cvarName, std::span<const char*, std::dynamic_extent> comboArray, - const ComboboxOptions& options = {}); - -struct IntSliderOptions { - const ImVec4 color = Colors::Gray; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool showButtons = true; - ImGuiSliderFlags flags = 0; - const char* format = "%d"; - const uint32_t step = 1; - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; -}; - -struct FloatSliderOptions { - const ImVec4 color = Colors::Gray; - const char* tooltip = ""; - bool disabled = false; - const char* disabledTooltip = ""; - bool showButtons = true; - ImGuiSliderFlags flags = 0; - const char* format = "%f"; - const float step = 0.01f; - bool isPercentage = false; // Multiplies visual value by 100 - ComponentAlignment alignment = ComponentAlignment::Left; - LabelPosition labelPosition = LabelPosition::Above; -}; - -void PushStyleSlider(const ImVec4& color = Colors::Indigo); -void PopStyleSlider(); -bool SliderInt(const char* label, int32_t* value, int32_t min, int32_t max, const IntSliderOptions& options = {}); -bool CVarSliderInt(const char* label, const char* cvarName, int32_t min, int32_t max, const int32_t defaultValue, - const IntSliderOptions& options = {}); -bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options = {}); -bool CVarSliderFloat(const char* label, const char* cvarName, float min, float max, const float defaultValue, - const FloatSliderOptions& options = {}); -} // namespace UIWidgets + + template <typename T = size_t> + bool Combobox(const char* label, T* value, const std::vector<const char*>& comboVector, const ComboboxOptions& options = {}) { + bool dirty = false; + float startX = ImGui::GetCursorPosX(); + size_t currentValueIndex = static_cast<size_t>(*value); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::Text(label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - ImGui::GetStyle().ItemSpacing.x * 2); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboVector.at(currentValueIndex)).x + ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text(label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + float width = ImGui::CalcTextSize(comboVector.at(currentValueIndex)).x + ImGui::GetStyle().FramePadding.x * 4; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } + } + if (ImGui::BeginCombo(invisibleLabel, comboVector.at(currentValueIndex), options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (size_t i = 0; i < comboVector.size(); ++i) { + auto newValue = static_cast<T>(i); + if (strlen(comboVector.at(i)) > 1) { + if (ImGui::Selectable(comboVector.at(i), newValue == *value)) { + *value = newValue; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(); + ImGui::Text(label); + } else if (options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text(label); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(startX); + ImGui::Text(label); + } + } + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; + } + + template <typename T = size_t, size_t N> + bool Combobox(const char* label, T* value, const char* (&comboArray)[N], const ComboboxOptions& options = {}) { + bool dirty = false; + float startX = ImGui::GetCursorPosX(); + size_t currentValueIndex = static_cast<size_t>(*value); + if (currentValueIndex >= N) { + currentValueIndex = 0; + } + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + ImGui::PushID(label); + ImGui::BeginGroup(); + ImGui::BeginDisabled(options.disabled); + PushStyleCombobox(options.color); + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x - ImGui::GetStyle().ItemSpacing.x * 2); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + ImGui::SetNextItemWidth(ImGui::CalcTextSize(comboArray[currentValueIndex]).x + ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::NewLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x * 2); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { + float width = ImGui::CalcTextSize(comboArray[currentValueIndex]).x + ImGui::GetStyle().FramePadding.x * 4; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + } + } + if (ImGui::BeginCombo(invisibleLabel, comboArray[currentValueIndex], options.flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 10.0f)); + for (size_t i = 0; i < N; ++i) { + auto newValue = static_cast<T>(i); + if (strlen(comboArray[i]) > 1) { + if (ImGui::Selectable(comboArray[i], newValue == *value)) { + *value = newValue; + dirty = true; + } + } + } + ImGui::PopStyleVar(); + ImGui::EndCombo(); + } + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Near) { + ImGui::SameLine(); + ImGui::Text("%s", label); + } else if (options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x); + ImGui::Text("%s", label); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { + ImGui::SameLine(startX); + ImGui::Text("%s", label); + } + } + PopStyleCombobox(); + ImGui::EndDisabled(); + ImGui::EndGroup(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + ImGui::PopID(); + return dirty; + } + + template <typename T = int32_t> + bool CVarCombobox(const char* label, const char* cvarName, const std::unordered_map<T, const char*>& comboMap, const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox<T>(label, &value, comboMap, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + dirty = true; + } + return dirty; + } + + template <typename T = int32_t> + bool CVarCombobox(const char* label, const char* cvarName, const std::vector<const char*>& comboVector, const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox<T>(label, &value, comboVector, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + dirty = true; + } + return dirty; + } + + template <typename T = int32_t, size_t N> + bool CVarCombobox(const char* label, const char* cvarName, const char* (&comboArray)[N], const ComboboxOptions& options = {}) { + bool dirty = false; + int32_t value = CVarGetInteger(cvarName, options.defaultIndex); + if (Combobox<T>(label, &value, comboArray, options)) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + dirty = true; + } + return dirty; + } + + void PushStyleSlider(Colors color = Colors::LightBlue); + void PopStyleSlider(); + bool SliderInt(const char* label, int32_t* value, const IntSliderOptions& options = {}); + bool CVarSliderInt(const char* label, const char* cvarName, const IntSliderOptions& options = {}); + bool SliderFloat(const char* label, float* value, const FloatSliderOptions& options = {}); + bool CVarSliderFloat(const char* label, const char* cvarName, const FloatSliderOptions& options = {}); + bool CVarColorPicker(const char* label, const char* cvarName, Color_RGBA8 defaultColor); + void DrawFlagArray32(const std::string& name, uint32_t& flags); + void DrawFlagArray16(const std::string& name, uint16_t& flags); + void DrawFlagArray8(const std::string& name, uint8_t& flags); + void DrawFlagArray8Mask(const std::string& name, uint8_t& flags); +} + +#endif /* UIWidgets_hpp */ |
