Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package/SKSE/Plugins/CommunityShaders/Translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 3 additions & 7 deletions src/CSEditor/EditorWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Features/CSEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/Features/PerformanceOverlay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion src/Menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions src/Menu/FeatureListRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "SettingsOverrideManager.h"
#include "State.h"
#include "Util.h"
#include "Utils/UI.h"
#include "WeatherVariableRegistry.h"

namespace
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 4 additions & 9 deletions src/Menu/MenuHeaderRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,11 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector<ActionIcon>& 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) {
Expand All @@ -341,9 +339,6 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector<ActionIcon>& 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();
}
Expand Down Expand Up @@ -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);
}
}
197 changes: 142 additions & 55 deletions src/Utils/UI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include <functional>
#include <iomanip>
#include <mutex>
#include <numbers>
#include <sstream>
#include <stb_image.h>
#include <string>
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<float>;
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;
}

Expand Down
12 changes: 11 additions & 1 deletion src/Utils/UI.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Forward declarations
struct ID3D11Device;
struct ID3D11ShaderResourceView;
struct ImRect;
struct ImVec2;
class Menu;
class Feature;
Expand Down Expand Up @@ -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)
Expand Down
Loading