diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index b8dbab9f52..665ff94814 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1729,6 +1729,7 @@ "menu.features.scene_specific_settings": "Scene Specific Settings", "menu.features.select_feature_left": "Please select a feature from the left.", "menu.features.select_item_left": "Please select an item on the left.", + "menu.features.setting_change_warning_title": "Setting Change Warning", "menu.features.settings_adjusted_warning": "Some of your settings have been automatically adjusted due to feature incompatibilities.", "menu.features.settings_hidden_disabled": "Feature settings are hidden because this feature is disabled at boot.", "menu.features.unloaded_features": "Unloaded Features", diff --git a/src/CSEditor/EditorWindow.cpp b/src/CSEditor/EditorWindow.cpp index b5c5b050d1..fce755896e 100644 --- a/src/CSEditor/EditorWindow.cpp +++ b/src/CSEditor/EditorWindow.cpp @@ -93,16 +93,12 @@ bool IconButton(const char* label, bool filled, const char* iconType) bool result = ImGui::InvisibleButton(label, buttonSize); - bool hovered = ImGui::IsItemHovered(); - bool active = ImGui::IsItemActive(); - - ImU32 bgColor = active ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : - hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : - ImGui::GetColorU32(ImGuiCol_Button); ImU32 iconColor = ImGui::GetColorU32(ImGuiCol_Text); auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(cursorPos, ImVec2(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y), bgColor, ImGui::GetStyle().FrameRounding); + const ImVec2 buttonMax(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y); + drawList->AddRectFilled(cursorPos, buttonMax, ImGui::GetColorU32(ImGuiCol_Button), ImGui::GetStyle().FrameRounding); + Util::DrawCurrentItemRoundedButtonHighlight(drawList); ImVec2 center(cursorPos.x + buttonSize.x * 0.5f, cursorPos.y + buttonSize.y * 0.5f); float iconSize = buttonSize.x * 0.35f; diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index f78b53119a..d6c87c1392 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -392,7 +392,7 @@ void CSEditor::RenderWeatherDetailsWindow(bool* open) } ImGui::SetNextWindowSize(ImVec2(600 * scale, 800 * scale), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Weather Details##Popup", open, ImGuiWindowFlags_None)) { + if (Util::BeginWithRoundedClose("Weather Details##Popup", open, ImGuiWindowFlags_None)) { // Remember window position for next frame ImVec2 currentPos = ImGui::GetWindowPos(); if (currentPos.x != WeatherDetailsWindow.Position.x || currentPos.y != WeatherDetailsWindow.Position.y) { diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 87e1c5a840..cf4c8421a1 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -344,7 +344,7 @@ void PerformanceOverlay::DrawOverlay() } // Create the window - ImGui::Begin(T(TKEY("overlay_title"), "Performance Overlay"), NULL, windowFlags); + Util::BeginWithRoundedClose(T(TKEY("overlay_title"), "Performance Overlay"), nullptr, windowFlags); // Remember window position for next frame if (ImGui::IsWindowAppearing()) { diff --git a/src/Menu.cpp b/src/Menu.cpp index eecdca93da..99e716d600 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -685,7 +685,7 @@ void Menu::DrawSettings() windowFlags |= ImGuiWindowFlags_NoTitleBar; } - ImGui::Begin(title.c_str(), &IsEnabled, windowFlags); + Util::BeginWithRoundedClose(title.c_str(), &IsEnabled, windowFlags); { // Update docking state tracking bool isDocked = ImGui::IsWindowDocked(); diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index e52012bd3c..e1c0fabdef 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -23,6 +23,7 @@ #include "SettingsOverrideManager.h" #include "State.h" #include "Util.h" +#include "Utils/UI.h" #include "WeatherVariableRegistry.h" namespace @@ -942,16 +943,19 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog return; } + constexpr const char* popupId = "###SettingChangeWarning"; + const std::string popupTitle = fmt::format("{}{}", T("menu.features.setting_change_warning_title", "Setting Change Warning"), popupId); + // OpenPopup is idempotent while the popup is already open, so calling it // every frame while the flag is set is safe and ensures we don't miss the // one-frame window where ImGui expects it. - ImGui::OpenPopup("Setting Change Warning"); + ImGui::OpenPopup(popupId); // Center the popup (ImGuiCond_Always matches the Clear Cache dialog pattern) ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - if (ImGui::BeginPopupModal("Setting Change Warning", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + if (Util::BeginPopupModalWithRoundedClose(popupTitle.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::TextWrapped("%s", T("menu.features.settings_adjusted_warning", "Some of your settings have been automatically adjusted due to feature incompatibilities.")); ImGui::Spacing(); ImGui::Separator(); diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 5b48a79131..c7fd4e32e7 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -313,13 +313,11 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector& action ImVec2 iconMax(iconX + iconSize - paddingReduction, iconY + iconSize - paddingReduction); // Use the full area for mouse interaction (including padding) - ImVec2 interactionMin(iconX, iconY); - ImVec2 interactionMax(iconX + iconSize, iconY + iconSize); + ImRect interactionRect({ iconX, iconY }, { iconX + iconSize, iconY + iconSize }); // Check mouse interaction against full area - ImVec2 mousePos = ImGui::GetMousePos(); - bool isHovered = mousePos.x >= interactionMin.x && mousePos.x <= interactionMax.x && - mousePos.y >= interactionMin.y && mousePos.y <= interactionMax.y; + const bool isHovered = ImGui::IsMouseHoveringRect(interactionRect.Min, interactionRect.Max, false); + Util::DrawRoundedButtonHighlight(interactionRect, isHovered, isHovered && ImGui::IsMouseDown(ImGuiMouseButton_Left), fgDrawList); // Only render if texture is valid if (it->texture) { @@ -341,9 +339,6 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector& action // Handle interaction if (isHovered) { - // Draw subtle background for hovered icon using interaction area - fgDrawList->AddRectFilled(interactionMin, interactionMax, IM_COL32(255, 255, 255, 40)); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { it->callback(); } @@ -446,4 +441,4 @@ void MenuHeaderRenderer::RenderWatermarkLogo(const Menu::UIIcons& uiIcons) } drawList->AddImage(uiIcons.logo.texture, logoMin, logoMax, ImVec2(0, 0), ImVec2(1, 1), watermarkColor); -} \ No newline at end of file +} diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index f23dd9b7e6..b60471bd8e 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -126,7 +127,7 @@ namespace Util // measurement frame, causing TextWrapped to wrap at 0px and produce an enormous height. // Setting an initial width gives TextWrapped a sensible wrap column on that frame. ImGui::SetNextWindowSize(ImVec2(400.0f * GetUIScale(), 0.0f), ImGuiCond_Appearing); - isOpen = ImGui::BeginPopupModal(name, p_open, flags | ImGuiWindowFlags_NoSavedSettings); + isOpen = BeginPopupModalWithRoundedClose(name, p_open, flags | ImGuiWindowFlags_NoSavedSettings); } CenteredPopupModal::~CenteredPopupModal() @@ -619,96 +620,182 @@ namespace Util return theme.UseMonochromeIcons ? theme.Palette.Text : ImVec4(1, 1, 1, 1); } + static float GetPillRounding(const ImVec2& min, const ImVec2& max) + { + IM_ASSERT(max.x >= min.x && max.y >= min.y); + return ImMin(max.x - min.x, max.y - min.y) * 0.5f; + } + + static float GetThemedButtonHighlightRounding(const ImVec2& min, const ImVec2& max) + { + const float frameRounding = ImGui::GetStyle().FrameRounding; + IM_ASSERT(frameRounding >= 0.0f); + return ImMin(ImMax(frameRounding, 0.0f), GetPillRounding(min, max)); + } + + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(min, max, hovered, active, GetThemedButtonHighlightRounding(min, max), drawList); + } + + bool DrawRoundedButtonHighlight(const ImRect& rect, bool hovered, bool active, ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(rect.Min, rect.Max, hovered, active, drawList); + } + + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, float rounding, ImDrawList* drawList) + { + if (!hovered && !active) + return false; + + IM_ASSERT(max.x >= min.x && max.y >= min.y); + IM_ASSERT(rounding >= 0.0f); + if (!drawList) + drawList = ImGui::GetWindowDrawList(); + + drawList->AddRectFilled(min, max, ImGui::GetColorU32(active ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), rounding); + return true; + } + + bool DrawCurrentItemRoundedButtonHighlight(ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), ImGui::IsItemHovered(), ImGui::IsItemActive(), drawList); + } + // Shared constants for title-bar button overlays - static constexpr float kButtonPad = 2.0f; // extra padding around hit/highlight area - static constexpr float kCrossDiag = 0.5f * 0.7071f; // half-size * 1/sqrt(2) for cross line endpoints - static constexpr float kCrossInset = 1.0f; // inward inset so cross doesn't touch edges + static constexpr float kTitleBarButtonPadding = 2.0f; + static constexpr float kCloseCrossDiagonalScale = 0.5f / std::numbers::sqrt2_v; + static constexpr float kCloseCrossInset = 1.0f; + static constexpr ImVec4 kTransparentButtonChrome(0, 0, 0, 0); - // Compute the bounding rect for a title-bar button of font-sized square + padding. - static ImRect ButtonBB(const ImVec2& origin, float fontSize) + static ImRect TitleBarButtonRect(const ImVec2& origin, float fontSize) { - const float full = fontSize + kButtonPad * 2.0f; + const float full = fontSize + kTitleBarButtonPadding * 2.0f; return ImRect(origin, ImVec2(origin.x + full, origin.y + full)); } - // Draws a rounded highlight overlay for a title bar button. - static void DrawRoundedButtonHighlight(ImGuiWindow* window, const ImRect& bb, float rounding) + static ImVec2 RightTitleBarButtonOrigin(ImGuiWindow* window, float fontSize, float offset = 0.0f) { - ImGuiContext& g = *ImGui::GetCurrentContext(); - bool isTop = (g.HoveredWindow == window); - bool hovered = isTop && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); - bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); - if (hovered || held) - window->DrawList->AddRectFilled(bb.Min, bb.Max, ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), rounding); + const auto& style = ImGui::GetStyle(); + return ImVec2(window->Rect().Max.x - window->WindowBorderSize - style.FramePadding.x - fontSize - offset - kTitleBarButtonPadding, + window->Rect().Min.y + style.FramePadding.y - kTitleBarButtonPadding); } - // Draws a rounded close button overlay, matching native ImGui CloseButton position. - static void DrawRoundedCloseButton(ImGuiWindow* window, bool* p_open) + static ImVec2 CollapseTitleBarButtonOrigin(ImGuiWindow* window, bool hasCloseButton, float fontSize) { const auto& style = ImGui::GetStyle(); - const float sz = ImGui::GetFontSize(); - const ImVec2 pos(window->Rect().Max.x - window->WindowBorderSize - style.FramePadding.x - sz - kButtonPad, - window->Rect().Min.y + style.FramePadding.y - kButtonPad); - const ImRect bb = ButtonBB(pos, sz); - const float rounding = (sz + kButtonPad * 2.0f) * 0.5f; + IM_ASSERT(style.WindowMenuButtonPosition == ImGuiDir_Left || style.WindowMenuButtonPosition == ImGuiDir_Right); + + if (style.WindowMenuButtonPosition == ImGuiDir_Right) + return RightTitleBarButtonOrigin(window, fontSize, hasCloseButton ? fontSize : 0.0f); + return ImVec2(window->Pos.x + window->WindowBorderSize + style.FramePadding.x - kTitleBarButtonPadding, + window->Pos.y + style.FramePadding.y - kTitleBarButtonPadding); + } + + static bool IsTitleBarButtonHovered(ImGuiWindow* window, const ImRect& bb) + { ImGuiContext& g = *ImGui::GetCurrentContext(); - bool isTop = (g.HoveredWindow == window); - bool hovered = isTop && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); + return g.HoveredWindow == window && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); + } + + class NativeTitleBarButtonHighlightGuard + { + public: + NativeTitleBarButtonHighlightGuard() + { + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kTransparentButtonChrome); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, kTransparentButtonChrome); + } + + ~NativeTitleBarButtonHighlightGuard() { ImGui::PopStyleColor(2); } + }; + + // Draws a rounded close button overlay, matching native ImGui CloseButton position. + static void DrawRoundedCloseHighlight(ImGuiWindow* window) + { + if (window->Flags & ImGuiWindowFlags_NoTitleBar) + return; + + const float sz = ImGui::GetFontSize(); + const ImVec2 pos = RightTitleBarButtonOrigin(window, sz); + const ImRect bb = TitleBarButtonRect(pos, sz); + const bool hovered = IsTitleBarButtonHovered(window, bb); + const bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); window->DrawList->PushClipRect(window->Rect().Min, window->Rect().Max); - DrawRoundedButtonHighlight(window, bb, rounding); - - // Cross lines — matches ImGui's internal RenderCloseButton geometry - const ImVec2 c = bb.GetCenter(); - const float d = sz * kCrossDiag - kCrossInset; - const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); - window->DrawList->AddLine({ c.x - d, c.y - d }, { c.x + d, c.y + d }, col); - window->DrawList->AddLine({ c.x + d, c.y - d }, { c.x - d, c.y + d }, col); - window->DrawList->PopClipRect(); + const bool highlighted = DrawRoundedButtonHighlight(bb, hovered, held, window->DrawList); - if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - *p_open = false; + // Cross lines match ImGui's internal RenderCloseButton geometry. + if (highlighted) { + const ImVec2 c = bb.GetCenter(); + const float d = sz * kCloseCrossDiagonalScale - kCloseCrossInset; + const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); + window->DrawList->AddLine({ c.x - d, c.y - d }, { c.x + d, c.y + d }, col); + window->DrawList->AddLine({ c.x + d, c.y - d }, { c.x - d, c.y + d }, col); + } + window->DrawList->PopClipRect(); } // Draws a rounded highlight for the collapse/triangle button in the title bar. - static void DrawRoundedCollapseHighlight(ImGuiWindow* window) + static void DrawRoundedCollapseHighlight(ImGuiWindow* window, bool hasCloseButton) { + if (window->Flags & ImGuiWindowFlags_NoTitleBar) + return; if (window->Flags & ImGuiWindowFlags_NoCollapse) return; if (ImGui::GetStyle().WindowMenuButtonPosition == ImGuiDir_None) return; - const auto& style = ImGui::GetStyle(); const float sz = ImGui::GetFontSize(); - const ImVec2 pos(window->Pos.x + window->WindowBorderSize + style.FramePadding.x - kButtonPad, - window->Pos.y + style.FramePadding.y - kButtonPad); - const ImRect bb = ButtonBB(pos, sz); - const float rounding = (sz + kButtonPad * 2.0f) * 0.5f; + const ImVec2 pos = CollapseTitleBarButtonOrigin(window, hasCloseButton, sz); + const ImRect bb = TitleBarButtonRect(pos, sz); + const bool hovered = IsTitleBarButtonHovered(window, bb); + const bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); window->DrawList->PushClipRect(window->Rect().Min, window->Rect().Max); - DrawRoundedButtonHighlight(window, bb, rounding); + const bool highlighted = DrawRoundedButtonHighlight(bb, hovered, held, window->DrawList); - // Redraw the triangle arrow on top of the highlight so it stays visible - const ImVec2 arrowPos(pos.x + kButtonPad, pos.y + kButtonPad); - const ImGuiDir dir = window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down; - ImGui::RenderArrow(window->DrawList, arrowPos, ImGui::GetColorU32(ImGuiCol_Text), dir, 1.0f); + if (highlighted) { + const ImVec2 arrowPos(pos.x + kTitleBarButtonPadding, pos.y + kTitleBarButtonPadding); + const ImGuiDir dir = window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down; + ImGui::RenderArrow(window->DrawList, arrowPos, ImGui::GetColorU32(ImGuiCol_Text), dir, 1.0f); + } window->DrawList->PopClipRect(); } + static void DrawRoundedTitleBarButtonHighlights(ImGuiWindow* window, bool hasCloseButton, bool hasCollapseButton) + { + if (!window) + return; + + if (hasCollapseButton) + DrawRoundedCollapseHighlight(window, hasCloseButton); + if (hasCloseButton) + DrawRoundedCloseHighlight(window); + } + bool BeginWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags) { - // Hide native sharp-cornered highlights; we draw rounded ones after Begin() - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0)); - bool visible = ImGui::Begin(name, p_open, flags); - ImGui::PopStyleColor(2); - if (auto* window = ImGui::GetCurrentWindowRead()) { - DrawRoundedCollapseHighlight(window); - if (p_open) - DrawRoundedCloseButton(window, p_open); + bool visible = false; + { + NativeTitleBarButtonHighlightGuard guard; + visible = ImGui::Begin(name, p_open, flags); + } + DrawRoundedTitleBarButtonHighlights(ImGui::GetCurrentWindowRead(), p_open != nullptr, true); + return visible; + } + + bool BeginPopupModalWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags) + { + bool visible = false; + { + NativeTitleBarButtonHighlightGuard guard; + visible = ImGui::BeginPopupModal(name, p_open, flags); } + if (visible) + DrawRoundedTitleBarButtonHighlights(ImGui::GetCurrentWindowRead(), p_open != nullptr, false); return visible; } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 5848aa470a..e81bad9c23 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -15,6 +15,7 @@ // Forward declarations struct ID3D11Device; struct ID3D11ShaderResourceView; +struct ImRect; struct ImVec2; class Menu; class Feature; @@ -319,8 +320,17 @@ namespace Util /** Returns theme text color if monochrome icons enabled, otherwise white. */ ImVec4 GetIconTint(); - /// ImGui::Begin() wrapper that replaces the native close button with a rounded one. + /// Draws a theme-rounded hover/active fill over a button rect. + bool DrawRoundedButtonHighlight(const ImRect& rect, bool hovered, bool active, ImDrawList* drawList = nullptr); + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, ImDrawList* drawList = nullptr); + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, float rounding, ImDrawList* drawList); + + /// Draws the rounded hover/active fill for the last submitted item. + bool DrawCurrentItemRoundedButtonHighlight(ImDrawList* drawList = nullptr); + + /// ImGui::Begin() wrappers that replace native title-bar button highlights with rounded ones. bool BeginWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags = 0); + bool BeginPopupModalWithRoundedClose(const char* name, bool* p_open = nullptr, ImGuiWindowFlags flags = 0); /** * Button with simple flash feedback (matches action icon hover effect style)