#include "options_menu.h" #include "options_menu_thumbnails.h" #include "tv_static.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static constexpr double MILES_ELECTRIC_SCALE_DURATION = 16.0; static constexpr double MILES_ELECTRIC_BACKGROUND_DURATION = 16.0; static constexpr double MILES_ELECTRIC_FOREGROUND_FADE_DURATION = 6.0; static constexpr double MILES_ELECTRIC_FOREGROUND_FADE_IN_TIME = MILES_ELECTRIC_SCALE_DURATION - 2.0; static constexpr double MILES_ELECTRIC_FOREGROUND_FADE_OUT_TIME = MILES_ELECTRIC_FOREGROUND_FADE_IN_TIME + MILES_ELECTRIC_FOREGROUND_FADE_DURATION; static constexpr double VALUE_SLIDER_INTRO_DURATION = 20.0; static constexpr double CONTAINER_LINE_ANIMATION_DURATION = 8.0; static constexpr double CONTAINER_OUTER_TIME = CONTAINER_LINE_ANIMATION_DURATION + 8.0; // 8 frame delay static constexpr double CONTAINER_OUTER_DURATION = 8.0; static constexpr double CONTAINER_INNER_TIME = CONTAINER_OUTER_TIME + CONTAINER_OUTER_DURATION + 8.0; // 8 frame delay static constexpr double CONTAINER_INNER_DURATION = 8.0; static constexpr double CONTAINER_BACKGROUND_TIME = CONTAINER_INNER_TIME + CONTAINER_INNER_DURATION + 8.0; // 8 frame delay static constexpr double CONTAINER_BACKGROUND_DURATION = 12.0; static constexpr double CONTAINER_FULL_DURATION = CONTAINER_BACKGROUND_TIME + CONTAINER_BACKGROUND_DURATION; static constexpr double CONTAINER_CATEGORY_TIME = (CONTAINER_INNER_TIME + CONTAINER_BACKGROUND_TIME) / 2.0; static constexpr double CONTAINER_CATEGORY_DURATION = 12.0; static constexpr float CONTAINER_POS_Y = 117.0f; static constexpr float SETTINGS_WIDE_GRID_COUNT = 90.0f; static constexpr float INFO_WIDE_GRID_COUNT = 42.0f; static constexpr float PADDING_WIDE_GRID_COUNT = 3.0f; static constexpr float SETTINGS_NARROW_GRID_COUNT = 70.0f; static constexpr float INFO_NARROW_GRID_COUNT = 34.0f; static constexpr float PADDING_NARROW_GRID_COUNT = 1.0f; static constexpr float INFO_TEXT_MARQUEE_DELAY = 1.2f; static constexpr int32_t g_categoryCount = 4; static int32_t g_categoryIndex; static ImVec2 g_categoryAnimMin; static ImVec2 g_categoryAnimMax; static int32_t g_firstVisibleRowIndex; static int32_t g_prevSelectedRowIndex; static int32_t g_selectedRowIndex; static double g_rowSelectionTime; static float g_prevOffsetRatio; static bool g_leftWasHeld; static bool g_upWasHeld; static bool g_rightWasHeld; static bool g_downWasHeld; static bool g_lockedOnOption; static double g_lockedOnTime; static double g_lastTappedTime; static double g_lastIncrementTime; static double g_lastIncrementSoundTime; static double g_fastIncrementHoldTime; static constexpr size_t GRID_SIZE = 9; static ImFont* g_seuratFont; static ImFont* g_dfsogeistdFont; static ImFont* g_newRodinFont; static const IConfigDef* g_selectedItem; static std::string* g_inaccessibleReason; static bool g_isStage = false; static bool g_isClosing = false; static bool g_isControlsVisible = false; static bool g_isEnterKeyBuffered = false; static bool g_canReset = false; static bool g_isLanguageOptionChanged = false; static bool g_titleAnimBegin = true; static EChannelConfiguration g_currentChannelConfig; static double g_appearTime = 0.0; static std::unique_ptr g_upMilesElectric; static float g_rightStickY; class SDLEventListenerForOptionsMenu : public SDLEventListener { public: bool OnSDLEvent(SDL_Event* event) override { if (!OptionsMenu::s_isVisible || !hid::IsInputAllowed()) return false; if (event->type == SDL_CONTROLLERAXISMOTION && event->caxis.axis == SDL_CONTROLLER_AXIS_RIGHTY) g_rightStickY = event->caxis.value / 32767.0f; return false; } } g_sdlEventListenerForOptionsMenu; static void DrawTitle() { static constexpr double fadeOffset = 3.0; static constexpr double fadeDuration = 28.0; static constexpr double fadeAdditiveDuration = 20.0; static constexpr double squareMoveDuration = 5.0; static constexpr double squareMoveEndDuration = 40.0; static constexpr double squareBlinkDuration = 10.0; static constexpr double squareFadeDuration = 45.0; static bool isRectVisible; static bool isRectFinalAdjustment; static float rectX; static double rectMoveMotionOffset; static double rectBlinkMotionOffset; if (g_titleAnimBegin) { isRectVisible = true; isRectFinalAdjustment = false; rectX = 0; rectMoveMotionOffset = 0; rectBlinkMotionOffset = 0; g_titleAnimBegin = false; } auto drawList = ImGui::GetBackgroundDrawList(); auto x = Scale(122); auto y = Scale(56); if (g_aspectRatio >= WIDE_ASPECT_RATIO) x += g_aspectRatioOffsetX; else x += (1.0f - g_aspectRatioNarrowScale) * g_aspectRatioScale * -20.0f; ImVec2 optionsMin = { x, y }; auto drawText = [&](float alpha) { DrawTextWithOutline ( g_dfsogeistdFont, Scale(48), optionsMin, IM_COL32(255, 190, 33, 255 * alpha), Localise("Options_Header_Name").c_str(), 4, IM_COL32(0, 0, 0, 255 * alpha), IMGUI_SHADER_MODIFIER_TITLE_BEVEL ); }; if (g_isStage) { drawText(1.0f); return; } drawText(Hermite(0.0f, 1.0f, ComputeMotion(g_appearTime, fadeOffset, fadeDuration))); auto rectMoveMotion = ComputeMotion(g_appearTime, rectMoveMotionOffset, squareMoveDuration); auto rectEndMotion = ComputeMotion(g_appearTime, 0.0, squareMoveEndDuration); auto rectBlinkMotion = sin(ComputeMotion(g_appearTime, squareMoveEndDuration + rectBlinkMotionOffset, squareBlinkDuration) * M_PI); auto rectAlphaMotion = 1.0f; auto rectY = Scale(10); auto rectSize = Scale(32); if (rectBlinkMotion > 0.0) { if (!isRectFinalAdjustment) { /* Ensure the blinking animation starts past the German localisation. */ rectX += rectSize + (rectSize * 0.25f); isRectFinalAdjustment = true; } isRectVisible = !isRectVisible; rectBlinkMotionOffset += squareBlinkDuration; } if (rectEndMotion >= 1.0) { // Fade out the square. rectAlphaMotion = 1.0f - ComputeMotion(g_appearTime, squareMoveEndDuration + squareBlinkDuration, squareFadeDuration); } else if (rectMoveMotion >= 1.0) { // Move the square along in steps by its own width and offset the animation to repeat. rectX += rectSize; rectMoveMotionOffset += squareMoveDuration; } if (isRectVisible) { float rectScale = 1.0f; if (rectBlinkMotion == 0.0) { constexpr float RECT_SCALES[] = { 1.2f, 1.1f, 1.1f, 1.0f, 1.15f, 0.4f, 1.2f, 1.1f, 1.05f, 1.0f, 1.5f, 1.2f, 1.0f }; rectScale = RECT_SCALES[uint32_t(round(rectX / rectSize)) % std::size(RECT_SCALES)]; } ImVec2 rectMin = { optionsMin.x + rectX, optionsMin.y + rectY }; ImVec2 rectMax = { optionsMin.x + rectX + rectSize * rectScale, optionsMin.y + rectY + rectSize }; auto rectOutlineMargin = Scale(2.5f); ImVec2 rectOutlineMin = { rectMin.x - rectOutlineMargin, rectMin.y - rectOutlineMargin }; ImVec2 rectOutlineMax = { rectMax.x + rectOutlineMargin, rectMax.y + rectOutlineMargin }; drawList->AddRectFilled(rectOutlineMin, rectOutlineMax, IM_COL32(0, 0, 0, 255 * rectAlphaMotion), Scale(5)); SetShaderModifier(IMGUI_SHADER_MODIFIER_RECTANGLE_BEVEL); SetGradient(rectMin, rectMax, IM_COL32_WHITE, IM_COL32_WHITE); drawList->AddRectFilled(rectMin, rectMax, IM_COL32(255, 188, 0, 255 * rectAlphaMotion)); ResetGradient(); SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); } // The flash gets rendered after the rectangle in the original game. SetAdditive(true); drawText(1.0 - 2.0 * abs(ComputeLinearMotion(g_appearTime, fadeDuration, fadeAdditiveDuration) - 0.5)); SetAdditive(false); } static void DrawScanlineBars() { constexpr uint32_t COLOR0 = IM_COL32(203, 255, 0, 0); constexpr uint32_t COLOR1 = IM_COL32(203, 255, 0, 55); constexpr uint32_t FADE_COLOR0 = IM_COL32(0, 0, 0, 255); constexpr uint32_t FADE_COLOR1 = IM_COL32(0, 0, 0, 0); float height = Scale(105.0f); auto& res = ImGui::GetIO().DisplaySize; auto drawList = ImGui::GetBackgroundDrawList(); if (OptionsMenu::s_pauseMenuType != SWA::eMenuType_WorldMap) { // Top bar fade drawList->AddRectFilledMultiColor ( { 0.0f, 0.0f }, { res.x, height }, FADE_COLOR0, FADE_COLOR0, FADE_COLOR1, FADE_COLOR1 ); // Bottom bar fade drawList->AddRectFilledMultiColor ( { res.x, res.y }, { 0.0f, res.y - height }, FADE_COLOR0, FADE_COLOR0, FADE_COLOR1, FADE_COLOR1 ); } SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE); // Top bar drawList->AddRectFilledMultiColor ( { 0.0f, 0.0f }, { res.x, height }, COLOR0, COLOR0, COLOR1, COLOR1 ); // Bottom bar ImVec2 max{ 0.0f, res.y - height }; SetProceduralOrigin(max); drawList->AddRectFilledMultiColor ( { res.x, res.y }, max, COLOR0, COLOR0, COLOR1, COLOR1 ); ResetProceduralOrigin(); SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); DrawTitle(); auto drawLine = [&](bool top) { float y = top ? height : (res.y - height); constexpr uint32_t TOP_COLOR0 = IM_COL32(222, 255, 189, 7); constexpr uint32_t TOP_COLOR1 = IM_COL32(222, 255, 189, 65); constexpr uint32_t BOTTOM_COLOR0 = IM_COL32(173, 255, 156, 65); constexpr uint32_t BOTTOM_COLOR1 = IM_COL32(173, 255, 156, 7); drawList->AddRectFilledMultiColor( { 0.0f, y - Scale(2.0f) }, { res.x, y }, top ? TOP_COLOR0 : BOTTOM_COLOR1, top ? TOP_COLOR0 : BOTTOM_COLOR1, top ? TOP_COLOR1 : BOTTOM_COLOR0, top ? TOP_COLOR1 : BOTTOM_COLOR0); drawList->AddRectFilledMultiColor( { 0.0f, y + Scale(1.0f) }, { res.x, y + Scale(3.0f) }, top ? BOTTOM_COLOR0 : TOP_COLOR1, top ? BOTTOM_COLOR0 : TOP_COLOR1, top ? BOTTOM_COLOR1 : TOP_COLOR0, top ? BOTTOM_COLOR1 : TOP_COLOR0); constexpr uint32_t CENTER_COLOR = IM_COL32(115, 178, 104, 255); drawList->AddRectFilled({ 0.0f, y }, { res.x, y + Scale(1.0f) }, CENTER_COLOR); }; // Top bar line drawLine(true); // Bottom bar line drawLine(false); DrawVersionString(g_newRodinFont); } static float AlignToNextGrid(float value) { return round(value / GRID_SIZE) * GRID_SIZE; } static void DrawContainer(ImVec2 min, ImVec2 max, bool drawRightOutline) { double containerHeight = g_isStage ? 1.0 : ComputeMotion(g_appearTime, 0.0, CONTAINER_LINE_ANIMATION_DURATION); float center = (min.y + max.y) / 2.0f; min.y = Lerp(center, min.y, containerHeight); max.y = Lerp(center, max.y, containerHeight); auto& res = ImGui::GetIO().DisplaySize; auto drawList = ImGui::GetBackgroundDrawList(); double outerAlpha = g_isStage ? 1.0 : ComputeMotion(g_appearTime, CONTAINER_OUTER_TIME, CONTAINER_OUTER_DURATION); double innerAlpha = g_isStage ? 1.0 : ComputeMotion(g_appearTime, CONTAINER_INNER_TIME, CONTAINER_INNER_DURATION); double backgroundAlpha = g_isStage ? 1.0 : ComputeMotion(g_appearTime, CONTAINER_BACKGROUND_TIME, CONTAINER_BACKGROUND_DURATION); const uint32_t lineColor = IM_COL32(0, 89, 0, 255 * containerHeight); const uint32_t outerColor = IM_COL32(0, 49, 0, 255 * outerAlpha); const uint32_t innerColor = IM_COL32(0, 33, 0, 255 * innerAlpha); const uint32_t backgroundColor = IM_COL32(0, 0, 0, 223 * backgroundAlpha); float gridSize = Scale(GRID_SIZE); drawList->AddRectFilled(min, max, backgroundColor); // Background SetShaderModifier(IMGUI_SHADER_MODIFIER_CHECKERBOARD); drawList->AddRectFilled({ min.x, min.y + gridSize }, { min.x + gridSize, max.y - gridSize }, outerColor); // Container outline left drawList->AddRectFilled({ max.x - gridSize, min.y + gridSize }, { max.x, max.y - gridSize }, drawRightOutline ? outerColor : innerColor); // Container outline right drawList->AddRectFilled(min, { max.x, min.y + gridSize }, outerColor); // Container outline top drawList->AddRectFilled({ min.x, max.y - gridSize }, max, outerColor); // Container outline bottom drawList->AddRectFilled({ min.x + gridSize, min.y + gridSize }, { max.x - gridSize, max.y - gridSize }, innerColor); // Inner container SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); float lineSize = Scale(2); // Top line drawList->AddLine({ min.x + gridSize, min.y + gridSize }, { min.x + gridSize, min.y + gridSize * 2.0f }, lineColor, lineSize); // Vertical left drawList->AddLine({ min.x + gridSize, min.y + gridSize }, { max.x - gridSize, min.y + gridSize }, lineColor, lineSize); // Horizontal drawList->AddLine({ max.x - gridSize, min.y + gridSize }, { max.x - gridSize, min.y + gridSize * 2.0f }, lineColor, lineSize); // Vertical right // Bottom line drawList->AddLine({ min.x + gridSize, max.y - gridSize }, { min.x + gridSize, max.y - gridSize * 2.0f }, lineColor, lineSize); // Vertical left drawList->AddLine({ min.x + gridSize, max.y - gridSize }, { max.x - gridSize, max.y - gridSize }, lineColor, lineSize); // Horizontal drawList->AddLine({ max.x - gridSize, max.y - gridSize }, { max.x - gridSize, max.y - gridSize * 2.0f }, lineColor, lineSize); // Vertical right // The draw area drawList->PushClipRect({ min.x + gridSize * 2.0f, min.y + gridSize * 2.0f }, { max.x - gridSize * 2.0f + 1.0f, max.y - gridSize * 2.0f + 1.0f }); } static std::string& GetCategory(int index) { // TODO: Don't use raw numbers here! switch (index) { case 0: return Localise("Options_Category_System"); case 1: return Localise("Options_Category_Input"); case 2: return Localise("Options_Category_Audio"); case 3: return Localise("Options_Category_Video"); } return g_localeMissing; } static void ResetSelection() { g_firstVisibleRowIndex = 0; g_selectedRowIndex = 0; g_prevSelectedRowIndex = 0; g_rowSelectionTime = ImGui::GetTime(); g_prevOffsetRatio = 0.0f; g_leftWasHeld = false; g_upWasHeld = false; g_rightWasHeld = false; g_downWasHeld = false; g_canReset = false; } static bool DrawCategories() { double motion = g_isStage ? 1.0 : ComputeMotion(g_appearTime, CONTAINER_CATEGORY_TIME, CONTAINER_CATEGORY_DURATION); if (motion == 0.0) return false; auto inputState = SWA::CInputState::GetInstance(); bool moveLeft = !g_lockedOnOption && inputState->GetPadState().IsTapped(SWA::eKeyState_LeftBumper); bool moveRight = !g_lockedOnOption && inputState->GetPadState().IsTapped(SWA::eKeyState_RightBumper); if (moveLeft) { --g_categoryIndex; if (g_categoryIndex < 0) g_categoryIndex = g_categoryCount - 1; } else if (moveRight) { ++g_categoryIndex; if (g_categoryIndex >= g_categoryCount) g_categoryIndex = 0; } if (moveLeft || moveRight) { ResetSelection(); Game_PlaySound("sys_actstg_score"); } auto drawList = ImGui::GetBackgroundDrawList(); auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); float gridSize = Scale(GRID_SIZE); float size = Scale(32.0f); ImVec2 textSizes[g_categoryCount]; float clipRectWidth = clipRectMax.x - clipRectMin.x; float textWidthSum = 0.0f; for (size_t i = 0; i < g_categoryCount; i++) { textSizes[i] = g_dfsogeistdFont->CalcTextSizeA(size, FLT_MAX, 0.0f, GetCategory(i).c_str()); textWidthSum += textSizes[i].x; } float textSquashRatio = 1.0f; float maxTextWidthSum = clipRectWidth - (gridSize * 4.0f * (g_categoryCount - 1)); if (textWidthSum > maxTextWidthSum) { textSquashRatio = maxTextWidthSum / textWidthSum; for (auto& textSize : textSizes) textSize.x *= textSquashRatio; textWidthSum = maxTextWidthSum; } float tabHeight = gridSize * 4.0f; float textPadding = (clipRectWidth - textWidthSum) / (g_categoryCount + 1.0f); float xOffset = textPadding; xOffset -= (1.0 - motion) * gridSize * 4.0; ImVec2 textPositions[g_categoryCount]; for (size_t i = 0; i < g_categoryCount; i++) { float tabPadding = std::min(textPadding / 2.0f, gridSize * 3.0f); ImVec2 min = { clipRectMin.x + xOffset - tabPadding, clipRectMin.y }; ImVec2 max = { min.x + textSizes[i].x + tabPadding * 2.0f, min.y + tabHeight}; if (g_categoryIndex == i) { // Animation interrupted by entering/exiting or resizing the options menu if (motion < 1.0 || abs(g_categoryAnimMin.y - min.y) > 0.01f || abs(g_categoryAnimMax.y - max.y) > 0.01f) { g_categoryAnimMin = min; g_categoryAnimMax = max; } else { float animWidth = g_categoryAnimMax.x - g_categoryAnimMin.x; float width = max.x - min.x; float height = max.y - min.y; animWidth = Lerp(animWidth, width, 1.0f - exp(-64.0f * ImGui::GetIO().DeltaTime)); auto center = Lerp(min, max, 0.5f); auto animCenter = Lerp(g_categoryAnimMin, g_categoryAnimMax, 0.5f); auto animatedCenter = Lerp(animCenter, center, 1.0f - exp(-16.0f * ImGui::GetIO().DeltaTime)); float widthHalfExtent = width / 2.0f; float heightHalfExtent = height / 2.0f; g_categoryAnimMin = { animatedCenter.x - widthHalfExtent, animatedCenter.y - heightHalfExtent }; g_categoryAnimMax = { animatedCenter.x + widthHalfExtent, animatedCenter.y + heightHalfExtent }; } SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON); drawList->AddRectFilledMultiColor ( g_categoryAnimMin, g_categoryAnimMax, IM_COL32(0, 130, 0, 223 * motion), IM_COL32(0, 130, 0, 178 * motion), IM_COL32(0, 130, 0, 223 * motion), IM_COL32(0, 130, 0, 178 * motion) ); drawList->AddRectFilledMultiColor ( g_categoryAnimMin, g_categoryAnimMax, IM_COL32(0, 0, 0, 13 * motion), IM_COL32(0, 0, 0, 0), IM_COL32(0, 0, 0, 55 * motion), IM_COL32(0, 0, 0, 6) ); drawList->AddRectFilledMultiColor ( g_categoryAnimMin, g_categoryAnimMax, IM_COL32(0, 130, 0, 13 * motion), IM_COL32(0, 130, 0, 111 * motion), IM_COL32(0, 130, 0, 0), IM_COL32(0, 130, 0, 55 * motion) ); SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); } // Store to draw again later, otherwise the tab background gets drawn on top of text during the animation. textPositions[i] = { clipRectMin.x + xOffset, clipRectMin.y }; xOffset += textSizes[i].x + textPadding; } SetScale({ textSquashRatio, 1.0f }); for (size_t i = 0; i < g_categoryCount; i++) { auto& pos = textPositions[i]; uint8_t alpha = (i == g_categoryIndex ? 235 : 128) * motion; SetOrigin({ pos.x, pos.y }); SetGradient ( pos, { pos.x + textSizes[i].x, pos.y + textSizes[i].y }, IM_COL32(128, 255, 0, alpha), IM_COL32(255, 192, 0, alpha) ); DrawTextWithOutline ( g_dfsogeistdFont, size, pos, IM_COL32_WHITE, GetCategory(i).c_str(), 4, IM_COL32_BLACK, IMGUI_SHADER_MODIFIER_CATEGORY_BEVEL ); } SetScale({ 1.0f, 1.0f }); SetOrigin({ 0.0f, 0.0f }); ResetGradient(); if (g_isStage || (ImGui::GetTime() - g_appearTime) >= (CONTAINER_FULL_DURATION / 60.0)) { drawList->PushClipRect({ clipRectMin.x, clipRectMin.y + gridSize * 6.0f }, { clipRectMax.x - gridSize, clipRectMax.y - gridSize }); return true; } return false; } static void DrawSelectionArrows(ImVec2 min, ImVec2 max, bool isLeftTapped, bool isRightTapped, bool isSlider) { static constexpr double sizeMotionDuration = 16.0; static bool isLeftArrowMotion = false; static bool isRightArrowMotion = false; auto drawList = ImGui::GetBackgroundDrawList(); auto gridSize = Scale(GRID_SIZE); auto width = gridSize * 2.5f; auto padding = gridSize; auto bgMotion = (isLeftArrowMotion || isRightArrowMotion) ? ComputeMotion(g_lastTappedTime, 8.0, sizeMotionDuration) : 0; if (isLeftTapped) { isLeftArrowMotion = true; isRightArrowMotion = false; } if (isRightTapped) { isLeftArrowMotion = false; isRightArrowMotion = true; } if (bgMotion >= 1.0 || isSlider) { isLeftArrowMotion = false; isRightArrowMotion = false; } auto getBgColour = [&](bool isAnim) -> ImU32 { return IM_COL32(0, 97, 0, Lerp(96, 255, isAnim ? bgMotion : 1)); }; auto bgLeftColour = getBgColour(isLeftArrowMotion); auto bgRightColour = getBgColour(isRightArrowMotion); auto invertMotion = isSlider ? ComputeMotion(g_lockedOnTime, 0, VALUE_SLIDER_INTRO_DURATION) : 0; auto invertMotionX = invertMotion > 0.5 ? 1.0 : 0.0; // Arrow side point invert animation auto xAdd = Hermite(0, Scale(10), sin(invertMotion * M_PI)); // Arrow jump animation auto y = (min.y + max.y) / 2.0f; // Left triangle vertices auto leftX = Lerp(min.x - padding, min.x - padding - width, invertMotionX) - xAdd; auto leftV1Y = Hermite(min.y, max.y, invertMotion); auto leftV2Y = Hermite(max.y, min.y, invertMotion); auto leftV3X = Hermite(min.x - padding - width, min.x - padding, invertMotionX) - xAdd; // Right triangle vertices auto rightX = Lerp(max.x + padding, max.x + padding + width, invertMotionX) + xAdd; auto rightV1Y = Hermite(max.y, min.y, invertMotion); auto rightV2Y = Hermite(min.y, max.y, invertMotion); auto rightV3X = Hermite(max.x + padding + width, max.x + padding, invertMotionX) + xAdd; auto drawLeftArrow = [&](ImU32 col) { drawList->AddTriangleFilled({ leftX, leftV1Y }, { leftX, leftV2Y }, { leftV3X, y }, col); }; auto drawRightArrow = [&](ImU32 col) { drawList->AddTriangleFilled({ rightX, rightV1Y }, { rightX, rightV2Y }, { rightV3X, y }, col); }; drawLeftArrow(bgLeftColour); drawRightArrow(bgRightColour); // Additive gradient colours auto c0 = IM_COL32(255, 0, 255, 255); auto c1 = IM_COL32(255, 128, 255, 255); SetAdditive(true); // Apply additive gradients SetGradient({ leftX, leftV1Y }, { leftV3X, y }, c0, c1, c1, c0); drawLeftArrow(bgLeftColour); SetGradient({ rightX, rightV1Y }, { rightV3X, y }, c0, c1, c1, c0); drawRightArrow(bgRightColour); ResetGradient(); if (isSlider) { auto col = IM_COL32(0, 97, 0, 255 * invertMotion); drawLeftArrow(col); drawRightArrow(col); } else { auto fgMotion = ComputeMotion(g_lastTappedTime, 0, sizeMotionDuration); auto fgMotionSine = sin(fgMotion * M_PI); auto fgScale = Lerp(0, Scale(4), fgMotionSine); auto fgColour = IM_COL32(0, 97, 0, 255 * fgMotionSine); if (isLeftArrowMotion) { drawList->AddTriangleFilled ( { min.x - padding, min.y - fgScale }, { min.x - padding, max.y + fgScale }, { min.x - padding - width - (fgScale + Scale(2)), (min.y + max.y) / 2.0f }, fgColour ); } if (isRightArrowMotion) { drawList->AddTriangleFilled ( { max.x + padding, max.y + fgScale }, { max.x + padding, min.y - fgScale }, { max.x + padding + width + (fgScale + Scale(2)), (min.y + max.y) / 2.0f }, fgColour ); } } SetAdditive(false); } template static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef* config, bool isAccessible, std::string* inaccessibleReason = nullptr, T valueMin = T(0), T valueCenter = T(0.5), T valueMax = T(1), bool isSlider = true) { auto drawList = ImGui::GetBackgroundDrawList(); auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); auto& padState = SWA::CInputState::GetInstance()->GetPadState(); constexpr float OPTION_NARROW_GRID_COUNT = 36.0f; constexpr float OPTION_WIDE_GRID_COUNT = 54.0f; constexpr bool IS_SLIDER_TYPE = std::is_same_v || std::is_same_v; auto isValueSlider = IS_SLIDER_TYPE && isSlider; auto gridSize = Scale(GRID_SIZE); auto optionWidth = gridSize * floor(Lerp(OPTION_NARROW_GRID_COUNT, OPTION_WIDE_GRID_COUNT, g_aspectRatioNarrowScale)); auto optionHeight = gridSize * 5.5f; auto optionPadding = gridSize * 0.5f; auto valueWidth = Scale(192.0f); auto valueHeight = gridSize * 3.0f; // Left side ImVec2 min = { clipRectMin.x, clipRectMin.y + (optionHeight + optionPadding) * rowIndex + yOffset }; ImVec2 max = { min.x + optionWidth, min.y + optionHeight }; auto configName = config->GetNameLocalised(Config::Language); auto size = Scale(26.0f); auto textSize = g_seuratFont->CalcTextSizeA(size, FLT_MAX, 0.0f, configName.c_str()); ImVec2 textPos = { min.x + gridSize, min.y + (optionHeight - textSize.y) / 2.0f }; ImVec4 textClipRect = { min.x, min.y, max.x, max.y }; bool lockedOnOption = false; if (g_selectedRowIndex == rowIndex) { g_selectedItem = config; g_inaccessibleReason = isAccessible ? nullptr : inaccessibleReason; if (!g_isEnterKeyBuffered) { if (isAccessible) { if constexpr (std::is_same_v) { if (padState.IsTapped(SWA::eKeyState_A)) { config->Value = !config->Value; if (config->Callback) config->Callback(config); VideoConfigValueChangedCallback(config); Game_PlaySound("sys_worldmap_finaldecide"); } } else { static T s_oldValue; if (padState.IsTapped(SWA::eKeyState_A)) { g_lockedOnOption ^= true; if (g_lockedOnOption) { g_lockedOnTime = ImGui::GetTime(); g_leftWasHeld = false; g_rightWasHeld = false; // remember value s_oldValue = config->Value; if (config->LockCallback) config->LockCallback(config); Game_PlaySound("sys_worldmap_decide"); } else { // released lock, do callbacks if value is different if (config->Value != s_oldValue) { VideoConfigValueChangedCallback(config); if (config->ApplyCallback) config->ApplyCallback(config); } Game_PlaySound("sys_worldmap_finaldecide"); } } else if (padState.IsTapped(SWA::eKeyState_B)) { // released lock, restore old value config->Value = s_oldValue; g_lockedOnOption = false; Game_PlaySound("sys_worldmap_cansel"); } lockedOnOption = g_lockedOnOption; } if (g_canReset && padState.IsTapped(SWA::eKeyState_X)) { if (!config->IsDefaultValue()) { config->MakeDefault(); VideoConfigValueChangedCallback(config); if (config->Callback) config->Callback(config); if (config->ApplyCallback) config->ApplyCallback(config); } Game_PlaySound("sys_worldmap_decide"); } } else { if (padState.IsTapped(SWA::eKeyState_A)) Game_PlaySound("sys_actstg_stateserror"); } } } auto fadedOut = (g_lockedOnOption && g_selectedItem != config) || !isAccessible; auto alpha = fadedOut ? 0.5f : 1.0f; auto textColour = IM_COL32(255, 255, 255, 255 * alpha); if (Config::Language == ELanguage::Japanese) textPos.y += Scale(10.0f); if (g_selectedItem == config) { float prevItemOffset = (g_prevSelectedRowIndex - g_selectedRowIndex) * (optionHeight + optionPadding); double animRatio = std::clamp((ImGui::GetTime() - g_rowSelectionTime) * 60.0 / 8.0, 0.0, 1.0); prevItemOffset *= pow(1.0 - animRatio, 3.0); auto c0 = IM_COL32(0xE2, 0x71, 0x22, isAccessible ? 0x80 : 0x30); auto c1 = IM_COL32(0x92, 0xFF, 0x31, isAccessible ? 0x80 : 0x30); drawList->AddRectFilledMultiColor({ min.x, min.y + prevItemOffset }, { max.x, max.y + prevItemOffset }, c0, c0, c1, c1); DrawTextWithMarquee(g_seuratFont, size, textPos, min, max, textColour, configName.c_str(), g_rowSelectionTime, 0.9, Scale(250.0)); // large g_canReset = g_isControlsVisible && !g_lockedOnOption && g_selectedItem->GetName().find("Language") == std::string::npos && g_selectedItem != &Config::WindowSize && isAccessible; } else { drawList->PushClipRect(min, max, true); DrawRubyAnnotatedText ( g_seuratFont, size, FLT_MAX, textPos, 0.0f, configName.c_str(), [=](const char* str, ImVec2 pos) { DrawTextBasic(g_seuratFont, size, pos, textColour, str); }, [=](const char* str, float annotationSize, ImVec2 pos) { DrawTextBasic(g_seuratFont, annotationSize, pos, textColour, str); } ); drawList->PopClipRect(); } // Right side min = { max.x + (clipRectMax.x - max.x - valueWidth) / 2.0f, min.y + (optionHeight - valueHeight) / 2.0f }; max = { min.x + valueWidth, min.y + valueHeight }; SetShaderModifier(IMGUI_SHADER_MODIFIER_SCANLINE_BUTTON); drawList->AddRectFilledMultiColor(min, max, IM_COL32(0, 130, 0, 223 * alpha), IM_COL32(0, 130, 0, 178 * alpha), IM_COL32(0, 130, 0, 223 * alpha), IM_COL32(0, 130, 0, 178 * alpha)); drawList->AddRectFilledMultiColor(min, max, IM_COL32(0, 0, 0, 13 * alpha), IM_COL32(0, 0, 0, 0), IM_COL32(0, 0, 0, 55 * alpha), IM_COL32(0, 0, 0, 6 * alpha)); drawList->AddRectFilledMultiColor(min, max, IM_COL32(0, 130, 0, 13 * alpha), IM_COL32(0, 130, 0, 111 * alpha), IM_COL32(0, 130, 0, 0), IM_COL32(0, 130, 0, 55 * alpha)); if (isSlider) { if constexpr (std::is_same_v || std::is_same_v) { if (lockedOnOption) { SetAdditive(true); drawList->AddRectFilled(min, max, IM_COL32(192, 192, 0, 96 * ComputeMotion(g_lockedOnTime, 0, VALUE_SLIDER_INTRO_DURATION))); SetAdditive(false); } // Inner container of slider const uint32_t innerColor0 = IM_COL32(0, 65, 0, 255 * alpha); const uint32_t innerColor1 = IM_COL32(0, 32, 0, 255 * alpha); float xPadding = Scale(6); float yPadding = Scale(3); drawList->AddRectFilledMultiColor ( { min.x + xPadding, min.y + yPadding }, { max.x - xPadding, max.y - yPadding }, innerColor0, innerColor0, innerColor1, innerColor1 ); // The actual slider const uint32_t sliderColor0 = IM_COL32(57, 241, 0, 255 * alpha); const uint32_t sliderColor1 = IM_COL32(2, 106, 0, 255 * alpha); xPadding += Scale(2); yPadding += Scale(2); ImVec2 sliderMin = { min.x + xPadding, min.y + yPadding }; ImVec2 sliderMax = { max.x - xPadding, max.y - yPadding }; float factor; if (config->Value <= valueCenter) factor = float(config->Value - valueMin) / (valueCenter - valueMin) * 0.5f; else factor = 0.5f + float(config->Value - valueCenter) / (valueMax - valueCenter) * 0.5f; sliderMax.x = sliderMin.x + (sliderMax.x - sliderMin.x) * factor; drawList->AddRectFilledMultiColor(sliderMin, sliderMax, sliderColor0, sliderColor0, sliderColor1, sliderColor1); } } SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE); if constexpr (std::is_same_v) DrawToggleLight({ min.x + Scale(14), min.y + ((max.y - min.y) - Scale(14)) / 2 + Scale(1) }, config->Value, alpha); // Selection triangles if (lockedOnOption) { bool leftIsHeld = padState.IsDown(SWA::eKeyState_DpadLeft) || padState.LeftStickHorizontal < -0.5f; bool rightIsHeld = padState.IsDown(SWA::eKeyState_DpadRight) || padState.LeftStickHorizontal > 0.5f; bool leftTapped = !g_leftWasHeld && leftIsHeld; bool rightTapped = !g_rightWasHeld && rightIsHeld; double time = ImGui::GetTime(); if (leftTapped || rightTapped) g_lastTappedTime = time; bool decrement = leftTapped; bool increment = rightTapped; g_leftWasHeld = leftIsHeld; g_rightWasHeld = rightIsHeld; DrawSelectionArrows(min, max, leftTapped, rightTapped, isValueSlider); if constexpr (std::is_enum_v) { auto it = config->EnumTemplateReverse.find(config->Value); if (leftTapped) { if (it == config->EnumTemplateReverse.begin()) it = config->EnumTemplateReverse.end(); --it; } else if (rightTapped) { ++it; if (it == config->EnumTemplateReverse.end()) it = config->EnumTemplateReverse.begin(); } config->Value = it->first; config->SnapToNearestAccessibleValue(rightTapped); if (increment || decrement) Game_PlaySound("sys_actstg_pausecursor"); } else if constexpr (std::is_same_v || std::is_same_v) { float deltaTime = std::fmin(ImGui::GetIO().DeltaTime, 1.0f / 15.0f); bool fastIncrement = isSlider && (leftIsHeld || rightIsHeld) && (time - g_lastTappedTime) > 0.5; bool isPlayIncrementSound = true; constexpr double INCREMENT_TIME = 1.0 / 120.0; constexpr double INCREMENT_SOUND_TIME = 1.0 / 7.5; if (fastIncrement) g_fastIncrementHoldTime += deltaTime; else g_fastIncrementHoldTime = 0; if (fastIncrement) { isPlayIncrementSound = (time - g_lastIncrementSoundTime) > INCREMENT_SOUND_TIME; if (g_fastIncrementHoldTime < INCREMENT_TIME) fastIncrement = false; else g_lastIncrementTime = time; } if (fastIncrement) { decrement = leftIsHeld; increment = rightIsHeld; } do { if constexpr (std::is_integral_v) { if (decrement) config->Value -= 1; else if (increment) config->Value += 1; } else { if (decrement) config->Value -= 0.01f; else if (increment) config->Value += 0.01f; } if (fastIncrement) g_fastIncrementHoldTime -= INCREMENT_TIME; } while (fastIncrement && g_fastIncrementHoldTime >= INCREMENT_TIME); bool isConfigValueInBounds = config->Value >= valueMin && config->Value <= valueMax; if ((increment || decrement) && isConfigValueInBounds && isPlayIncrementSound) { g_lastIncrementSoundTime = time; Game_PlaySound("sys_actstg_twn_speechbutton"); } config->Value = std::clamp(config->Value, valueMin, valueMax); } if (!config->ApplyCallback) { if ((increment || decrement) && config->Callback) config->Callback(config); } } std::string valueText; if constexpr (std::is_same_v) { valueText = fmt::format("{}%", int32_t(round(config->Value * 100.0f))); } else if constexpr (std::is_same_v) { if (config == &Config::WindowSize) { if (Config::Fullscreen) { int displayW, displayH; GameWindow::GetSizeInPixels(&displayW, &displayH); valueText = fmt::format("{}x{}", displayW, displayH); } else { auto displayModes = GameWindow::GetDisplayModes(); if (config->Value >= 0 && config->Value < displayModes.size()) { auto& displayMode = displayModes[config->Value]; valueText = fmt::format("{}x{}", displayMode.w, displayMode.h); } else { valueText = fmt::format("{}x{}", GameWindow::s_width, GameWindow::s_height); } } } else if (config == &Config::Monitor) { valueText = fmt::format("{}", config->Value + 1); } else { valueText = fmt::format("{}", config->Value); if (isSlider && config->Value >= valueMax) valueText = Localise("Options_Value_Max"); } } else { valueText = config->GetValueLocalised(Config::Language); } size = Scale(20.0f); textSize = g_newRodinFont->CalcTextSizeA(size, FLT_MAX, 0.0f, valueText.data()); auto textSquashRatio = 1.0f; if (textSize.x > max.x - min.x) textSquashRatio = (max.x - min.x) / textSize.x - 0.1f; auto textX = min.x + ((max.x - min.x) - (textSize.x * textSquashRatio)) / 2.0f; auto textY = min.y + ((max.y - min.y) - textSize.y) / 2.0f; SetGradient ( { textX, textY }, { textX + textSize.x, textY + textSize.y }, IM_COL32(192, 255, 0, 255), IM_COL32(128, 170, 0, 255) ); SetScale({ textSquashRatio, 1.0f }); SetOrigin({ textX, textY }); DrawTextWithOutline ( g_newRodinFont, size, { textX, textY }, IM_COL32(255, 255, 255, 255 * alpha), valueText.data(), 4, IM_COL32(0, 0, 0, 255 * alpha) ); SetScale({ 1.0f, 1.0f }); SetOrigin({ 0.0f, 0.0f }); ResetGradient(); } static void DrawConfigOptions() { auto drawList = ImGui::GetBackgroundDrawList(); auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); drawList->PushClipRect({ clipRectMin.x, clipRectMin.y }, { clipRectMax.x, clipRectMax.y - Scale(5.0f) }); g_selectedItem = nullptr; float gridSize = Scale(GRID_SIZE); float optionHeightWithPadding = gridSize * 6.0f; float yOffset = -g_firstVisibleRowIndex * optionHeightWithPadding; int32_t rowCount = 0; bool isStage = OptionsMenu::s_pauseMenuType == SWA::eMenuType_Stage || OptionsMenu::s_pauseMenuType == SWA::eMenuType_Hub; auto cmnReason = &Localise("Options_Desc_NotAvailable"); // TODO: Don't use raw numbers here! switch (g_categoryIndex) { case 0: // SYSTEM DrawConfigOption(rowCount++, yOffset, &Config::Language, !OptionsMenu::s_isPause, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::VoiceLanguage, OptionsMenu::s_pauseMenuType == SWA::eMenuType_WorldMap, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::Subtitles, true); DrawConfigOption(rowCount++, yOffset, &Config::Hints, !isStage, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::ControlTutorial, !isStage, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::AchievementNotifications, true); DrawConfigOption(rowCount++, yOffset, &Config::TimeOfDayTransition, !Config::UseArrowsForTimeOfDayTransition); break; case 1: // INPUT DrawConfigOption(rowCount++, yOffset, &Config::HorizontalCamera, true); DrawConfigOption(rowCount++, yOffset, &Config::VerticalCamera, true); DrawConfigOption(rowCount++, yOffset, &Config::Vibration, true); DrawConfigOption(rowCount++, yOffset, &Config::AllowBackgroundInput, true); DrawConfigOption(rowCount++, yOffset, &Config::ControllerIcons, true); break; case 2: // AUDIO DrawConfigOption(rowCount++, yOffset, &Config::MasterVolume, true); DrawConfigOption(rowCount++, yOffset, &Config::MusicVolume, true); DrawConfigOption(rowCount++, yOffset, &Config::EffectsVolume, true); DrawConfigOption(rowCount++, yOffset, &Config::ChannelConfiguration, !OptionsMenu::s_isPause, cmnReason); DrawConfigOption(rowCount++, yOffset, &Config::MusicAttenuation, AudioPatches::CanAttenuate(), &Localise("Options_Desc_OSNotSupported")); DrawConfigOption(rowCount++, yOffset, &Config::BattleTheme, true); break; case 3: // VIDEO { DrawConfigOption(rowCount++, yOffset, &Config::WindowSize, !Config::Fullscreen, &Localise("Options_Desc_NotAvailableFullscreen"), 0, 0, (int32_t)GameWindow::GetDisplayModes().size() - 1, false); auto displayCount = GameWindow::GetDisplayCount(); auto canChangeMonitor = Config::Fullscreen && displayCount > 1; auto monitorReason = &Localise("Options_Desc_NotAvailableWindowed"); if (Config::Fullscreen && displayCount <= 1) monitorReason = &Localise("Options_Desc_NotAvailableHardware"); DrawConfigOption(rowCount++, yOffset, &Config::Monitor, canChangeMonitor, monitorReason, 0, 0, displayCount - 1, false); DrawConfigOption(rowCount++, yOffset, &Config::AspectRatio, true); DrawConfigOption(rowCount++, yOffset, &Config::ResolutionScale, true, nullptr, 0.25f, 1.0f, 2.0f); DrawConfigOption(rowCount++, yOffset, &Config::Fullscreen, true); DrawConfigOption(rowCount++, yOffset, &Config::VSync, true); DrawConfigOption(rowCount++, yOffset, &Config::FPS, true, nullptr, FPS_MIN, 120, FPS_MAX); DrawConfigOption(rowCount++, yOffset, &Config::Brightness, true); DrawConfigOption(rowCount++, yOffset, &Config::AntiAliasing, Config::AntiAliasing.InaccessibleValues.size() != 3, &Localise("Options_Desc_NotAvailableHardware")); DrawConfigOption(rowCount++, yOffset, &Config::TransparencyAntiAliasing, Config::AntiAliasing != EAntiAliasing::None, &Localise("Options_Desc_NotAvailableMSAA")); DrawConfigOption(rowCount++, yOffset, &Config::ShadowResolution, true); DrawConfigOption(rowCount++, yOffset, &Config::GITextureFiltering, true); DrawConfigOption(rowCount++, yOffset, &Config::MotionBlur, true); DrawConfigOption(rowCount++, yOffset, &Config::XboxColorCorrection, true); DrawConfigOption(rowCount++, yOffset, &Config::CutsceneAspectRatio, true); DrawConfigOption(rowCount++, yOffset, &Config::UIAlignmentMode, true); break; } } auto inputState = SWA::CInputState::GetInstance(); bool upIsHeld = !g_lockedOnOption && (inputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) || inputState->GetPadState().LeftStickVertical > 0.5f); bool downIsHeld = !g_lockedOnOption && (inputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) || inputState->GetPadState().LeftStickVertical < -0.5f); bool scrollUp = !g_upWasHeld && upIsHeld; bool scrollDown = !g_downWasHeld && downIsHeld; int32_t prevSelectedRowIndex = g_selectedRowIndex; auto time = ImGui::GetTime(); auto fastScroll = (time - g_lastTappedTime) > 0.6; auto fastScrollSpeed = 1.0 / 3.5; auto fastScrollEncounteredEdge = false; static auto fastScrollSpeedUp = false; if (scrollUp || scrollDown) g_lastTappedTime = time; if (!upIsHeld && !downIsHeld) fastScrollSpeedUp = false; if (fastScrollSpeedUp) fastScrollSpeed /= 2; if (fastScroll) { if ((time - g_lastIncrementTime) < fastScrollSpeed) { fastScroll = false; } else { g_lastIncrementTime = time; scrollUp = upIsHeld; scrollDown = downIsHeld; } } if (scrollUp) { --g_selectedRowIndex; if (g_selectedRowIndex < 0) { g_selectedRowIndex = fastScroll ? 0 : rowCount - 1; fastScrollEncounteredEdge = fastScroll; fastScrollSpeedUp = false; } } else if (scrollDown) { ++g_selectedRowIndex; if (g_selectedRowIndex >= rowCount) { g_selectedRowIndex = fastScroll ? rowCount - 1 : 0; fastScrollEncounteredEdge = fastScroll; fastScrollSpeedUp = false; } } if ((scrollUp || scrollDown) && !fastScrollEncounteredEdge) { g_rowSelectionTime = time; g_prevSelectedRowIndex = prevSelectedRowIndex; Game_PlaySound("sys_worldmap_cursor"); } g_upWasHeld = upIsHeld; g_downWasHeld = downIsHeld; int32_t visibleRowCount = int32_t(floor((clipRectMax.y - clipRectMin.y) / optionHeightWithPadding)); bool disableMoveAnimation = false; if (g_firstVisibleRowIndex > g_selectedRowIndex) { g_firstVisibleRowIndex = g_selectedRowIndex; disableMoveAnimation = true; if (g_selectedRowIndex > 0) fastScrollSpeedUp = true; } if (g_firstVisibleRowIndex + visibleRowCount - 1 < g_selectedRowIndex) { g_firstVisibleRowIndex = std::max(0, g_selectedRowIndex - visibleRowCount + 1); disableMoveAnimation = true; if (g_selectedRowIndex < rowCount - 1) fastScrollSpeedUp = true; } if (disableMoveAnimation) g_prevSelectedRowIndex = g_selectedRowIndex; drawList->PopClipRect(); // Pop clip rect from DrawCategories drawList->PopClipRect(); // Draw scroll bar if (rowCount > visibleRowCount) { float totalHeight = (clipRectMax.y - clipRectMin.y) - Scale(2.0f); float heightRatio = float(visibleRowCount) / float(rowCount); float offsetRatio = float(g_firstVisibleRowIndex) / float(rowCount); offsetRatio = Lerp(g_prevOffsetRatio, offsetRatio, 1.0f - exp(-16.0f * ImGui::GetIO().DeltaTime)); g_prevOffsetRatio = offsetRatio; float minY = offsetRatio * totalHeight + clipRectMin.y; drawList->AddRectFilled ( { clipRectMax.x, minY }, { clipRectMax.x + gridSize - Scale(1.0f), minY + totalHeight * heightRatio}, IM_COL32(0, 128, 0, 255) ); } } static void DrawSettingsPanel(ImVec2 settingsMin, ImVec2 settingsMax) { auto drawList = ImGui::GetBackgroundDrawList(); SetProceduralOrigin(settingsMin); DrawContainer(settingsMin, settingsMax, true); if (DrawCategories()) { DrawConfigOptions(); g_isControlsVisible = true; } else { ResetSelection(); } ResetProceduralOrigin(); // Pop clip rect from DrawContainer drawList->PopClipRect(); } static void DrawInfoPanel(ImVec2 infoMin, ImVec2 infoMax) { auto drawList = ImGui::GetBackgroundDrawList(); SetProceduralOrigin(infoMin); DrawContainer(infoMin, infoMax, false); auto clipRectMin = drawList->GetClipRectMin(); auto clipRectMax = drawList->GetClipRectMax(); if (g_selectedItem) { auto desc = g_selectedItem->GetDescription(Config::Language); auto thumbnail = GetThumbnail(g_selectedItem); float aspectRatio = float(thumbnail->width) / thumbnail->height; float thumbnailHeight = (clipRectMax.x - clipRectMin.x) / aspectRatio; ImVec2 thumbnailMin = { clipRectMin.x, clipRectMin.y + Scale(GRID_SIZE / 2.0f) }; ImVec2 thumbnailMax = { clipRectMax.x, thumbnailMin.y + thumbnailHeight }; if (g_isStage) { drawList->AddImage(thumbnail, thumbnailMin, thumbnailMax); } else { float time = g_appearTime + CONTAINER_FULL_DURATION / 60.0; drawList->AddImage( thumbnail, thumbnailMin, thumbnailMax, { 0.0f, 0.0f }, { 1.0f, 1.0f }, IM_COL32(255, 255, 255, 255 * TVStatic::ComputeThumbnailAlpha(time))); TVStatic::Draw( { (thumbnailMin.x + thumbnailMax.x) / 2.0f, (thumbnailMin.y + thumbnailMax.y) / 2.0f }, { (thumbnailMax.x - thumbnailMin.x), (thumbnailMax.y - thumbnailMin.y) }, time); } if (g_inaccessibleReason) { desc += "\n\n" + *g_inaccessibleReason; } else { // Specialised description for resolution scale. if (g_selectedItem == &Config::ResolutionScale) { char buf[100]; auto resScale = round(*(float*)g_selectedItem->GetValue() * 1000) / 1000; std::snprintf(buf, sizeof(buf), desc.c_str(), (int)((float)Video::s_viewportWidth * resScale), (int)((float)Video::s_viewportHeight * resScale)); desc = buf; } const auto& valueDescription = g_selectedItem->GetValueDescription(Config::Language); if (!valueDescription.empty()) desc += "\n\n" + valueDescription; } clipRectMin = { clipRectMin.x, thumbnailMax.y }; auto fontSize = Scale(28.0f); auto annotationFontSize = fontSize * ANNOTATION_FONT_SIZE_MODIFIER; /* Extra padding between the start of the description text and the bottom of the thumbnail. */ auto offsetY = Scale(24.0f); auto textX = clipRectMin.x - Scale(0.5f); auto textY = thumbnailMax.y + offsetY; float lineWidth = clipRectMax.x - clipRectMin.x; if (Config::Language == ELanguage::Japanese) { /* Removing some padding of the applied due to the inclusion of annotation for Japanese. */ textY -= Scale(8.0f); /* The annotation (and thus the Japanese) can be drawn above the edges of the info panel thus the clip needs to be extended a bit. */ clipRectMin.x -= annotationFontSize; clipRectMax.x += annotationFontSize; textY += annotationFontSize; // Dirty hack to disallow clipping on Japanese text // whilst allowing annotations to go over the border lineWidth -= annotationFontSize; } auto textSize = MeasureCentredParagraph(g_seuratFont, fontSize, lineWidth, 5.0f, desc.c_str()); drawList->PushClipRect(clipRectMin, clipRectMax, false); static auto isScrolling = false; static auto isManualScrolling = false; static auto scrollOffset = 0.0f; static auto scrollTimer = 0.0f; static auto scrollDirection = 1.0f; auto scrollMax = textSize.y - (clipRectMax.y - textY); auto scrollSpeed = Scale(50); if (scrollMax > 0.0f) { auto vert = -g_rightStickY; if (fabs(vert) > 0.25f) { isManualScrolling = true; scrollOffset += vert * scrollSpeed * App::s_deltaTime; } else if (isManualScrolling && fabs(vert) <= 0.25f) { isScrolling = false; isManualScrolling = false; scrollTimer = 0.0f; scrollDirection = vert > 0.0f ? 1.0f : -1.0f; } if (!isManualScrolling) { if (!isScrolling) { scrollTimer += App::s_deltaTime; if (scrollTimer >= INFO_TEXT_MARQUEE_DELAY) isScrolling = true; } if (isScrolling) { scrollOffset += scrollSpeed * scrollDirection * App::s_deltaTime; if (scrollOffset >= scrollMax) { isScrolling = false; scrollOffset = scrollMax; scrollTimer = 0.0f; scrollDirection = -1.0f; } else if (scrollOffset <= 0.0f) { isScrolling = false; scrollOffset = 0; scrollTimer = 0.0f; scrollDirection = 1.0f; } } } scrollOffset = std::clamp(scrollOffset, 0.0f, scrollMax); } else { isScrolling = false; scrollOffset = 0.0f; scrollTimer = 0.0f; scrollDirection = 1.0f; } SetVerticalMarqueeFade({ clipRectMin.x, clipRectMin.y + Scale(5.5f) }, clipRectMax, Scale(10), Scale(10)); DrawRubyAnnotatedText ( g_seuratFont, fontSize, lineWidth, { textX, textY - scrollOffset }, 5.0f, desc.c_str(), [=](const char* str, ImVec2 pos) { DrawTextBasic(g_seuratFont, fontSize, pos, IM_COL32_WHITE, str); }, [=](const char* str, float size, ImVec2 pos) { DrawTextBasic(g_seuratFont, size, pos, IM_COL32_WHITE, str); } ); ResetMarqueeFade(); drawList->PopClipRect(); // Reset parameters on new selected row. if (ImGui::GetTime() - g_rowSelectionTime <= 0.0f) { isScrolling = false; scrollOffset = 0.0f; scrollTimer = 0.0f; scrollDirection = 1.0f; } } ResetProceduralOrigin(); // Pop clip rect from DrawContainer drawList->PopClipRect(); } static void SetOptionsMenuVisible(bool isVisible) { OptionsMenu::s_isVisible = isVisible; *SWA::SGlobals::ms_IsRenderHud = !isVisible; } static bool DrawMilesElectric() { auto drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; // Compensate for the lack of CSD UI dimming the background. if (g_isStage) drawList->AddRectFilled({ 0.0f, 0.0f }, res, IM_COL32(0, 0, 0, 127)); auto scaleMotion = ComputeMotion(g_appearTime, 0, MILES_ELECTRIC_SCALE_DURATION); if (scaleMotion >= 1.0) { if (g_isClosing) SetOptionsMenuVisible(false); return true; } auto bgAlphaMotion = ComputeMotion(g_appearTime, 0, MILES_ELECTRIC_BACKGROUND_DURATION); auto bgAlpha = g_isClosing ? Hermite(255, 0, bgAlphaMotion) : Hermite(0, 255, bgAlphaMotion); drawList->AddRectFilled({ 0, 0 }, res, IM_COL32(64, 64, 64, bgAlpha)); auto y = g_isClosing ? Hermite(140, 0, scaleMotion) : Hermite(0, 140, scaleMotion); auto scale = g_isClosing ? Hermite(Scale(1400), Scale(64), scaleMotion) : Hermite(Scale(64), Scale(1400), scaleMotion); ImVec2 centre = { res.x / 2, res.y / 2 - Scale(y) }; auto alpha = g_isClosing ? Hermite(255, 0, scaleMotion) : Hermite(255, 127, scaleMotion); drawList->AddImage ( g_upMilesElectric.get(), { centre.x - scale, centre.y - scale }, { centre.x + scale, centre.y + scale }, { 0, 0 }, { 1, 1 }, IM_COL32(255, 255, 255, alpha) ); return false; } static bool DrawFadeTransition() { auto drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; auto scaleMotion = ComputeMotion(g_appearTime, 0, MILES_ELECTRIC_SCALE_DURATION); auto fgAlphaOutMotion = ComputeMotion(g_appearTime, MILES_ELECTRIC_FOREGROUND_FADE_OUT_TIME, MILES_ELECTRIC_FOREGROUND_FADE_DURATION); if (scaleMotion < 0.8) return false; drawList->AddRectFilled({ 0, 0 }, res, IM_COL32(0, 0, 0, Lerp(255, 0, fgAlphaOutMotion))); return fgAlphaOutMotion >= 1.0; } void OptionsMenu::Init() { auto& io = ImGui::GetIO(); g_seuratFont = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf"); g_dfsogeistdFont = ImFontAtlasSnapshot::GetFont("DFSoGeiStd-W7.otf"); g_newRodinFont = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf"); LoadThumbnails(); g_upMilesElectric = LOAD_ZSTD_TEXTURE(g_miles_electric); TVStatic::Init(); } void OptionsMenu::Draw() { if (!s_isVisible) return; // We've entered the menu now, no need to check this. auto pInputState = SWA::CInputState::GetInstance(); if (pInputState->GetPadState().IsReleased(SWA::eKeyState_A)) g_isEnterKeyBuffered = false; if (g_isStage) { if (!DrawMilesElectric()) return; } if (!g_isClosing) { auto drawList = ImGui::GetBackgroundDrawList(); auto& res = ImGui::GetIO().DisplaySize; if (g_isStage) drawList->AddRectFilled({ 0.0f, 0.0f }, res, IM_COL32(0, 0, 0, 223)); DrawScanlineBars(); float settingsGridCount = floor(Lerp(SETTINGS_NARROW_GRID_COUNT, SETTINGS_WIDE_GRID_COUNT, g_aspectRatioNarrowScale)); float paddingGridCount = Lerp(PADDING_NARROW_GRID_COUNT, PADDING_WIDE_GRID_COUNT, g_aspectRatioNarrowScale); float infoGridCount = floor(Lerp(INFO_NARROW_GRID_COUNT, INFO_WIDE_GRID_COUNT, g_aspectRatioNarrowScale)); float totalGridCount = settingsGridCount + paddingGridCount + infoGridCount; float minX = round(g_aspectRatioOffsetX + Scale((1280.0f - (GRID_SIZE * totalGridCount)) / 2.0f)); float maxX = res.x - minX; float minY = round(g_aspectRatioOffsetY + Scale(CONTAINER_POS_Y)); float maxY = round(g_aspectRatioOffsetY + Scale((720.0f - CONTAINER_POS_Y + 1.0f))); DrawSettingsPanel( { minX, minY }, { minX + Scale(settingsGridCount * GRID_SIZE), maxY } ); DrawInfoPanel( { maxX - Scale(infoGridCount * GRID_SIZE) - 1.0f, minY }, { maxX - 1.0f, maxY } ); if (g_isStage) DrawFadeTransition(); } s_isRestartRequired = Config::Language != App::s_language || Config::ChannelConfiguration != g_currentChannelConfig; } void OptionsMenu::Open(bool isPause, SWA::EMenuType pauseMenuType) { g_isClosing = false; s_isPause = isPause; s_pauseMenuType = pauseMenuType; g_isStage = isPause && pauseMenuType != SWA::eMenuType_WorldMap; g_appearTime = ImGui::GetTime(); g_categoryIndex = 0; g_categoryAnimMin = { 0.0f, 0.0f }; g_categoryAnimMax = { 0.0f, 0.0f }; g_selectedItem = nullptr; g_titleAnimBegin = true; g_currentChannelConfig = Config::ChannelConfiguration; /* Store button state so we can track it later and prevent the first item being selected. */ if (SWA::CInputState::GetInstance()->GetPadState().IsDown(SWA::eKeyState_A)) g_isEnterKeyBuffered = true; ResetSelection(); SetOptionsMenuVisible(true); std::array buttons = { Button("Common_Switch", 115.0f, EButtonIcon::LBRB, EButtonAlignment::Left, &g_isControlsVisible), Button("Common_Reset", 110.0f, EButtonIcon::X, &g_canReset), Button("Common_Select", 115.0f, EButtonIcon::A, &g_isControlsVisible), Button("Common_Back", 65.0f, EButtonIcon::B, &g_isControlsVisible) }; ButtonGuide::Open(buttons); ButtonGuide::SetSideMargins(250); hid::SetProhibitedInputs(XAMINPUT_GAMEPAD_START, false, true); } void OptionsMenu::Close() { if (!g_isClosing) { g_appearTime = ImGui::GetTime(); g_isControlsVisible = false; g_isClosing = true; ButtonGuide::Close(); Config::Save(); hid::SetProhibitedInputs(); } // Skip Miles Electric animation at main menu. if (!g_isStage) SetOptionsMenuVisible(false); } bool OptionsMenu::CanClose() { return !g_lockedOnOption && g_isControlsVisible; }