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
3 changes: 1 addition & 2 deletions src/Menu/SettingsTabRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,7 @@ void SettingsTabRenderer::RenderThemesTab()

if (!isPreset && currentThemeInfo && !currentThemeInfo->filePath.empty()) {
ImGui::SameLine();
auto _style = Util::ErrorButtonStyle();
if (Util::ButtonWithFlash("Delete")) {
if (Util::ErrorButtonWithFlash("Delete")) {
deleteThemePopup.message =
"Are you sure you want to delete the theme '" +
(currentThemeInfo->displayName.empty() ? currentThemePreset : currentThemeInfo->displayName) +
Expand Down
8 changes: 8 additions & 0 deletions src/Menu/ThemeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ class ThemeManager
static constexpr float OVERLAP_FADEIN_SPEED = 8.0f; // Fade-in speed (units/sec)
static constexpr float OVERLAP_FADEOUT_SPEED = 4.0f; // Fade-out speed (units/sec)
static constexpr float OVERLAP_ALPHA_EPSILON = 0.005f; // Below this alpha is clamped to zero

// Status button brightness adjustment offsets. Bright colors are darkened by the same amounts for contrast.
static constexpr float BUTTON_MIN_COLOR_CHANNEL = 0.0f;
static constexpr float BUTTON_MAX_COLOR_CHANNEL = 1.0f;
static constexpr float BUTTON_HOVER_BRIGHTEN = 0.2f;
static constexpr float BUTTON_ACTIVE_BRIGHTEN = 0.3f;
Comment thread
alandtse marked this conversation as resolved.
static constexpr float BUTTON_STATUS_TEXT_HOVER_ALPHA = 0.8f;
static constexpr float BUTTON_STATUS_TEXT_ACTIVE_ALPHA = 1.0f;
};

static ThemeManager* GetSingleton()
Expand Down
86 changes: 80 additions & 6 deletions src/Utils/UI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,90 @@ namespace Util
}
}

StyledButtonWrapper ErrorButtonStyle()
namespace ButtonHelpers
{
constexpr float kHoverBrighten = 0.2f;
constexpr float kActiveBrighten = 0.3f;
auto color = Menu::GetSingleton()->GetTheme().StatusPalette.Error;
auto hover = ImVec4(std::min(color.x + kHoverBrighten, 1.0f), std::min(color.y + kHoverBrighten, 1.0f), std::min(color.z + kHoverBrighten, 1.0f), color.w);
auto active = ImVec4(std::min(color.x + kActiveBrighten, 1.0f), std::min(color.y + kActiveBrighten, 1.0f), std::min(color.z + kActiveBrighten, 1.0f), color.w);
ImVec4 AdjustButtonColor(const ImVec4& color, float amount)
{
const float maxChannel = std::max({ color.x, color.y, color.z });
const float minChannel = ThemeManager::Constants::BUTTON_MIN_COLOR_CHANNEL;
const float maxColorChannel = ThemeManager::Constants::BUTTON_MAX_COLOR_CHANNEL;
const float adjustment = maxChannel <= (maxColorChannel - amount) ? amount : -amount;
return ImVec4(
std::clamp(color.x + adjustment, minChannel, maxColorChannel),
std::clamp(color.y + adjustment, minChannel, maxColorChannel),
std::clamp(color.z + adjustment, minChannel, maxColorChannel),
color.w);
}

ImVec4 WithAlpha(const ImVec4& color, float alpha)
{
return ImVec4(color.x, color.y, color.z, alpha);
}

template <typename StyleFn, typename ButtonFn>
bool InvokeStyledButton(StyleFn styleProvider, ButtonFn buttonCall)
{
auto _style = styleProvider();
return buttonCall();
}
}

