diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 35bddb40..63483604 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -81,6 +81,7 @@ set(SWA_PATCHES_CXX_SOURCES set(SWA_UI_CXX_SOURCES "ui/achievement_menu.cpp" "ui/achievement_overlay.cpp" + "ui/message_window.cpp" "ui/options_menu.cpp" "ui/sdl_listener.cpp" "ui/window.cpp" diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index abed18dd..45325c22 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "imgui_snapshot.h" @@ -1065,6 +1066,7 @@ static void CreateImGuiBackend() AchievementMenu::Init(); AchievementOverlay::Init(); + MessageWindow::Init(); OptionsMenu::Init(); ImGui_ImplSDL2_InitForOther(Window::s_pWindow); @@ -1739,6 +1741,7 @@ static void DrawImGui() AchievementMenu::Draw(); OptionsMenu::Draw(); AchievementOverlay::Draw(); + MessageWindow::Draw(); ImGui::Render(); diff --git a/UnleashedRecomp/stdafx.h b/UnleashedRecomp/stdafx.h index bf524468..964e928a 100644 --- a/UnleashedRecomp/stdafx.h +++ b/UnleashedRecomp/stdafx.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/UnleashedRecomp/ui/imgui_utils.h b/UnleashedRecomp/ui/imgui_utils.h index b3a1bd92..728e5c57 100644 --- a/UnleashedRecomp/ui/imgui_utils.h +++ b/UnleashedRecomp/ui/imgui_utils.h @@ -3,6 +3,12 @@ #include #include +#define PIXELS_TO_UV_COORDS(textureWidth, textureHeight, x, y, width, height) \ + std::make_tuple(ImVec2((float)x / (float)textureWidth, (float)y / (float)textureHeight), \ + ImVec2(((float)x + (float)width) / (float)textureWidth, ((float)y + (float)height) / (float)textureHeight)) + +#define GET_UV_COORDS(tuple) std::get<0>(tuple), std::get<1>(tuple) + static std::vector> g_callbackData; static uint32_t g_callbackDataIndex = 0; @@ -150,7 +156,80 @@ static void DrawTextWithShadow(const ImFont* font, float fontSize, const ImVec2& DrawTextWithOutline(font, fontSize, { pos.x + offset, pos.y + offset }, shadowColour, text, radius, shadowColour); - drawList->AddText(font, fontSize, pos, colour, text); + drawList->AddText(font, fontSize, pos, colour, text, nullptr); +} + +static float CalcWidestTextSize(const ImFont* font, float fontSize, std::span strs) +{ + auto result = 0.0f; + + for (auto& str : strs) + result = std::max(result, font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()).x); + + return result; +} + +static std::vector Split(const char* str, char delimiter) +{ + std::vector result; + + if (!str) + return result; + + const char* start = str; + const char* current = str; + + while (*current) + { + if (*current == delimiter) + { + result.emplace_back(start, current - start); + start = current + 1; + } + + current++; + } + + result.emplace_back(start); + + return result; +} + +static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, std::vector lines) +{ + auto x = 0.0f; + auto y = 0.0f; + + for (auto& str : lines) + { + auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); + + x = std::max(x, textSize.x); + y += textSize.y + Scale(lineMargin); + } + + return { x, y }; +} + +static ImVec2 MeasureCentredParagraph(const ImFont* font, float fontSize, float lineMargin, const char* text) +{ + return MeasureCentredParagraph(font, fontSize, lineMargin, Split(text, '\n')); +} + +static void DrawCentredParagraph(const ImFont* font, float fontSize, const ImVec2& centre, float lineMargin, const char* text, std::function drawMethod) +{ + auto lines = Split(text, '\n'); + auto paragraphSize = MeasureCentredParagraph(font, fontSize, lineMargin, lines); + auto offsetY = 0.0f; + + for (auto& str : lines) + { + auto textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str()); + + drawMethod(str.c_str(), ImVec2(/* X */ centre.x - textSize.x / 2, /* Y */ centre.y - paragraphSize.y / 2 + offsetY)); + + offsetY += textSize.y + Scale(lineMargin); + } } static void DrawTextWithMarqueeShadow(const ImFont* font, float fontSize, const ImVec2& pos, const ImVec2& min, const ImVec2& max, ImU32 colour, const char* text, double time, double delay, double speed, float offset = 2.0f, float radius = 0.4f, ImU32 shadowColour = IM_COL32(0, 0, 0, 255)) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp new file mode 100644 index 00000000..34c7bd49 --- /dev/null +++ b/UnleashedRecomp/ui/message_window.cpp @@ -0,0 +1,335 @@ +#include "message_window.h" +#include "imgui_utils.h" +#include +#include +#include +#include "../UnleashedRecompResources/images/pause.h" + +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_START = 0; +constexpr double OVERLAY_CONTAINER_COMMON_MOTION_END = 11; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_START = 5; +constexpr double OVERLAY_CONTAINER_INTRO_FADE_END = 9; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_START = 0; +constexpr double OVERLAY_CONTAINER_OUTRO_FADE_END = 4; + +static bool g_isAwaitingResult = false; +static bool g_isClosing = false; +static bool g_isControlsVisible = false; + +static int g_selectedRowIndex; +static int g_foregroundCount; + +static bool g_upWasHeld; +static bool g_downWasHeld; + +static double g_appearTime; +static double g_controlsAppearTime; + +static ImFont* g_fntSeurat; + +static std::unique_ptr g_upSelectionCursor; + +std::string g_text; +int g_result; +std::vector g_buttons; +int g_defaultButtonIndex; + +bool DrawContainer(float appearTime, ImVec2 centre, ImVec2 max, bool isForeground = true) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + ImVec2 _min = { centre.x - max.x, centre.y - max.y }; + ImVec2 _max = { centre.x + max.x, centre.y + max.y }; + + // Expand/retract animation. + auto containerMotion = ComputeMotion(appearTime, OVERLAY_CONTAINER_COMMON_MOTION_START, OVERLAY_CONTAINER_COMMON_MOTION_END); + + if (g_isClosing) + { + _min.x = Hermite(_min.x, centre.x, containerMotion); + _max.x = Hermite(_max.x, centre.x, containerMotion); + _min.y = Hermite(_min.y, centre.y, containerMotion); + _max.y = Hermite(_max.y, centre.y, containerMotion); + } + else + { + _min.x = Hermite(centre.x, _min.x, containerMotion); + _max.x = Hermite(centre.x, _max.x, containerMotion); + _min.y = Hermite(centre.y, _min.y, containerMotion); + _max.y = Hermite(centre.y, _max.y, containerMotion); + } + + auto vertices = GetPauseContainerVertices(_min, _max); + + // Transparency fade animation. + auto colourMotion = g_isClosing + ? ComputeMotion(appearTime, OVERLAY_CONTAINER_OUTRO_FADE_START, OVERLAY_CONTAINER_OUTRO_FADE_END) + : ComputeMotion(appearTime, OVERLAY_CONTAINER_INTRO_FADE_START, OVERLAY_CONTAINER_INTRO_FADE_END); + + auto alpha = g_isClosing + ? Lerp(1, 0, colourMotion) + : Lerp(0, 1, colourMotion); + + if (!isForeground) + g_foregroundCount++; + + if (isForeground) + drawList->AddRectFilled({ 0.0f, 0.0f }, ImGui::GetIO().DisplaySize, IM_COL32(0, 0, 0, 223 * (g_foregroundCount ? 1 : alpha))); + + auto colShadow = IM_COL32(0, 0, 0, 156 * alpha); + auto colGradientTop = IM_COL32(197, 194, 197, 200 * alpha); + auto colGradientBottom = IM_COL32(115, 113, 115, 236 * alpha); + + // Draw vertices with gradient. + SetGradient(_min, _max, colGradientTop, colGradientBottom); + drawList->AddConvexPolyFilled(vertices.data(), vertices.size(), IM_COL32(255, 255, 255, 255 * alpha)); + ResetGradient(); + + // Draw outline. + drawList->AddPolyline + ( + vertices.data(), + vertices.size(), + IM_COL32(247, 247, 247, 255 * alpha), + true, + Scale(2.5f) + ); + + // Offset vertices to draw 3D effect lines. + for (int i = 0; i < vertices.size(); i++) + { + vertices[i].x -= Scale(0.4f); + vertices[i].y -= Scale(0.2f); + } + + auto colLineTop = IM_COL32(165, 170, 165, 230 * alpha); + auto colLineBottom = IM_COL32(190, 190, 190, 230 * alpha); + auto lineThickness = Scale(1.0f); + + // Top left corner bottom to top left corner top. + drawList->AddLine(vertices[0], vertices[1], colLineTop, lineThickness * 0.5f); + + // Top left corner bottom to bottom left. + drawList->AddRectFilledMultiColor({ vertices[0].x - 0.2f, vertices[0].y }, { vertices[6].x + lineThickness - 0.2f, vertices[6].y }, colLineTop, colLineTop, colLineBottom, colLineBottom); + + // Top left corner top to top right. + drawList->AddLine(vertices[1], vertices[2], colLineTop, lineThickness); + + drawList->PushClipRect(_min, _max); + + return containerMotion >= 1.0f && !g_isClosing; +} + +void DrawButton(int rowIndex, float yOffset, float width, float height, std::string& text) +{ + auto drawList = ImGui::GetForegroundDrawList(); + + auto clipRectMin = drawList->GetClipRectMin(); + auto clipRectMax = drawList->GetClipRectMax(); + + ImVec2 min = { clipRectMin.x + ((clipRectMax.x - clipRectMin.x) - width) / 2, clipRectMin.y + height * rowIndex + yOffset }; + ImVec2 max = { min.x + width, min.y + height }; + + bool isSelected = rowIndex == g_selectedRowIndex; + + if (isSelected) + { + static auto breatheStart = ImGui::GetTime(); + auto alpha = Lerp(1.0f, 0.75f, (sin((ImGui::GetTime() - breatheStart) * (2.0f * M_PI / (55.0f / 60.0f))) + 1.0f) / 2.0f); + auto colour = IM_COL32(255, 255, 255, 255 * alpha); + + auto width = Scale(11); + auto left = PIXELS_TO_UV_COORDS(128, 128, 0, 0, 11, 50); + auto centre = PIXELS_TO_UV_COORDS(128, 128, 11, 0, 8, 50); + auto right = PIXELS_TO_UV_COORDS(128, 128, 19, 0, 11, 50); + + drawList->AddImage(g_upSelectionCursor.get(), min, { min.x + width, max.y }, GET_UV_COORDS(left), colour); + drawList->AddImage(g_upSelectionCursor.get(), { min.x + width, min.y }, { max.x - width, max.y }, GET_UV_COORDS(centre), colour); + drawList->AddImage(g_upSelectionCursor.get(), { max.x - width, min.y }, max, GET_UV_COORDS(right), colour); + } + + auto fontSize = Scale(28); + auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, text.c_str()); + + DrawTextWithShadow + ( + g_fntSeurat, + fontSize, + { /* X */ min.x + ((max.x - min.x) - textSize.x) / 2, /* Y */ min.y + ((max.y - min.y) - textSize.y) / 2 }, + isSelected ? IM_COL32(255, 128, 0, 255) : IM_COL32(255, 255, 255, 255), + text.c_str() + ); +} + +static void ResetSelection() +{ + g_selectedRowIndex = g_defaultButtonIndex; + g_upWasHeld = false; + g_downWasHeld = false; +} + +void MessageWindow::Init() +{ + auto& io = ImGui::GetIO(); + + constexpr float FONT_SCALE = 2.0f; + + g_fntSeurat = io.Fonts->AddFontFromFileTTF("FOT-SeuratPro-M.otf", 28.0f * FONT_SCALE); + + g_upSelectionCursor = LoadTexture((uint8_t*)g_res_pause, g_res_pause_size); +} + +void MessageWindow::Draw() +{ + if (!s_isVisible) + return; + + auto pInputState = SWA::CInputState::GetInstance(); + auto drawList = ImGui::GetForegroundDrawList(); + auto& res = ImGui::GetIO().DisplaySize; + + ImVec2 centre = { res.x / 2, res.y / 2 }; + + auto fontSize = Scale(28); + auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, 5, g_text.c_str()); + auto textMarginX = Scale(32); + auto textMarginY = Scale(40); + + if (DrawContainer(g_appearTime, centre, { textSize.x / 2 + textMarginX, textSize.y / 2 + textMarginY }, !g_isControlsVisible)) + { + DrawCentredParagraph + ( + g_fntSeurat, + fontSize, + { centre.x, centre.y + Scale(3) }, + 5, + g_text.c_str(), + + [=](const char* str, ImVec2 pos) + { + DrawTextWithShadow(g_fntSeurat, fontSize, pos, IM_COL32(255, 255, 255, 255), str); + } + ); + + drawList->PopClipRect(); + + if (g_buttons.size()) + { + auto itemWidth = std::max(Scale(162), Scale(CalcWidestTextSize(g_fntSeurat, fontSize, g_buttons))); + auto itemHeight = Scale(57); + auto windowMarginX = Scale(18); + auto windowMarginY = Scale(25); + + ImVec2 controlsMax = { /* X */ itemWidth / 2 + windowMarginX, /* Y */ itemHeight / 2 * g_buttons.size() + windowMarginY }; + + if (g_isControlsVisible && DrawContainer(g_controlsAppearTime, centre, controlsMax)) + { + auto rowCount = 0; + + for (auto& button : g_buttons) + DrawButton(rowCount++, windowMarginY, itemWidth, itemHeight, button); + + drawList->PopClipRect(); + + bool upIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || + pInputState->GetPadState().LeftStickVertical > 0.5f; + + bool downIsHeld = pInputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || + pInputState->GetPadState().LeftStickVertical < -0.5f; + + bool scrollUp = !g_upWasHeld && upIsHeld; + bool scrollDown = !g_downWasHeld && downIsHeld; + + if (scrollUp) + { + --g_selectedRowIndex; + if (g_selectedRowIndex < 0) + g_selectedRowIndex = rowCount - 1; + } + else if (scrollDown) + { + ++g_selectedRowIndex; + if (g_selectedRowIndex >= rowCount) + g_selectedRowIndex = 0; + } + + if (scrollUp || scrollDown) + Game_PlaySound("sys_actstg_pausecursor"); + + g_upWasHeld = upIsHeld; + g_downWasHeld = downIsHeld; + + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + { + g_result = g_selectedRowIndex; + + Game_PlaySound("sys_actstg_pausedecide"); + MessageWindow::Close(); + } + else if (pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) + { + g_result = -1; + + Game_PlaySound("sys_actstg_pausecansel"); + MessageWindow::Close(); + } + } + else + { + if (!g_isControlsVisible && pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + { + g_controlsAppearTime = ImGui::GetTime(); + g_isControlsVisible = true; + + Game_PlaySound("sys_actstg_pausewinopen"); + } + } + } + else + { + if (pInputState->GetPadState().IsTapped(SWA::eKeyState_A)) + MessageWindow::Close(); + } + } +} + +bool MessageWindow::Open(std::string text, int* result, std::span buttons, int defaultButtonIndex) +{ + if (!g_isAwaitingResult && *result == -1) + { + s_isVisible = true; + g_isClosing = false; + g_isControlsVisible = false; + g_foregroundCount = 0; + g_appearTime = ImGui::GetTime(); + g_controlsAppearTime = ImGui::GetTime(); + + g_text = text; + g_buttons = std::vector(buttons.begin(), buttons.end()); + g_defaultButtonIndex = defaultButtonIndex; + + ResetSelection(); + + Game_PlaySound("sys_actstg_pausewinopen"); + + g_isAwaitingResult = true; + } + + *result = g_result; + + return !g_isAwaitingResult; +} + +void MessageWindow::Close() +{ + if (!g_isClosing) + { + g_appearTime = ImGui::GetTime(); + g_controlsAppearTime = ImGui::GetTime(); + g_isClosing = true; + g_isControlsVisible = false; + g_isAwaitingResult = false; + } + + Game_PlaySound("sys_actstg_pausewinclose"); +} diff --git a/UnleashedRecomp/ui/message_window.h b/UnleashedRecomp/ui/message_window.h new file mode 100644 index 00000000..b1baca00 --- /dev/null +++ b/UnleashedRecomp/ui/message_window.h @@ -0,0 +1,12 @@ +#pragma once + +class MessageWindow +{ +public: + inline static bool s_isVisible = false; + + static void Init(); + static void Draw(); + static bool Open(std::string text, int* result, std::span buttons = {}, int defaultButtonIndex = 0); + static void Close(); +}; diff --git a/UnleashedRecomp/ui/options_menu.h b/UnleashedRecomp/ui/options_menu.h index 6ea404b5..85268806 100644 --- a/UnleashedRecomp/ui/options_menu.h +++ b/UnleashedRecomp/ui/options_menu.h @@ -2,7 +2,7 @@ #include -struct OptionsMenu +class OptionsMenu { public: inline static bool s_isVisible = false;