StyledButtonWrapper StatusButtonStyle(const ImVec4& color)
{
auto hover = ButtonHelpers::AdjustButtonColor(color, ThemeManager::Constants::BUTTON_HOVER_BRIGHTEN);
auto active = ButtonHelpers::AdjustButtonColor(color, ThemeManager::Constants::BUTTON_ACTIVE_BRIGHTEN);
return StyledButtonWrapper(color, hover, active);
}

StyledButtonWrapper DestructiveButtonStyle()
{
return StatusButtonStyle(Menu::GetSingleton()->GetTheme().StatusPalette.Error);
}

bool ErrorButton(const char* label, const ImVec2& size)
{
return ButtonHelpers::InvokeStyledButton(DestructiveButtonStyle, [&] { return ImGui::Button(label, size); });
}

bool ErrorButtonWithFlash(const char* label, const ImVec2& size, int flashDurationMs)
{
return ButtonHelpers::InvokeStyledButton(DestructiveButtonStyle, [&] { return ButtonWithFlash(label, size, flashDurationMs); });
}

StyledButtonWrapper StatusTextButtonStyle(const ImVec4& color)
{
return StyledButtonWrapper(color,
ButtonHelpers::WithAlpha(color, ThemeManager::Constants::BUTTON_STATUS_TEXT_HOVER_ALPHA),
ButtonHelpers::WithAlpha(color, ThemeManager::Constants::BUTTON_STATUS_TEXT_ACTIVE_ALPHA));
}

StyledButtonWrapper SuccessButtonStyle()
{
return StatusTextButtonStyle(Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor);
}

StyledButtonWrapper WarningButtonStyle()
{
return StatusTextButtonStyle(Menu::GetSingleton()->GetTheme().StatusPalette.Warning);
}

bool SuccessButton(const char* label, const ImVec2& size)
{
return ButtonHelpers::InvokeStyledButton(SuccessButtonStyle, [&] { return ImGui::Button(label, size); });
}

bool WarningButton(const char* label, const ImVec2& size)
{
return ButtonHelpers::InvokeStyledButton(WarningButtonStyle, [&] { return ImGui::Button(label, size); });
}

bool ErrorTextButton(const char* label, const ImVec2& size)
{
return ButtonHelpers::InvokeStyledButton(
[] { return StatusTextButtonStyle(Menu::GetSingleton()->GetTheme().StatusPalette.Error); },
[&] { return ImGui::Button(label, size); });
}

StyledButtonWrapper TransparentIconButtonStyle()
{
constexpr float kHoverAlpha = 0.25f;
Comment thread
alandtse marked this conversation as resolved.
Expand Down
62 changes: 60 additions & 2 deletions src/Utils/UI.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,67 @@ namespace Util
};

/**
* Creates a StyledButtonWrapper using the theme's error color with auto-derived hover/active variants.
* Creates a StyledButtonWrapper using a status color with shared hover/active adjustment.
* Use when a caller needs a custom status color instead of one of the semantic helpers below.
*/
StyledButtonWrapper ErrorButtonStyle();
StyledButtonWrapper StatusButtonStyle(const ImVec4& color);

/**
* Style for destructive or critical actions such as Delete, Clear, Remove, or irreversible confirms.
* Uses the theme error color as the button fill and adjusts hover/active brightness for contrast.
*/
StyledButtonWrapper DestructiveButtonStyle();

/**
* Creates a StyledButtonWrapper using alpha-based hover/active transitions.
* Used for status text buttons where the color itself communicates intent.
* Prefer the named helpers below.
*/
StyledButtonWrapper StatusTextButtonStyle(const ImVec4& color);

/** Style for confirmatory or positive actions such as Apply, Confirm, or Accept. */
StyledButtonWrapper SuccessButtonStyle();

/** Draws a theme success-colored button for confirmatory or positive actions. */
bool SuccessButton(const char* label, const ImVec2& size = ImVec2(0, 0));

/** Style for cautionary or reversible actions such as Revert, Undo, or Reset to saved values. */
StyledButtonWrapper WarningButtonStyle();

/** Draws a theme warning-colored button for cautionary or reversible actions. */
bool WarningButton(const char* label, const ImVec2& size = ImVec2(0, 0));

/**
* Alpha-based error-color button — use in toolbar rows alongside SuccessButton/WarningButton
* for visual consistency. For standalone destructive actions (delete icons, close buttons),
* prefer ErrorButton which uses the brightness-based DestructiveButtonStyle.
*/
bool ErrorTextButton(const char* label, const ImVec2& size = ImVec2(0, 0));

/** Draws a destructive theme error-colored button for delete, clear, remove, or irreversible actions. */
bool ErrorButton(const char* label, const ImVec2& size = ImVec2(0, 0));

/**
* Draws a destructive icon/image button using the theme error color for button chrome.
* Use for destructive image-only controls such as delete icons.
* id must be unique per ImGui element to prevent ID collisions.
*/
template <class TextureID>
bool ErrorImageButton(
const char* id,
TextureID textureId,
const ImVec2& imageSize,
const ImVec2& uv0 = ImVec2(0, 0),
const ImVec2& uv1 = ImVec2(1, 1),
const ImVec4& bgCol = ImVec4(0, 0, 0, 0),
const ImVec4& tintCol = ImVec4(1, 1, 1, 1))
{
auto _style = DestructiveButtonStyle();
return ImGui::ImageButton(id, textureId, imageSize, uv0, uv1, bgCol, tintCol);
}

/** Draws a destructive button with ButtonWithFlash click feedback. */
bool ErrorButtonWithFlash(const char* label, const ImVec2& size = ImVec2(0, 0), int flashDurationMs = 200);

/**
* Creates a transparent button with theme text color hover. Caller must push/pop FrameBorderSize=0 separately.
Expand Down
11 changes: 3 additions & 8 deletions src/WeatherEditor/EditorWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,11 +465,10 @@ void EditorWindow::ShowObjectsWindow()
auto* menu = globals::menu;
if (menu && menu->uiIcons.deleteSettings.texture) {
const float iconSize = ImGui::GetFrameHeight() * 0.85f;
auto _style = Util::ErrorButtonStyle();
ImGui::SetNextItemAllowOverlap();
char idBuf[32];
snprintf(idBuf, sizeof(idBuf), "##jsondel_%s", widget->GetFormID().c_str());
if (ImGui::ImageButton(idBuf, menu->uiIcons.deleteSettings.texture, { iconSize, iconSize })) {
if (Util::ErrorImageButton(idBuf, menu->uiIcons.deleteSettings.texture, { iconSize, iconSize })) {
pendingDeleteWidget = widget;
pendingDeletePopupRequested = true;
}
Expand Down Expand Up @@ -1248,12 +1247,8 @@ void EditorWindow::RenderUI()

// Close button
ImGui::SetCursorScreenPos(ImVec2(xButtonX, cursorY));
{
auto _style = Util::ErrorButtonStyle();
if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) {
open = false;
}
}
if (Util::ErrorButton("X", ImVec2(closeButtonSize, closeButtonSize)))
open = false;
Util::AddTooltip("Close Weather Editor (Esc)");

ImGui::PopClipRect(); // End bottom-border clip rect
Expand Down
25 changes: 11 additions & 14 deletions src/WeatherEditor/InteriorOnlyPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,17 @@ namespace InteriorOnlyPanel

// Delete button
ImGui::SameLine();
{
auto styledButton = Util::ErrorButtonStyle();
if (ImGui::Button("X", ImVec2(C::SCENE_DELETE_BUTTON_WIDTH * scale, 0))) {
if (entry.source == EntrySource::Overwrite) {
pendingDeleteIndex = index;
deleteSingleOverwritePopup.message = std::format(
"Delete overwrite file '{}'?\nThis will permanently remove the file from disk.",
entry.sourceFilename);
deleteSingleOverwritePopup.Request();
} else {
manager->RemoveSetting(kSceneType, index);
ImGui::PopID();
return;
}
if (Util::ErrorButton("X", ImVec2(C::SCENE_DELETE_BUTTON_WIDTH * scale, 0))) {
if (entry.source == EntrySource::Overwrite) {
pendingDeleteIndex = index;
deleteSingleOverwritePopup.message = std::format(
"Delete overwrite file '{}'?\nThis will permanently remove the file from disk.",
entry.sourceFilename);
deleteSingleOverwritePopup.Request();
} else {
manager->RemoveSetting(kSceneType, index);
ImGui::PopID();
return;
}
}
if (auto _tt = Util::HoverTooltipWrapper())
Expand Down
53 changes: 19 additions & 34 deletions src/WeatherEditor/Widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,8 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav

if (HasSavedFile() && menu->uiIcons.deleteSettings.texture) {
ImGui::SameLine();
{
auto _style = Util::ErrorButtonStyle();
if (ImGui::ImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize))
ImGui::OpenPopup("DeleteConfirmation");
}
if (Util::ErrorImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize))
ImGui::OpenPopup("DeleteConfirmation");
Util::AddTooltip("Delete saved file");
}
}
Expand All @@ -391,57 +388,45 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(2);
} else {
const float buttonHeight = ImGui::GetFrameHeight();
if (!menu) {
drawSearchBar();
drawForceWeatherButton();
ImGui::Separator();
return;
}
const auto& palette = menu->GetTheme().StatusPalette;

drawSearchBar();
drawForceWeatherButton();

auto styledTextButton = [&](const char* label, const ImVec4& color, const char* tooltip, auto callback) {
ImGui::SameLine();
ImVec2 size = ImGui::CalcTextSize(label);
size.x += ImGui::GetStyle().FramePadding.x * 2.0f;
size.y = buttonHeight;
auto hover = color;
hover.w = 0.8f;
auto active = color;
active.w = 1.0f;
{
auto styledButton = Util::StyledButtonWrapper(color, hover, active);
if (ImGui::Button(label, size))
callback();
}
Util::AddTooltip(tooltip);
};

auto textButton = [&](const char* label, const char* tooltip, auto callback) {
ImGui::SameLine();
ImVec2 size = ImGui::CalcTextSize(label);
size.x += ImGui::GetStyle().FramePadding.x * 2.0f;
size.y = buttonHeight;
if (Util::ButtonWithFlash(label, size))
if (Util::ButtonWithFlash(label))
callback();
Util::AddTooltip(tooltip);
};

// Apply button
if (showApply && (!editorWindow->settings.autoApplyChanges || RequiresManualApply()))
styledTextButton("Apply", palette.SuccessColor, "Apply changes to the game", [&]() { ApplyChanges(); });
if (showApply && (!editorWindow->settings.autoApplyChanges || RequiresManualApply())) {
ImGui::SameLine();
if (Util::SuccessButton("Apply"))
ApplyChanges();
Util::AddTooltip("Apply changes to the game");
}

// Save/Load/Revert/Delete group
if (showSaveLoadRevert) {
textButton("Save", "Save to file", [&]() { Save(); });
textButton("Load", "Load saved file (or reset to vanilla if no file)", [&]() { Load(); });
styledTextButton("Revert", palette.Warning, "Revert to original game values", [&]() { RevertChanges(); });
ImGui::SameLine();
if (Util::WarningButton("Revert"))
RevertChanges();
Util::AddTooltip("Revert to original game values");

if (HasSavedFile())
styledTextButton("Delete", palette.Error, "Delete saved file", [&]() { ImGui::OpenPopup("DeleteConfirmation"); });
if (HasSavedFile()) {
ImGui::SameLine();
if (Util::ErrorTextButton("Delete"))
ImGui::OpenPopup("DeleteConfirmation");
Util::AddTooltip("Delete saved file");
}
}

drawUnsavedIndicator();
Expand Down
Loading