From ab93519d69210b9b570edf36f038bcba74417d97 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 22 Sep 2025 22:05:17 +1000 Subject: [PATCH 01/29] v1 --- src/Menu.cpp | 12 +++++++ src/Menu.h | 3 ++ src/Menu/MenuHeaderRenderer.cpp | 6 +++- src/State.cpp | 57 +++++++++++++++++++++++++++++++++ src/State.h | 7 +++- src/Utils/FileSystem.cpp | 25 +++++++++++++++ src/Utils/FileSystem.h | 6 ++++ src/XSEPlugin.cpp | 1 + 8 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index e05cb2f342..6791de169a 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -153,6 +153,18 @@ void Menu::Save(json& o_json) o_json = settings; } +void Menu::LoadTheme(json& o_json) +{ + if (o_json["Theme"].is_object()) { + settings.Theme = o_json["Theme"]; + } +} + +void Menu::SaveTheme(json& o_json) +{ + o_json["Theme"] = settings.Theme; +} + void Menu::Init() { // Setup Dear ImGui context diff --git a/src/Menu.h b/src/Menu.h index 2715a5383d..bc46d28f80 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -30,6 +30,9 @@ class Menu void Load(json& o_json); void Save(json& o_json); + void LoadTheme(json& o_json); + void SaveTheme(json& o_json); + void Init(); void DrawSettings(); diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 75b07215a5..466862e426 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -92,6 +92,7 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow ImGui::TableNextColumn(); if (ImGui::Button("Save Settings", { -1, 0 })) { globals::state->Save(); + globals::state->SaveTheme(); } // Restore Saved Settings Button @@ -174,7 +175,10 @@ std::vector MenuHeaderRenderer::BuildActionIcons if (uiIcons.saveSettings.texture) { actionIcons.push_back({ uiIcons.saveSettings.texture, "Save Settings", - []() { globals::state->Save(); } }); + []() { + globals::state->Save(); + globals::state->SaveTheme(); + } }); } if (uiIcons.loadSettings.texture) { actionIcons.push_back({ uiIcons.loadSettings.texture, diff --git a/src/State.cpp b/src/State.cpp index b5c5bfd612..d92b3d7da5 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -158,6 +158,8 @@ static const std::string& GetConfigPath(State::ConfigMode a_configMode) return globals::state->userConfigPath; case State::ConfigMode::TEST: return globals::state->testConfigPath; + case State::ConfigMode::THEME: + return globals::state->themeConfigPath; case State::ConfigMode::DEFAULT: default: return globals::state->defaultConfigPath; @@ -827,3 +829,58 @@ float State::GetTotalSmoothedDrawCalls() const { return static_cast(smoothDrawCalls[magic_enum::enum_integer(RE::BSShader::Type::Total)]); } + +void State::LoadTheme() +{ + auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); + + if (!std::filesystem::exists(themeConfigPath)) { + logger::info("No theme config file found at: {}", themeConfigPath.string()); + return; + } + + try { + std::ifstream themeFile(themeConfigPath); + if (!themeFile.is_open()) { + logger::warn("Unable to open theme config file: {}", themeConfigPath.string()); + return; + } + + json themeSettings; + themeFile >> themeSettings; + themeFile.close(); + + if (themeSettings["Menu"].is_object()) { + logger::info("Loading theme settings from: {}", themeConfigPath.string()); + globals::menu->LoadTheme(themeSettings["Menu"]); + } + } catch (const std::exception& e) { + logger::warn("Error loading theme config file: {}", e.what()); + } +} + +void State::SaveTheme() +{ + auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); + + try { + std::filesystem::create_directories(themeConfigPath.parent_path()); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating directory during SaveTheme: {}", e.what()); + return; + } + + std::ofstream themeFile(themeConfigPath); + if (!themeFile.is_open()) { + logger::warn("Failed to open theme config file for saving: {}", themeConfigPath.string()); + return; + } + + json themeSettings; + globals::menu->SaveTheme(themeSettings["Menu"]); + + themeFile << std::setw(4) << themeSettings << std::endl; + themeFile.close(); + + logger::info("Theme settings saved to: {}", themeConfigPath.string()); +} diff --git a/src/State.h b/src/State.h index ce49517510..3d1feffa04 100644 --- a/src/State.h +++ b/src/State.h @@ -55,6 +55,7 @@ class State const std::string testConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsTest.json"; const std::string userConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsUser.json"; const std::string defaultConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsDefault.json"; + const std::string themeConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsTheme.json"; float timer = 0; double smoothDrawCalls[RE::BSShader::Type::Total + 1]; @@ -73,7 +74,8 @@ class State { DEFAULT, USER, - TEST + TEST, + THEME }; void Draw(); @@ -84,6 +86,9 @@ class State void Load(ConfigMode a_configMode = ConfigMode::USER, bool a_allowReload = true); void Save(ConfigMode a_configMode = ConfigMode::USER); + void LoadTheme(); + void SaveTheme(); + bool ValidateCache(CSimpleIniA& a_ini); void WriteDiskCacheInfo(CSimpleIniA& a_ini); diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index 8db3322b15..470d1eb57c 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -73,6 +73,11 @@ namespace Util return GetCommunityShaderPath() / "SettingsDefault.json"; } + std::filesystem::path GetSettingsThemePath() + { + return GetCommunityShaderPath() / "SettingsTheme.json"; + } + std::filesystem::path GetOverridesPath() { return GetCommunityShaderPath() / "Overrides"; @@ -179,6 +184,26 @@ namespace Util } } +std::vector> Util::EnumerateDllVersions(const std::filesystem::path& dir) +{ + std::vector> result; + try { + for (const auto& entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file() && entry.path().extension() == L".dll") { + const auto& path = entry.path(); + auto version = Util::GetDllVersion(path.c_str()); + auto name = path.filename().string(); + std::string versionStr = version ? Util::GetFormattedVersion(*version) : "Unknown"; + result.emplace_back(name, versionStr); + } + } + } catch (const std::filesystem::filesystem_error& e) { + // Log error but return empty vector to avoid crashing + logger::warn("Failed to enumerate DLL versions in {}: {}", dir.string(), e.what()); + } + return result; +} + std::vector Util::FileSystem::LoadJsonDiff(const std::filesystem::path& userPath, const std::filesystem::path& testPath) { std::vector diffEntries; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index d9635307b8..3fce942eb9 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -86,6 +86,12 @@ namespace Util */ std::filesystem::path GetSettingsDefaultPath(); + /** + * Gets the SettingsTheme.json file path + * @return CommunityShaderPath / "SettingsTheme.json" + */ + std::filesystem::path GetSettingsThemePath(); + /** * Gets the Overrides directory path * @return CommunityShaderPath / "Overrides" diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 522c58a167..d7a1e755e3 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -161,6 +161,7 @@ bool Load() auto state = globals::state; state->Load(); + state->LoadTheme(); // Load theme settings from SettingsTheme.json auto log = spdlog::default_logger(); log->set_level(state->GetLogLevel()); From 79d233e893d9294411cf6b8c97a8dcfd5d15f6b4 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 22 Sep 2025 22:31:24 +1000 Subject: [PATCH 02/29] Theme multi choosing --- .../CommunityShaders/Themes/Amber.json | 14 + .../CommunityShaders/Themes/Default.json | 14 + .../CommunityShaders/Themes/DragonBlood.json | 14 + .../CommunityShaders/Themes/DwemerBronze.json | 14 + .../CommunityShaders/Themes/Forest.json | 14 + .../CommunityShaders/Themes/HighContrast.json | 14 + .../CommunityShaders/Themes/Light.json | 14 + .../CommunityShaders/Themes/Mystic.json | 14 + .../CommunityShaders/Themes/NordicFrost.json | 14 + .../CommunityShaders/Themes/Ocean.json | 14 + .../Plugins/CommunityShaders/Themes/README.md | 105 +++++ src/Menu.cpp | 47 ++- src/Menu.h | 6 + src/Menu/SettingsTabRenderer.cpp | 73 ++++ src/SettingsOverrideManager.cpp | 5 +- src/SettingsOverrideManager.h | 2 - src/State.cpp | 15 +- src/State.h | 5 - src/ThemeManager.cpp | 384 ++++++++++++++++++ src/ThemeManager.h | 123 ++++++ src/Utils/FileSystem.cpp | 5 + src/Utils/FileSystem.h | 6 + src/XSEPlugin.cpp | 7 + 23 files changed, 906 insertions(+), 17 deletions(-) create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Amber.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Default.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Forest.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Light.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/README.md create mode 100644 src/ThemeManager.cpp create mode 100644 src/ThemeManager.h diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json new file mode 100644 index 0000000000..d6db1c5513 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Warm Amber", + "Description": "Cozy amber tones reminiscent of hearth fires and candlelight", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.2, 0.15, 0.05, 0.9], + "Text": [1.0, 0.9, 0.7, 1.0], + "Border": [0.8, 0.6, 0.3, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json new file mode 100644 index 0000000000..99bd23c966 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Default Dark", + "Description": "The classic Community Shaders dark theme", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.09, 0.09, 0.09, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.5, 0.5, 0.5, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json new file mode 100644 index 0000000000..2050381f75 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Dragon Blood", + "Description": "Dark red theme inspired by dragon lore and ancient power", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.25, 0.05, 0.05, 0.9], + "Text": [1.0, 0.85, 0.85, 1.0], + "Border": [0.8, 0.3, 0.3, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json new file mode 100644 index 0000000000..98cbe608dd --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Dwemer Bronze", + "Description": "Ancient bronze theme inspired by lost Dwemer technology", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.15, 0.12, 0.08, 0.9], + "Text": [0.9, 0.75, 0.5, 1.0], + "Border": [0.7, 0.5, 0.3, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json new file mode 100644 index 0000000000..f6d46bbf6c --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Forest Green", + "Description": "Natural green theme inspired by Skyrim's ancient forests", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.3, 0.15, 0.9], + "Text": [0.9, 1.0, 0.9, 1.0], + "Border": [0.4, 0.7, 0.4, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json new file mode 100644 index 0000000000..842050e2e1 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "High Contrast", + "Description": "High contrast theme for improved accessibility and visibility", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.0, 0.0, 0.0, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [1.0, 1.0, 1.0, 0.9] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json new file mode 100644 index 0000000000..9c2debfacd --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Light Mode", + "Description": "Clean light theme with dark text for daytime use", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.9, 0.9, 0.9, 0.95], + "Text": [0.1, 0.1, 0.1, 1.0], + "Border": [0.3, 0.3, 0.3, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json new file mode 100644 index 0000000000..7286f71523 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Mystic Purple", + "Description": "Magical purple theme with mystical vibes for spell crafting", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.2, 0.1, 0.3, 0.9], + "Text": [0.95, 0.9, 1.0, 1.0], + "Border": [0.6, 0.4, 0.8, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json new file mode 100644 index 0000000000..84c53acc56 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Nordic Frost", + "Description": "Cool blue-white theme reflecting the harsh Nordic climate", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.05, 0.15, 0.25, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "Border": [0.6, 0.8, 1.0, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json new file mode 100644 index 0000000000..dbeeb6d541 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -0,0 +1,14 @@ +{ + "DisplayName": "Ocean Blue", + "Description": "Cool blue tones inspired by deep ocean waters", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.2, 0.4, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "Border": [0.3, 0.5, 0.8, 0.8] + } + } +} \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/README.md b/package/SKSE/Plugins/CommunityShaders/Themes/README.md new file mode 100644 index 0000000000..05cdb8e706 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Themes/README.md @@ -0,0 +1,105 @@ +# Community Shaders - Hot-Swappable Theme System + +This directory contains JSON theme files that can be hot-swapped at runtime without requiring code changes or restarts. + +## How It Works + +The theme system automatically discovers `.json` files in this directory and makes them available in the Community Shaders menu. Simply: + +1. Create or edit a `.json` theme file in this directory +2. Click "Refresh Themes" in the Colors tab of the Community Shaders menu +3. Select your theme from the dropdown + +## Theme File Format + +Theme files use JSON format and should follow this structure: + +```json +{ + "DisplayName": "My Custom Theme", + "Description": "A beautiful custom theme", + "Version": "1.0.0", + "Author": "Your Name", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.1, 0.1, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.5, 0.5, 0.5, 0.8] + } + } +} +``` + +### Required Fields + +- `Theme`: The main theme object containing all visual settings +- `Theme.UseSimplePalette`: Set to `true` for simple 3-color themes, `false` for full ImGui color palette control + +### Optional Metadata + +- `DisplayName`: Human-readable name shown in the dropdown (defaults to filename) +- `Description`: Brief description shown in the UI +- `Version`: Theme version number +- `Author`: Theme creator name + +### Color Format + +Colors are specified as arrays of 4 floating-point values: `[red, green, blue, alpha]` +- Values range from 0.0 to 1.0 +- Alpha (transparency) typically ranges from 0.8 to 1.0 for UI elements + +## Simple vs Full Palette + +### Simple Palette (`UseSimplePalette: true`) + +Uses only 3 colors for a clean, consistent look: +- `Background`: Main UI background color +- `Text`: Primary text color +- `Border`: Border and accent color + +### Full Palette (`UseSimplePalette: false`) + +Allows complete control over all ImGui colors. See existing theme files for examples of the full color array structure. + +## Tips for Creating Themes + +1. **Start Simple**: Begin with `UseSimplePalette: true` and the 3-color system +2. **Test Contrast**: Ensure good readability between text and background colors +3. **Consider Alpha**: Use appropriate transparency for backgrounds (0.9-0.95 recommended) +4. **Backup Originals**: Keep copies of default themes before modifying +5. **Use Descriptive Names**: Choose clear, descriptive filenames and display names + +## Example Themes Included + +- **Default**: Classic dark theme +- **Light**: Clean light mode for daytime use +- **Ocean**: Cool blue oceanic tones +- **Forest**: Natural green forest theme +- **Mystic**: Purple magical theme +- **Amber**: Warm candlelight theme +- **HighContrast**: Accessibility-focused high contrast +- **DragonBlood**: Dark red dragon-inspired theme +- **NordicFrost**: Cool Nordic blue-white theme +- **DwemerBronze**: Ancient bronze Dwemer technology theme + +## Hot-Swapping Features + +- **Runtime Discovery**: New themes are discovered immediately with "Refresh Themes" +- **No Restart Required**: Themes apply instantly when selected +- **Live Editing**: Edit theme files and refresh to see changes immediately +- **Fallback Safety**: Invalid themes are safely ignored +- **File Size Limits**: Theme files are limited to 1MB for performance +- **Error Handling**: Malformed JSON files are logged but don't crash the system + +## Sharing Themes + +Theme files are completely portable and can be shared between users. Simply copy `.json` files to this directory and refresh to make them available. + +## Technical Notes + +- Theme discovery is performed on-demand for performance +- Files are validated for basic JSON structure and required fields +- Theme loading uses the same robust error handling as the settings override system +- Maximum of 100 theme files can be loaded (prevent performance issues) +- File modification times are tracked for change detection \ No newline at end of file diff --git a/src/Menu.cpp b/src/Menu.cpp index 6791de169a..d8456d0c8f 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -3,12 +3,19 @@ #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 #endif +#include #include +#include #include +#include +#include #include #include #include #include +#include +#include +#include #include "Deferred.h" #include "Feature.h" @@ -24,8 +31,10 @@ #include "Menu/ThemeManager.h" #include "ShaderCache.h" #include "State.h" +#include "ThemeManager.h" #include "TruePBR.h" #include "Util.h" +#include "Util.h" #include "Utils/UI.h" #include "Features/PerformanceOverlay.h" @@ -112,7 +121,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( SkipCompilationKey, EffectToggleKey, OverlayToggleKey, - Theme) + Theme, + SelectedThemePreset) bool IsEnabled = false; std::unordered_map Menu::categoryCounts; @@ -165,6 +175,41 @@ void Menu::SaveTheme(json& o_json) o_json["Theme"] = settings.Theme; } +std::vector Menu::DiscoverThemes() +{ + auto themeManager = ThemeManager::GetSingleton(); + return themeManager->GetThemeNames(); +} + +bool Menu::LoadThemePreset(const std::string& themeName) +{ + if (themeName.empty()) { + // Empty theme name means custom/user theme + settings.SelectedThemePreset = ""; + return true; + } + + auto themeManager = ThemeManager::GetSingleton(); + json themeSettings; + + if (themeManager->LoadTheme(themeName, themeSettings)) { + settings.Theme = themeSettings; + settings.SelectedThemePreset = themeName; + logger::info("Loaded theme preset: {}", themeName); + return true; + } else { + logger::warn("Failed to load theme preset: {}", themeName); + return false; + } +} + +void Menu::CreateDefaultThemes() +{ + // Use ThemeManager to create default theme files + auto themeManager = ThemeManager::GetSingleton(); + themeManager->CreateDefaultThemeFiles(); +} + void Menu::Init() { // Setup Dear ImGui context diff --git a/src/Menu.h b/src/Menu.h index bc46d28f80..fa3c0a6cd8 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -33,6 +33,11 @@ class Menu void LoadTheme(json& o_json); void SaveTheme(json& o_json); + // Multi-theme support (deprecated - use ThemeManager) + std::vector DiscoverThemes(); + bool LoadThemePreset(const std::string& themeName); + void CreateDefaultThemes(); // Deprecated - creates JSON files instead + void Init(); void DrawSettings(); @@ -212,6 +217,7 @@ class Menu uint32_t EffectToggleKey = VK_MULTIPLY; // toggle all effects uint32_t OverlayToggleKey = VK_F10; // Global overlay toggle key for all overlays ThemeSettings Theme; + std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; const ThemeSettings& GetTheme() const { return settings.Theme; } // Provide read-only access to the Theme. Settings& GetSettings() { return settings; } // Provide access to settings for other components diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 9dc95ff51e..77a75bd9a7 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1,11 +1,13 @@ #include "SettingsTabRenderer.h" +#include #include #include #include "Globals.h" #include "Menu.h" #include "ShaderCache.h" +#include "ThemeManager.h" #include "Util.h" void SettingsTabRenderer::RenderGeneralSettings( @@ -242,6 +244,77 @@ void SettingsTabRenderer::RenderColorsTab() auto& themeSettings = globals::menu->GetSettings().Theme; auto& colors = themeSettings.FullPalette; + // Theme Preset Selection + ImGui::SeparatorText("Theme Preset"); + + // Get theme manager + auto themeManager = ThemeManager::GetSingleton(); + + // Get available themes (force discovery if not done) + if (!themeManager->IsDiscovered()) { + themeManager->DiscoverThemes(); + } + + const auto& themes = themeManager->GetThemes(); + + // Create dropdown items + std::vector items; + std::vector displayNames; + items.push_back("Custom"); // First item for custom theme + displayNames.push_back("Custom"); + + for (const auto& theme : themes) { + displayNames.push_back(theme.displayName); + items.push_back(displayNames.back().c_str()); + } + + // Find current selection index + int currentItem = 0; // Default to "Custom" + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + for (size_t i = 0; i < themes.size(); ++i) { + if (themes[i].name == globals::menu->GetSettings().SelectedThemePreset) { + currentItem = static_cast(i) + 1; // +1 for "Custom" + break; + } + } + } + + // Theme preset dropdown + if (ImGui::Combo("##ThemePreset", ¤tItem, items.data(), static_cast(items.size()))) { + if (currentItem == 0) { + // Custom theme selected + globals::menu->GetSettings().SelectedThemePreset = ""; + } else { + // Preset theme selected + std::string selectedTheme = themes[currentItem - 1].name; // -1 for "Custom" offset + if (globals::menu->LoadThemePreset(selectedTheme)) { + // Theme loaded successfully, update UI + themeSettings = globals::menu->GetSettings().Theme; + } + } + } + + ImGui::SameLine(); + if (ImGui::Button("Refresh Themes")) { + themeManager->RefreshThemes(); + // Reset selection if current theme no longer exists + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); + if (!themeInfo) { + globals::menu->GetSettings().SelectedThemePreset = ""; + } + } + } + + // Show theme description if available + if (currentItem > 0 && currentItem - 1 < static_cast(themes.size())) { + const auto& selectedTheme = themes[currentItem - 1]; + if (!selectedTheme.description.empty()) { + ImGui::SameLine(); + ImGui::Text("- %s", selectedTheme.description.c_str()); + } + } + ImGui::SeparatorText("Status"); ImGui::ColorEdit4("Disabled Text", (float*)&themeSettings.StatusPalette.Disable); diff --git a/src/SettingsOverrideManager.cpp b/src/SettingsOverrideManager.cpp index f8e6a68932..146f63f61b 100644 --- a/src/SettingsOverrideManager.cpp +++ b/src/SettingsOverrideManager.cpp @@ -1,6 +1,7 @@ #include "SettingsOverrideManager.h" #include "FeatureIssues.h" +#include "Utils/FileSystem.h" #include #include @@ -279,7 +280,7 @@ void SettingsOverrideManager::RefreshOverrides() std::filesystem::path SettingsOverrideManager::GetOverridesDirectory() const { - return std::filesystem::path(OVERRIDES_DIR); + return Util::PathHelpers::GetOverridesPath(); } json SettingsOverrideManager::LoadAppliedOverridesTracking() const @@ -403,7 +404,7 @@ void SettingsOverrideManager::SaveAppliedOverridesTracking(const json& appliedOv std::filesystem::path SettingsOverrideManager::GetAppliedOverridesTrackingPath() const { - return std::filesystem::path(APPLIED_OVERRIDES_TRACKING_FILE); + return Util::PathHelpers::GetAppliedOverridesPath(); } size_t SettingsOverrideManager::ApplyNewOverrides(json& baseSettings, json& appliedOverrides) diff --git a/src/SettingsOverrideManager.h b/src/SettingsOverrideManager.h index c6b08431af..91ab11da6b 100644 --- a/src/SettingsOverrideManager.h +++ b/src/SettingsOverrideManager.h @@ -220,9 +220,7 @@ class SettingsOverrideManager bool enabled = true; bool discovered = false; - static constexpr const char* OVERRIDES_DIR = "Data\\SKSE\\Plugins\\CommunityShaders\\Overrides"; static constexpr const char* GLOBAL_SUFFIX = "_Global.json"; - static constexpr const char* APPLIED_OVERRIDES_TRACKING_FILE = "Data\\SKSE\\Plugins\\CommunityShaders\\AppliedOverrides.json"; // Security limits for JSON validation static constexpr size_t MAX_JSON_DEPTH = 10; diff --git a/src/State.cpp b/src/State.cpp index d92b3d7da5..284d2e8fe8 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -15,6 +15,7 @@ #include "SettingsOverrideManager.h" #include "ShaderCache.h" #include "TruePBR.h" +#include "Utils/FileSystem.h" void State::Draw() { @@ -151,18 +152,18 @@ void State::Setup() globals::deferred->SetupResources(); } -static const std::string& GetConfigPath(State::ConfigMode a_configMode) +static std::string GetConfigPath(State::ConfigMode a_configMode) { switch (a_configMode) { case State::ConfigMode::USER: - return globals::state->userConfigPath; + return Util::PathHelpers::GetSettingsUserPath().string(); case State::ConfigMode::TEST: - return globals::state->testConfigPath; + return Util::PathHelpers::GetSettingsTestPath().string(); case State::ConfigMode::THEME: - return globals::state->themeConfigPath; + return Util::PathHelpers::GetSettingsThemePath().string(); case State::ConfigMode::DEFAULT: default: - return globals::state->defaultConfigPath; + return Util::PathHelpers::GetSettingsDefaultPath().string(); } } @@ -398,9 +399,9 @@ void State::Save(ConfigMode a_configMode) std::ofstream o{ configPath }; try { - std::filesystem::create_directories(folderPath); + std::filesystem::create_directories(Util::PathHelpers::GetCommunityShaderPath()); } catch (const std::filesystem::filesystem_error& e) { - logger::warn("Error creating directory during Save ({}) : {}\n", folderPath, e.what()); + logger::warn("Error creating directory during Save ({}) : {}\n", Util::PathHelpers::GetCommunityShaderPath().string(), e.what()); return; } diff --git a/src/State.h b/src/State.h index 3d1feffa04..1f04f773de 100644 --- a/src/State.h +++ b/src/State.h @@ -51,11 +51,6 @@ class State spdlog::level::level_enum logLevel = spdlog::level::info; std::string shaderDefinesString = ""; std::vector> shaderDefines{}; // data structure to parse string into; needed to avoid dangling pointers - const std::string folderPath = "Data\\SKSE\\Plugins\\CommunityShaders"; - const std::string testConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsTest.json"; - const std::string userConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsUser.json"; - const std::string defaultConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsDefault.json"; - const std::string themeConfigPath = "Data\\SKSE\\Plugins\\CommunityShaders\\SettingsTheme.json"; float timer = 0; double smoothDrawCalls[RE::BSShader::Type::Total + 1]; diff --git a/src/ThemeManager.cpp b/src/ThemeManager.cpp new file mode 100644 index 0000000000..6f98b0f483 --- /dev/null +++ b/src/ThemeManager.cpp @@ -0,0 +1,384 @@ +#include "ThemeManager.h" + +#include +#include +#include +#include + +#include "Utils/FileSystem.h" + +using namespace SKSE; + +namespace +{ + /** + * @brief Gets file modification time + */ + std::time_t GetFileModTime(const std::filesystem::path& filePath) + { + try { + auto fileTime = std::filesystem::last_write_time(filePath); + auto systemTime = std::chrono::time_point_cast( + fileTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::system_clock::to_time_t(systemTime); + } catch (...) { + return 0; + } + } +} + +size_t ThemeManager::DiscoverThemes() +{ + if (discovered) { + return themes.size(); + } + + themes.clear(); + + auto themesDir = GetThemesDirectory(); + if (!std::filesystem::exists(themesDir)) { + logger::info("Themes directory does not exist: {}", themesDir.string()); + discovered = true; + return 0; + } + + logger::info("Discovering themes in: {}", themesDir.string()); + + try { + for (const auto& entry : std::filesystem::directory_iterator(themesDir)) { + if (!entry.is_regular_file() || entry.path().extension() != ".json") { + continue; + } + + // Check file size + auto fileSize = entry.file_size(); + if (fileSize > MAX_FILE_SIZE) { + logger::warn("Theme file too large, skipping: {} ({}MB)", + entry.path().filename().string(), fileSize / (1024 * 1024)); + continue; + } + + if (themes.size() >= MAX_THEMES) { + logger::warn("Maximum number of themes ({}) reached, skipping remaining files", MAX_THEMES); + break; + } + + auto themeInfo = LoadThemeFile(entry.path()); + if (themeInfo && themeInfo->isValid) { + themes.push_back(std::move(*themeInfo)); + logger::info("Discovered theme: {} ({})", themes.back().name, themes.back().displayName); + } + } + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error discovering themes: {}", e.what()); + } + + // Sort themes alphabetically by display name + std::sort(themes.begin(), themes.end(), [](const ThemeInfo& a, const ThemeInfo& b) { + return a.displayName < b.displayName; + }); + + discovered = true; + logger::info("Theme discovery complete. Found {} themes", themes.size()); + return themes.size(); +} + +std::vector ThemeManager::GetThemeNames() const +{ + std::vector names; + names.reserve(themes.size()); + + for (const auto& theme : themes) { + names.push_back(theme.name); + } + + return names; +} + +bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) +{ + if (!discovered) { + DiscoverThemes(); + } + + if (themeName.empty()) { + // Empty theme name means use current/custom theme + return true; + } + + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); + + if (it == themes.end()) { + logger::warn("Theme not found: {}", themeName); + return false; + } + + if (!it->isValid) { + logger::warn("Theme is invalid: {}", themeName); + return false; + } + + try { + if (it->themeData.contains("Theme") && it->themeData["Theme"].is_object()) { + themeSettings = it->themeData["Theme"]; + logger::info("Loaded theme: {} ({})", it->name, it->displayName); + return true; + } else { + logger::warn("Theme file missing 'Theme' object: {}", themeName); + return false; + } + } catch (const std::exception& e) { + logger::warn("Error loading theme {}: {}", themeName, e.what()); + return false; + } +} + +const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const +{ + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); + + return (it != themes.end()) ? &(*it) : nullptr; +} + +void ThemeManager::RefreshThemes() +{ + discovered = false; + DiscoverThemes(); +} + +std::filesystem::path ThemeManager::GetThemesDirectory() const +{ + return Util::PathHelpers::GetThemesPath(); +} + +void ThemeManager::CreateDefaultThemeFiles() +{ + auto themesDir = GetThemesDirectory(); + + try { + std::filesystem::create_directories(themesDir); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to create themes directory: {}", e.what()); + return; + } + + // Define default themes as JSON strings (what users would create) + struct DefaultTheme { + std::string name; + std::string content; + }; + + std::vector defaultThemes = { + {"Default", R"({ + "DisplayName": "Default Dark", + "Description": "The classic Community Shaders dark theme", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.09, 0.09, 0.09, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.5, 0.5, 0.5, 0.8] + } + } +})"}, + + {"Light", R"({ + "DisplayName": "Light Mode", + "Description": "Clean light theme with dark text", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.9, 0.9, 0.9, 0.95], + "Text": [0.1, 0.1, 0.1, 1.0], + "Border": [0.3, 0.3, 0.3, 0.8] + } + } +})"}, + + {"Ocean", R"({ + "DisplayName": "Ocean Blue", + "Description": "Cool blue tones with subtle gradients", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.2, 0.4, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "Border": [0.3, 0.5, 0.8, 0.8] + } + } +})"}, + + {"Forest", R"({ + "DisplayName": "Forest Green", + "Description": "Natural green theme inspired by Skyrim's forests", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.3, 0.15, 0.9], + "Text": [0.9, 1.0, 0.9, 1.0], + "Border": [0.4, 0.7, 0.4, 0.8] + } + } +})"}, + + {"Mystic", R"({ + "DisplayName": "Mystic Purple", + "Description": "Magical purple theme with mystical vibes", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.2, 0.1, 0.3, 0.9], + "Text": [0.95, 0.9, 1.0, 1.0], + "Border": [0.6, 0.4, 0.8, 0.8] + } + } +})"}, + + {"Amber", R"({ + "DisplayName": "Warm Amber", + "Description": "Warm amber tones reminiscent of candlelight", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.2, 0.15, 0.05, 0.9], + "Text": [1.0, 0.9, 0.7, 1.0], + "Border": [0.8, 0.6, 0.3, 0.8] + } + } +})"}, + + {"HighContrast", R"({ + "DisplayName": "High Contrast", + "Description": "High contrast theme for accessibility", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.0, 0.0, 0.0, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [1.0, 1.0, 1.0, 0.9] + } + } +})"}} + }; + + // Create theme files + for (const auto& theme : defaultThemes) { + auto themeFile = themesDir / (theme.name + ".json"); + + // Only create if it doesn't exist (don't overwrite user modifications) + if (std::filesystem::exists(themeFile)) { + continue; + } + + try { + std::ofstream file(themeFile); + if (!file.is_open()) { + logger::warn("Failed to create theme file: {}", themeFile.string()); + continue; + } + + file << theme.content; + file.close(); + + logger::info("Created default theme file: {}", theme.name); + } catch (const std::exception& e) { + logger::warn("Error creating theme file {}: {}", theme.name, e.what()); + } + } +} + +std::unique_ptr ThemeManager::LoadThemeFile(const std::filesystem::path& filePath) +{ + auto themeInfo = std::make_unique(); + themeInfo->name = filePath.stem().string(); + themeInfo->filePath = filePath.string(); + themeInfo->lastModified = GetFileModTime(filePath); + + try { + std::ifstream file(filePath); + if (!file.is_open()) { + logger::warn("Cannot open theme file: {}", filePath.string()); + return themeInfo; + } + + json themeJson; + file >> themeJson; + file.close(); + + if (!ValidateThemeData(themeJson)) { + logger::warn("Invalid theme data in: {}", filePath.string()); + return themeInfo; + } + + themeInfo->themeData = themeJson; + + // Extract metadata + if (themeJson.contains("DisplayName") && themeJson["DisplayName"].is_string()) { + themeInfo->displayName = themeJson["DisplayName"]; + } else { + themeInfo->displayName = themeInfo->name; // Fallback to filename + } + + if (themeJson.contains("Description") && themeJson["Description"].is_string()) { + themeInfo->description = themeJson["Description"]; + } + + if (themeJson.contains("Version") && themeJson["Version"].is_string()) { + themeInfo->version = themeJson["Version"]; + } + + if (themeJson.contains("Author") && themeJson["Author"].is_string()) { + themeInfo->author = themeJson["Author"]; + } + + themeInfo->isValid = true; + + } catch (const std::exception& e) { + logger::warn("Error loading theme file {}: {}", filePath.string(), e.what()); + } + + return themeInfo; +} + +bool ThemeManager::ValidateThemeData(const json& themeData) const +{ + try { + // Must have Theme object + if (!themeData.contains("Theme") || !themeData["Theme"].is_object()) { + return false; + } + + const auto& theme = themeData["Theme"]; + + // Basic validation - check for required structure + // This is a minimal check; the Menu system will handle detailed validation + if (theme.contains("UseSimplePalette") && theme["UseSimplePalette"].is_boolean()) { + if (theme["UseSimplePalette"] == true) { + // Simple palette should have Palette object + if (!theme.contains("Palette") || !theme["Palette"].is_object()) { + return false; + } + } + } + + return true; + } catch (...) { + return false; + } +} \ No newline at end of file diff --git a/src/ThemeManager.h b/src/ThemeManager.h new file mode 100644 index 0000000000..cae93c65dd --- /dev/null +++ b/src/ThemeManager.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include + +using json = nlohmann::json; + +/** + * @brief Manages hot-swappable theme system for Community Shaders + * + * This class handles discovery and loading of theme JSON files from the Themes directory, + * allowing users to create and modify themes without code changes. Similar to the + * SettingsOverrideManager but specifically for theme management. + * + * Theme files should be placed in: Data\SKSE\Plugins\CommunityShaders\Themes\ + * File format: {ThemeName}.json + */ +class ThemeManager +{ +public: + struct ThemeInfo + { + std::string name; // Filename without extension + std::string displayName; // Human-readable name from JSON + std::string description; // Theme description from JSON + std::string filePath; // Full path to theme file + json themeData; // Complete theme settings + bool isValid = false; // Whether theme loaded successfully + + // Metadata + std::string version; + std::string author; + std::time_t lastModified = 0; + }; + + static ThemeManager* GetSingleton() + { + static ThemeManager instance; + return &instance; + } + + /** + * @brief Discovers all theme files in the themes directory + * @return Number of theme files discovered + */ + size_t DiscoverThemes(); + + /** + * @brief Gets list of all discovered themes + * @return Vector of theme information + */ + const std::vector& GetThemes() const { return themes; } + + /** + * @brief Gets theme names for dropdown display + * @return Vector of theme names + */ + std::vector GetThemeNames() const; + + /** + * @brief Loads a specific theme by name + * @param themeName Name of the theme to load + * @param themeSettings Output parameter for loaded theme settings + * @return True if theme was loaded successfully + */ + bool LoadTheme(const std::string& themeName, json& themeSettings); + + /** + * @brief Gets theme info by name + * @param themeName Name of the theme + * @return Pointer to theme info or nullptr if not found + */ + const ThemeInfo* GetThemeInfo(const std::string& themeName) const; + + /** + * @brief Refreshes theme discovery (for runtime updates) + */ + void RefreshThemes(); + + /** + * @brief Checks if themes have been discovered + */ + bool IsDiscovered() const { return discovered; } + + /** + * @brief Gets the themes directory path + */ + std::filesystem::path GetThemesDirectory() const; + + /** + * @brief Creates default theme files if they don't exist + */ + void CreateDefaultThemeFiles(); + +private: + ThemeManager() = default; + ~ThemeManager() = default; + ThemeManager(const ThemeManager&) = delete; + ThemeManager& operator=(const ThemeManager&) = delete; + + /** + * @brief Loads a single theme file + * @param filePath Path to the theme file + * @return Theme info if successful, nullptr otherwise + */ + std::unique_ptr LoadThemeFile(const std::filesystem::path& filePath); + + /** + * @brief Validates theme data structure + * @param themeData JSON data to validate + * @return True if theme data is valid + */ + bool ValidateThemeData(const json& themeData) const; + + std::vector themes; + bool discovered = false; + + // Constants + static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading + static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size +}; \ No newline at end of file diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index 470d1eb57c..fff78789a3 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -78,6 +78,11 @@ namespace Util return GetCommunityShaderPath() / "SettingsTheme.json"; } + std::filesystem::path GetThemesPath() + { + return GetCommunityShaderPath() / "Themes"; + } + std::filesystem::path GetOverridesPath() { return GetCommunityShaderPath() / "Overrides"; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index 3fce942eb9..e74df643b4 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -92,6 +92,12 @@ namespace Util */ std::filesystem::path GetSettingsThemePath(); + /** + * Gets the Themes directory path + * @return CommunityShaderPath / "Themes" + */ + std::filesystem::path GetThemesPath(); + /** * Gets the Overrides directory path * @return CommunityShaderPath / "Overrides" diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index d7a1e755e3..7c83016446 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -6,6 +6,7 @@ #include "Menu.h" #include "ShaderCache.h" #include "State.h" +#include "ThemeManager.h" #include "TruePBR.h" #include "ENB/ENBSeriesAPI.h" @@ -162,6 +163,12 @@ bool Load() auto state = globals::state; state->Load(); state->LoadTheme(); // Load theme settings from SettingsTheme.json + + // Initialize theme system - create default themes and discover existing ones + globals::menu->CreateDefaultThemes(); // Creates JSON files if they don't exist + auto themeManager = ThemeManager::GetSingleton(); + themeManager->DiscoverThemes(); // Discover all available themes + auto log = spdlog::default_logger(); log->set_level(state->GetLogLevel()); From 9cff35884e3a7e0e2a56a5faa09433fa63384ebf Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 23 Sep 2025 16:18:24 +1000 Subject: [PATCH 03/29] Updates & Fine Tuning --- .../CommunityShaders/Themes/Amber.json | 94 ++++- .../CommunityShaders/Themes/Default.json | 94 ++++- .../CommunityShaders/Themes/DragonBlood.json | 92 ++++- .../CommunityShaders/Themes/DwemerBronze.json | 94 ++++- .../CommunityShaders/Themes/Forest.json | 94 ++++- .../CommunityShaders/Themes/HighContrast.json | 94 ++++- .../CommunityShaders/Themes/Light.json | 94 ++++- .../CommunityShaders/Themes/Mystic.json | 94 ++++- .../CommunityShaders/Themes/NordicFrost.json | 94 ++++- .../CommunityShaders/Themes/Ocean.json | 94 ++++- src/Menu.cpp | 3 +- src/Menu.h | 2 +- src/Menu/FeatureListRenderer.cpp | 2 +- src/Menu/HomePageRenderer.cpp | 1 - src/Menu/SettingsTabRenderer.cpp | 175 ++++---- src/Menu/SettingsTabRenderer.h | 4 +- src/Menu/ThemeManager.cpp | 382 +++++++++++++---- src/Menu/ThemeManager.h | 122 +++++- src/ThemeManager.cpp | 384 ------------------ src/ThemeManager.h | 123 ------ src/Utils/FileSystem.cpp | 20 - src/Utils/UI.cpp | 29 +- src/XSEPlugin.cpp | 2 +- 23 files changed, 1436 insertions(+), 751 deletions(-) delete mode 100644 src/ThemeManager.cpp delete mode 100644 src/ThemeManager.h diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json index d6db1c5513..b978950063 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -1,14 +1,100 @@ { "DisplayName": "Warm Amber", - "Description": "Cozy amber tones reminiscent of hearth fires and candlelight", - "Version": "1.0.0", + "Description": "Cozy amber tones reminiscent of hearth fires and candlelight in Nordic taverns", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 26.8, + "GlobalScale": -0.02, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.2, 0.15, 0.05, 0.9], "Text": [1.0, 0.9, 0.7, 1.0], "Border": [0.8, 0.6, 0.3, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.5, 0.4, 0.3, 1.0], + "Error": [1.0, 0.4, 0.2, 1.0], + "Warning": [1.0, 0.7, 0.1, 1.0], + "RestartNeeded": [0.8, 0.9, 0.3, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.4, 1.0], + "SuccessColor": [0.6, 0.8, 0.3, 1.0], + "InfoColor": [0.7, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.9, 0.8, 0.6, 1.0], + "ColorHovered": [1.0, 0.9, 0.7, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [14.0, 12.0], + "WindowRounding": 5.0, + "IndentSpacing": 8.0, + "FramePadding": [7.0, 5.0], + "CellPadding": [12.0, 5.0], + "ItemSpacing": [8.0, 8.0] + }, + "FullPalette": [ + [1.00, 0.90, 0.70, 0.90], + [0.80, 0.70, 0.50, 1.00], + [0.20, 0.15, 0.05, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.18, 0.13, 0.04, 0.85], + [0.70, 0.60, 0.40, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.15, 0.10, 0.03, 1.00], + [0.40, 0.30, 0.15, 0.40], + [0.50, 0.40, 0.20, 0.45], + [0.15, 0.10, 0.03, 0.83], + [0.18, 0.13, 0.05, 0.87], + [0.30, 0.22, 0.10, 0.90], + [0.22, 0.16, 0.06, 0.90], + [0.35, 0.26, 0.12, 0.90], + [0.45, 0.35, 0.18, 1.00], + [0.55, 0.45, 0.25, 1.00], + [0.65, 0.55, 0.35, 1.00], + [1.00, 0.90, 0.70, 1.00], + [0.80, 0.70, 0.50, 1.00], + [0.40, 0.30, 0.15, 1.00], + [0.80, 0.60, 0.30, 0.40], + [0.90, 0.70, 0.40, 0.67], + [1.00, 0.80, 0.50, 1.00], + [0.95, 0.65, 0.25, 1.00], + [1.00, 0.75, 0.35, 1.00], + [1.00, 0.85, 0.45, 1.00], + [0.70, 0.55, 0.30, 1.00], + [0.80, 0.65, 0.40, 1.00], + [0.95, 0.80, 0.55, 1.00], + [1.00, 0.90, 0.70, 1.00], + [1.00, 0.90, 0.70, 0.60], + [1.00, 0.90, 0.70, 0.90], + [0.80, 0.60, 0.30, 0.31], + [0.90, 0.70, 0.40, 0.80], + [1.00, 0.80, 0.50, 1.00], + [0.18, 0.13, 0.04, 0.97], + [0.85, 0.70, 0.40, 1.00], + [0.70, 0.55, 0.30, 0.50], + [0.00, 0.00, 0.00, 0.00], + [1.00, 0.90, 0.70, 1.00], + [1.00, 0.80, 0.30, 1.00], + [1.00, 0.80, 0.30, 1.00], + [1.00, 0.80, 0.30, 1.00], + [0.80, 0.60, 0.30, 0.40], + [0.35, 0.26, 0.12, 1.00], + [0.28, 0.20, 0.09, 1.00], + [0.00, 0.00, 0.00, 0.00], + [1.00, 0.90, 0.70, 0.06], + [0.80, 0.60, 0.30, 0.35], + [0.90, 0.50, 0.30, 1.00], + [0.90, 0.70, 0.40, 1.00], + [0.50, 0.40, 0.20, 0.56], + [0.35, 0.26, 0.12, 0.35], + [0.35, 0.26, 0.12, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 99bd23c966..12688f62ac 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -1,14 +1,100 @@ { "DisplayName": "Default Dark", - "Description": "The classic Community Shaders dark theme", - "Version": "1.0.0", + "Description": "The classic Community Shaders dark theme with comprehensive styling", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 27.0, + "GlobalScale": 0.0, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.09, 0.09, 0.09, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], "Border": [0.5, 0.5, 0.5, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.4, 0.4, 1.0], + "Warning": [1.0, 0.6, 0.2, 1.0], + "RestartNeeded": [0.4, 1.0, 0.4, 1.0], + "CurrentHotkey": [1.0, 1.0, 0.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.2, 0.6, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.8, 0.8, 1.0], + "ColorHovered": [0.6, 0.6, 0.6, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 0.0, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 12.0], + "WindowRounding": 4.0, + "IndentSpacing": 8.0, + "FramePadding": [6.0, 4.0], + "CellPadding": [12.0, 4.0], + "ItemSpacing": [8.0, 8.0] + }, + "FullPalette": [ + [0.90, 0.90, 0.90, 0.90], + [0.60, 0.60, 0.60, 1.00], + [0.09, 0.09, 0.09, 0.95], + [0.00, 0.00, 0.00, 0.00], + [0.05, 0.05, 0.10, 0.85], + [0.70, 0.70, 0.70, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.00, 0.00, 0.00, 1.00], + [0.26, 0.26, 0.26, 0.40], + [0.40, 0.40, 0.40, 0.45], + [0.00, 0.00, 0.00, 0.83], + [0.00, 0.00, 0.00, 0.87], + [0.20, 0.20, 0.30, 0.90], + [0.02, 0.02, 0.03, 0.90], + [0.20, 0.22, 0.27, 0.90], + [0.28, 0.28, 0.28, 1.00], + [0.42, 0.42, 0.42, 1.00], + [0.56, 0.56, 0.56, 1.00], + [1.00, 1.00, 1.00, 1.00], + [0.70, 0.70, 0.70, 1.00], + [0.26, 0.26, 0.26, 1.00], + [0.26, 0.59, 0.98, 0.40], + [0.26, 0.59, 0.98, 0.67], + [0.26, 0.59, 0.98, 1.00], + [0.06, 0.53, 0.98, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.50, 0.50, 0.50, 1.00], + [0.70, 0.60, 0.60, 1.00], + [0.90, 0.70, 0.70, 1.00], + [1.00, 1.00, 1.00, 1.00], + [1.00, 1.00, 1.00, 0.60], + [1.00, 1.00, 1.00, 0.90], + [0.26, 0.59, 0.98, 0.31], + [0.26, 0.59, 0.98, 0.80], + [0.26, 0.59, 0.98, 1.00], + [0.15, 0.15, 0.15, 0.97], + [0.26, 0.59, 0.98, 1.00], + [0.70, 0.60, 0.60, 0.50], + [0.00, 0.00, 0.00, 0.00], + [1.00, 1.00, 1.00, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.26, 0.59, 0.98, 0.40], + [0.26, 0.26, 0.26, 1.00], + [0.19, 0.19, 0.19, 1.00], + [0.00, 0.00, 0.00, 0.00], + [1.00, 1.00, 1.00, 0.06], + [0.26, 0.59, 0.98, 0.35], + [0.80, 0.50, 0.50, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.30, 0.30, 0.30, 0.56], + [0.20, 0.20, 0.20, 0.35], + [0.20, 0.20, 0.20, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 2050381f75..11907b8b27 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -1,14 +1,100 @@ { "DisplayName": "Dragon Blood", "Description": "Dark red theme inspired by dragon lore and ancient power", - "Version": "1.0.0", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 28.0, + "GlobalScale": 0.1, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.4, "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], "Border": [0.8, 0.3, 0.3, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.4, 0.2, 0.2, 1.0], + "Error": [1.0, 0.1, 0.1, 1.0], + "Warning": [1.0, 0.5, 0.0, 1.0], + "RestartNeeded": [0.8, 0.6, 0.2, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.2, 1.0], + "SuccessColor": [0.6, 0.8, 0.2, 1.0], + "InfoColor": [0.8, 0.4, 0.6, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.9, 0.6, 0.6, 1.0], + "ColorHovered": [1.0, 0.7, 0.7, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 3.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 8.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 10.0] + }, + "FullPalette": [ + [1.00, 0.85, 0.85, 0.90], + [0.80, 0.60, 0.60, 1.00], + [0.25, 0.05, 0.05, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.20, 0.03, 0.03, 0.85], + [0.60, 0.40, 0.40, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.15, 0.00, 0.00, 1.00], + [0.40, 0.15, 0.15, 0.40], + [0.50, 0.20, 0.20, 0.45], + [0.10, 0.00, 0.00, 0.83], + [0.15, 0.00, 0.00, 0.87], + [0.30, 0.10, 0.10, 0.90], + [0.25, 0.05, 0.05, 0.90], + [0.35, 0.15, 0.15, 0.90], + [0.45, 0.15, 0.15, 1.00], + [0.60, 0.25, 0.25, 1.00], + [0.75, 0.35, 0.35, 1.00], + [1.00, 0.85, 0.85, 1.00], + [0.80, 0.60, 0.60, 1.00], + [0.40, 0.15, 0.15, 1.00], + [0.80, 0.30, 0.30, 0.40], + [0.90, 0.40, 0.40, 0.67], + [1.00, 0.50, 0.50, 1.00], + [0.85, 0.25, 0.25, 1.00], + [0.90, 0.35, 0.35, 1.00], + [0.95, 0.45, 0.45, 1.00], + [0.60, 0.30, 0.30, 1.00], + [0.80, 0.40, 0.40, 1.00], + [0.95, 0.55, 0.55, 1.00], + [1.00, 0.85, 0.85, 1.00], + [1.00, 0.85, 0.85, 0.60], + [1.00, 0.85, 0.85, 0.90], + [0.80, 0.30, 0.30, 0.31], + [0.90, 0.40, 0.40, 0.80], + [1.00, 0.50, 0.50, 1.00], + [0.20, 0.05, 0.05, 0.97], + [0.85, 0.35, 0.35, 1.00], + [0.70, 0.30, 0.30, 0.50], + [0.00, 0.00, 0.00, 0.00], + [1.00, 0.85, 0.85, 1.00], + [1.00, 0.60, 0.20, 1.00], + [1.00, 0.60, 0.20, 1.00], + [1.00, 0.60, 0.20, 1.00], + [0.80, 0.30, 0.30, 0.40], + [0.40, 0.15, 0.15, 1.00], + [0.30, 0.10, 0.10, 1.00], + [0.00, 0.00, 0.00, 0.00], + [1.00, 0.85, 0.85, 0.06], + [0.80, 0.30, 0.30, 0.35], + [1.00, 0.30, 0.30, 1.00], + [0.90, 0.40, 0.40, 1.00], + [0.50, 0.20, 0.20, 0.56], + [0.35, 0.15, 0.15, 0.35], + [0.35, 0.15, 0.15, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index 98cbe608dd..ec7ea64ed1 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -1,14 +1,100 @@ { "DisplayName": "Dwemer Bronze", - "Description": "Ancient bronze theme inspired by lost Dwemer technology", - "Version": "1.0.0", + "Description": "Ancient bronze theme inspired by lost Dwemer technology and metallic machinery", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 27.5, + "GlobalScale": 0.05, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.3, "Palette": { "Background": [0.15, 0.12, 0.08, 0.9], "Text": [0.9, 0.75, 0.5, 1.0], "Border": [0.7, 0.5, 0.3, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.4, 0.35, 0.25, 1.0], + "Error": [0.9, 0.3, 0.1, 1.0], + "Warning": [1.0, 0.7, 0.2, 1.0], + "RestartNeeded": [0.8, 0.8, 0.4, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.3, 1.0], + "SuccessColor": [0.5, 0.7, 0.3, 1.0], + "InfoColor": [0.6, 0.7, 0.8, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.65, 0.4, 1.0], + "ColorHovered": [0.95, 0.8, 0.55, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 3.0, + "ChildBorderSize": 2.0, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 2.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 9.0] + }, + "FullPalette": [ + [0.90, 0.75, 0.50, 0.90], + [0.70, 0.60, 0.40, 1.00], + [0.15, 0.12, 0.08, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.13, 0.10, 0.06, 0.85], + [0.60, 0.50, 0.35, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.10, 0.08, 0.05, 1.00], + [0.35, 0.28, 0.18, 0.40], + [0.45, 0.36, 0.23, 0.45], + [0.10, 0.08, 0.05, 0.83], + [0.13, 0.10, 0.07, 0.87], + [0.25, 0.20, 0.13, 0.90], + [0.18, 0.14, 0.09, 0.90], + [0.30, 0.24, 0.15, 0.90], + [0.40, 0.32, 0.20, 1.00], + [0.50, 0.42, 0.28, 1.00], + [0.60, 0.52, 0.38, 1.00], + [0.90, 0.75, 0.50, 1.00], + [0.70, 0.60, 0.40, 1.00], + [0.35, 0.28, 0.18, 1.00], + [0.70, 0.50, 0.30, 0.40], + [0.80, 0.60, 0.35, 0.67], + [0.90, 0.70, 0.45, 1.00], + [0.85, 0.55, 0.25, 1.00], + [0.90, 0.65, 0.35, 1.00], + [0.95, 0.75, 0.45, 1.00], + [0.65, 0.50, 0.30, 1.00], + [0.75, 0.60, 0.40, 1.00], + [0.85, 0.70, 0.50, 1.00], + [0.90, 0.75, 0.50, 1.00], + [0.90, 0.75, 0.50, 0.60], + [0.90, 0.75, 0.50, 0.90], + [0.70, 0.50, 0.30, 0.31], + [0.80, 0.60, 0.35, 0.80], + [0.90, 0.70, 0.45, 1.00], + [0.13, 0.10, 0.06, 0.97], + [0.75, 0.60, 0.35, 1.00], + [0.65, 0.50, 0.30, 0.50], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.75, 0.50, 1.00], + [0.90, 0.70, 0.30, 1.00], + [0.90, 0.70, 0.30, 1.00], + [0.90, 0.70, 0.30, 1.00], + [0.70, 0.50, 0.30, 0.40], + [0.30, 0.24, 0.15, 1.00], + [0.23, 0.18, 0.11, 1.00], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.75, 0.50, 0.06], + [0.70, 0.50, 0.30, 0.35], + [0.80, 0.45, 0.25, 1.00], + [0.80, 0.60, 0.35, 1.00], + [0.45, 0.36, 0.23, 0.56], + [0.30, 0.24, 0.15, 0.35], + [0.30, 0.24, 0.15, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json index f6d46bbf6c..2cbc5455be 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -1,14 +1,100 @@ { "DisplayName": "Forest Green", - "Description": "Natural green theme inspired by Skyrim's ancient forests", - "Version": "1.0.0", + "Description": "Natural green theme inspired by Skyrim's ancient forests and wilderness", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 26.5, + "GlobalScale": -0.05, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.6, "Palette": { "Background": [0.1, 0.3, 0.15, 0.9], "Text": [0.9, 1.0, 0.9, 1.0], "Border": [0.4, 0.7, 0.4, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.3, 0.4, 0.3, 1.0], + "Error": [0.8, 0.3, 0.2, 1.0], + "Warning": [0.9, 0.7, 0.2, 1.0], + "RestartNeeded": [0.6, 0.9, 0.3, 1.0], + "CurrentHotkey": [0.8, 1.0, 0.6, 1.0], + "SuccessColor": [0.2, 0.8, 0.3, 1.0], + "InfoColor": [0.4, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.9, 0.7, 1.0], + "ColorHovered": [0.8, 1.0, 0.8, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.5, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [14.0, 12.0], + "WindowRounding": 6.0, + "IndentSpacing": 9.0, + "FramePadding": [7.0, 5.0], + "CellPadding": [13.0, 5.0], + "ItemSpacing": [9.0, 9.0] + }, + "FullPalette": [ + [0.90, 1.00, 0.90, 0.90], + [0.60, 0.80, 0.60, 1.00], + [0.10, 0.30, 0.15, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.08, 0.25, 0.12, 0.85], + [0.50, 0.70, 0.50, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.05, 0.15, 0.05, 1.00], + [0.20, 0.40, 0.25, 0.40], + [0.30, 0.50, 0.35, 0.45], + [0.05, 0.15, 0.08, 0.83], + [0.08, 0.20, 0.10, 0.87], + [0.15, 0.35, 0.20, 0.90], + [0.10, 0.25, 0.15, 0.90], + [0.18, 0.38, 0.22, 0.90], + [0.25, 0.45, 0.30, 1.00], + [0.35, 0.55, 0.40, 1.00], + [0.45, 0.65, 0.50, 1.00], + [0.90, 1.00, 0.90, 1.00], + [0.70, 0.85, 0.75, 1.00], + [0.20, 0.40, 0.25, 1.00], + [0.40, 0.70, 0.45, 0.40], + [0.50, 0.80, 0.55, 0.67], + [0.60, 0.90, 0.65, 1.00], + [0.35, 0.75, 0.40, 1.00], + [0.45, 0.85, 0.50, 1.00], + [0.55, 0.95, 0.60, 1.00], + [0.40, 0.60, 0.45, 1.00], + [0.50, 0.70, 0.55, 1.00], + [0.70, 0.90, 0.75, 1.00], + [0.90, 1.00, 0.90, 1.00], + [0.90, 1.00, 0.90, 0.60], + [0.90, 1.00, 0.90, 0.90], + [0.40, 0.70, 0.45, 0.31], + [0.50, 0.80, 0.55, 0.80], + [0.60, 0.90, 0.65, 1.00], + [0.12, 0.22, 0.15, 0.97], + [0.45, 0.75, 0.50, 1.00], + [0.50, 0.70, 0.55, 0.50], + [0.00, 0.00, 0.00, 0.00], + [0.90, 1.00, 0.90, 1.00], + [0.80, 0.90, 0.20, 1.00], + [0.80, 0.90, 0.20, 1.00], + [0.80, 0.90, 0.20, 1.00], + [0.40, 0.70, 0.45, 0.40], + [0.20, 0.35, 0.25, 1.00], + [0.15, 0.28, 0.20, 1.00], + [0.00, 0.00, 0.00, 0.00], + [0.90, 1.00, 0.90, 0.06], + [0.40, 0.70, 0.45, 0.35], + [0.70, 0.40, 0.30, 1.00], + [0.50, 0.80, 0.55, 1.00], + [0.25, 0.45, 0.30, 0.56], + [0.18, 0.35, 0.22, 0.35], + [0.18, 0.35, 0.22, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index 842050e2e1..99c6e018f4 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -1,14 +1,100 @@ { "DisplayName": "High Contrast", - "Description": "High contrast theme for improved accessibility and visibility", - "Version": "1.0.0", + "Description": "High contrast black and white theme for improved accessibility and visibility", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 29.0, + "GlobalScale": 0.05, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.3, "Palette": { "Background": [0.0, 0.0, 0.0, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], "Border": [1.0, 1.0, 1.0, 0.9] - } + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.0, 0.0, 1.0], + "Warning": [1.0, 1.0, 0.0, 1.0], + "RestartNeeded": [0.0, 1.0, 0.0, 1.0], + "CurrentHotkey": [0.0, 1.0, 1.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.0, 0.5, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [1.0, 1.0, 1.0, 1.0], + "ColorHovered": [0.8, 0.8, 0.8, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 4.0, + "ChildBorderSize": 2.0, + "FrameBorderSize": 2.0, + "WindowPadding": [18.0, 16.0], + "WindowRounding": 0.0, + "IndentSpacing": 12.0, + "FramePadding": [10.0, 6.0], + "CellPadding": [16.0, 8.0], + "ItemSpacing": [12.0, 12.0] + }, + "FullPalette": [ + [1.00, 1.00, 1.00, 0.90], + [0.80, 0.80, 0.80, 1.00], + [0.00, 0.00, 0.00, 0.95], + [0.00, 0.00, 0.00, 0.00], + [0.05, 0.05, 0.05, 0.85], + [1.00, 1.00, 1.00, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.00, 0.00, 0.00, 1.00], + [0.30, 0.30, 0.30, 0.40], + [0.50, 0.50, 0.50, 0.45], + [0.00, 0.00, 0.00, 0.83], + [0.00, 0.00, 0.00, 0.87], + [0.20, 0.20, 0.20, 0.90], + [0.00, 0.00, 0.00, 0.90], + [0.15, 0.15, 0.15, 0.90], + [0.30, 0.30, 0.30, 1.00], + [0.50, 0.50, 0.50, 1.00], + [0.70, 0.70, 0.70, 1.00], + [1.00, 1.00, 1.00, 1.00], + [0.80, 0.80, 0.80, 1.00], + [0.20, 0.20, 0.20, 1.00], + [0.60, 0.60, 0.60, 0.40], + [0.80, 0.80, 0.80, 0.67], + [1.00, 1.00, 1.00, 1.00], + [0.90, 0.90, 0.90, 1.00], + [0.85, 0.85, 0.85, 1.00], + [0.75, 0.75, 0.75, 1.00], + [0.60, 0.60, 0.60, 1.00], + [0.80, 0.80, 0.80, 1.00], + [0.90, 0.90, 0.90, 1.00], + [1.00, 1.00, 1.00, 1.00], + [1.00, 1.00, 1.00, 0.60], + [1.00, 1.00, 1.00, 0.90], + [0.60, 0.60, 0.60, 0.31], + [0.80, 0.80, 0.80, 0.80], + [1.00, 1.00, 1.00, 1.00], + [0.10, 0.10, 0.10, 0.97], + [0.85, 0.85, 0.85, 1.00], + [0.70, 0.70, 0.70, 0.50], + [0.00, 0.00, 0.00, 0.00], + [1.00, 1.00, 1.00, 1.00], + [1.00, 1.00, 0.00, 1.00], + [1.00, 1.00, 0.00, 1.00], + [1.00, 1.00, 0.00, 1.00], + [0.60, 0.60, 0.60, 0.40], + [0.20, 0.20, 0.20, 1.00], + [0.15, 0.15, 0.15, 1.00], + [0.00, 0.00, 0.00, 0.00], + [1.00, 1.00, 1.00, 0.06], + [0.60, 0.60, 0.60, 0.35], + [1.00, 0.00, 0.00, 1.00], + [0.80, 0.80, 0.80, 1.00], + [0.40, 0.40, 0.40, 0.56], + [0.25, 0.25, 0.25, 0.35], + [0.25, 0.25, 0.25, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 9c2debfacd..76027c7a30 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -1,14 +1,100 @@ { "DisplayName": "Light Mode", - "Description": "Clean light theme with dark text for daytime use", - "Version": "1.0.0", + "Description": "Clean bright theme with dark text for comfortable daytime use", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 26.0, + "GlobalScale": -0.1, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.4, "Palette": { "Background": [0.9, 0.9, 0.9, 0.95], "Text": [0.1, 0.1, 0.1, 1.0], "Border": [0.3, 0.3, 0.3, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.6, 0.6, 0.6, 1.0], + "Error": [0.8, 0.2, 0.2, 1.0], + "Warning": [0.9, 0.5, 0.1, 1.0], + "RestartNeeded": [0.2, 0.7, 0.2, 1.0], + "CurrentHotkey": [0.8, 0.6, 0.1, 1.0], + "SuccessColor": [0.1, 0.6, 0.1, 1.0], + "InfoColor": [0.2, 0.4, 0.8, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.3, 0.3, 0.3, 1.0], + "ColorHovered": [0.2, 0.2, 0.2, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 1.5, + "ChildBorderSize": 0.5, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 10.0], + "WindowRounding": 3.0, + "IndentSpacing": 7.0, + "FramePadding": [6.0, 3.0], + "CellPadding": [10.0, 3.0], + "ItemSpacing": [7.0, 6.0] + }, + "FullPalette": [ + [0.10, 0.10, 0.10, 0.90], + [0.40, 0.40, 0.40, 1.00], + [0.90, 0.90, 0.90, 0.95], + [1.00, 1.00, 1.00, 0.00], + [0.95, 0.95, 0.95, 0.85], + [0.30, 0.30, 0.30, 0.65], + [1.00, 1.00, 1.00, 0.00], + [1.00, 1.00, 1.00, 1.00], + [0.70, 0.70, 0.70, 0.40], + [0.50, 0.50, 0.50, 0.45], + [1.00, 1.00, 1.00, 0.83], + [1.00, 1.00, 1.00, 0.87], + [0.80, 0.80, 0.80, 0.90], + [0.98, 0.98, 0.98, 0.90], + [0.85, 0.85, 0.85, 0.90], + [0.75, 0.75, 0.75, 1.00], + [0.58, 0.58, 0.58, 1.00], + [0.44, 0.44, 0.44, 1.00], + [0.10, 0.10, 0.10, 1.00], + [0.30, 0.30, 0.30, 1.00], + [0.70, 0.70, 0.70, 1.00], + [0.26, 0.59, 0.98, 0.40], + [0.26, 0.59, 0.98, 0.67], + [0.26, 0.59, 0.98, 1.00], + [0.06, 0.53, 0.98, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.50, 0.50, 0.50, 1.00], + [0.40, 0.40, 0.40, 1.00], + [0.30, 0.30, 0.30, 1.00], + [0.10, 0.10, 0.10, 1.00], + [0.10, 0.10, 0.10, 0.60], + [0.10, 0.10, 0.10, 0.90], + [0.26, 0.59, 0.98, 0.31], + [0.26, 0.59, 0.98, 0.80], + [0.26, 0.59, 0.98, 1.00], + [0.85, 0.85, 0.85, 0.97], + [0.26, 0.59, 0.98, 1.00], + [0.30, 0.40, 0.40, 0.50], + [1.00, 1.00, 1.00, 0.00], + [0.10, 0.10, 0.10, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.90, 0.70, 0.00, 1.00], + [0.26, 0.59, 0.98, 0.40], + [0.74, 0.74, 0.74, 1.00], + [0.81, 0.81, 0.81, 1.00], + [1.00, 1.00, 1.00, 0.00], + [0.10, 0.10, 0.10, 0.06], + [0.26, 0.59, 0.98, 0.35], + [0.80, 0.20, 0.20, 1.00], + [0.26, 0.59, 0.98, 1.00], + [0.70, 0.70, 0.70, 0.56], + [0.80, 0.80, 0.80, 0.35], + [0.80, 0.80, 0.80, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json index 7286f71523..1214ac3f1d 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -1,14 +1,100 @@ { "DisplayName": "Mystic Purple", - "Description": "Magical purple theme with mystical vibes for spell crafting", - "Version": "1.0.0", + "Description": "Magical purple theme with mystical vibes perfect for spell crafting and enchanting", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 27.8, + "GlobalScale": 0.08, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.55, "Palette": { "Background": [0.2, 0.1, 0.3, 0.9], "Text": [0.95, 0.9, 1.0, 1.0], "Border": [0.6, 0.4, 0.8, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.4, 0.3, 0.5, 1.0], + "Error": [1.0, 0.3, 0.6, 1.0], + "Warning": [1.0, 0.7, 0.4, 1.0], + "RestartNeeded": [0.6, 0.9, 0.7, 1.0], + "CurrentHotkey": [0.9, 0.8, 1.0, 1.0], + "SuccessColor": [0.7, 0.3, 0.9, 1.0], + "InfoColor": [0.8, 0.6, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.7, 0.95, 1.0], + "ColorHovered": [0.9, 0.8, 1.0, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 2.5, + "ChildBorderSize": 1.5, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 7.0, + "IndentSpacing": 10.0, + "FramePadding": [9.0, 6.0], + "CellPadding": [15.0, 6.0], + "ItemSpacing": [10.0, 9.0] + }, + "FullPalette": [ + [0.95, 0.90, 1.00, 0.90], + [0.70, 0.60, 0.85, 1.00], + [0.20, 0.10, 0.30, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.18, 0.08, 0.25, 0.85], + [0.60, 0.50, 0.75, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.10, 0.05, 0.15, 1.00], + [0.35, 0.20, 0.50, 0.40], + [0.45, 0.25, 0.60, 0.45], + [0.10, 0.05, 0.15, 0.83], + [0.15, 0.08, 0.20, 0.87], + [0.25, 0.15, 0.40, 0.90], + [0.18, 0.10, 0.28, 0.90], + [0.30, 0.18, 0.45, 0.90], + [0.40, 0.25, 0.55, 1.00], + [0.50, 0.35, 0.65, 1.00], + [0.60, 0.45, 0.75, 1.00], + [0.95, 0.90, 1.00, 1.00], + [0.75, 0.65, 0.90, 1.00], + [0.35, 0.20, 0.50, 1.00], + [0.60, 0.40, 0.80, 0.40], + [0.70, 0.50, 0.90, 0.67], + [0.80, 0.60, 1.00, 1.00], + [0.65, 0.35, 0.95, 1.00], + [0.75, 0.45, 1.00, 1.00], + [0.85, 0.55, 1.00, 1.00], + [0.55, 0.40, 0.70, 1.00], + [0.65, 0.50, 0.80, 1.00], + [0.80, 0.65, 0.95, 1.00], + [0.95, 0.90, 1.00, 1.00], + [0.95, 0.90, 1.00, 0.60], + [0.95, 0.90, 1.00, 0.90], + [0.60, 0.40, 0.80, 0.31], + [0.70, 0.50, 0.90, 0.80], + [0.80, 0.60, 1.00, 1.00], + [0.15, 0.08, 0.22, 0.97], + [0.70, 0.45, 0.85, 1.00], + [0.60, 0.45, 0.75, 0.50], + [0.00, 0.00, 0.00, 0.00], + [0.95, 0.90, 1.00, 1.00], + [0.90, 0.70, 1.00, 1.00], + [0.90, 0.70, 1.00, 1.00], + [0.90, 0.70, 1.00, 1.00], + [0.60, 0.40, 0.80, 0.40], + [0.30, 0.18, 0.40, 1.00], + [0.25, 0.15, 0.35, 1.00], + [0.00, 0.00, 0.00, 0.00], + [0.95, 0.90, 1.00, 0.06], + [0.60, 0.40, 0.80, 0.35], + [0.90, 0.40, 0.60, 1.00], + [0.70, 0.50, 0.90, 1.00], + [0.40, 0.25, 0.55, 0.56], + [0.28, 0.18, 0.40, 0.35], + [0.28, 0.18, 0.40, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index 84c53acc56..9ef7af1325 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -1,14 +1,100 @@ { "DisplayName": "Nordic Frost", - "Description": "Cool blue-white theme reflecting the harsh Nordic climate", - "Version": "1.0.0", + "Description": "Cool blue-white theme reflecting the harsh Nordic climate and icy mountain peaks", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 26.0, + "GlobalScale": -0.08, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.4, "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], "Border": [0.6, 0.8, 1.0, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.4, 0.45, 0.5, 1.0], + "Error": [1.0, 0.3, 0.4, 1.0], + "Warning": [1.0, 0.8, 0.3, 1.0], + "RestartNeeded": [0.7, 0.9, 0.4, 1.0], + "CurrentHotkey": [0.5, 0.8, 1.0, 1.0], + "SuccessColor": [0.4, 0.8, 0.6, 1.0], + "InfoColor": [0.6, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.85, 0.95, 1.0], + "ColorHovered": [0.85, 0.95, 1.0, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 1.5, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 10.0], + "WindowRounding": 8.0, + "IndentSpacing": 6.0, + "FramePadding": [6.0, 4.0], + "CellPadding": [10.0, 4.0], + "ItemSpacing": [6.0, 6.0] + }, + "FullPalette": [ + [0.90, 0.95, 1.00, 0.90], + [0.70, 0.80, 0.90, 1.00], + [0.05, 0.15, 0.25, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.04, 0.12, 0.20, 0.85], + [0.50, 0.65, 0.80, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.03, 0.08, 0.15, 1.00], + [0.25, 0.35, 0.50, 0.40], + [0.30, 0.40, 0.55, 0.45], + [0.03, 0.08, 0.15, 0.83], + [0.05, 0.10, 0.18, 0.87], + [0.15, 0.25, 0.40, 0.90], + [0.08, 0.18, 0.28, 0.90], + [0.20, 0.30, 0.45, 0.90], + [0.30, 0.40, 0.55, 1.00], + [0.40, 0.50, 0.65, 1.00], + [0.50, 0.60, 0.75, 1.00], + [0.90, 0.95, 1.00, 1.00], + [0.70, 0.80, 0.90, 1.00], + [0.25, 0.35, 0.50, 1.00], + [0.50, 0.70, 0.90, 0.40], + [0.60, 0.75, 0.95, 0.67], + [0.70, 0.85, 1.00, 1.00], + [0.60, 0.80, 1.00, 1.00], + [0.70, 0.85, 1.00, 1.00], + [0.80, 0.90, 1.00, 1.00], + [0.50, 0.65, 0.80, 1.00], + [0.60, 0.75, 0.90, 1.00], + [0.80, 0.90, 1.00, 1.00], + [0.90, 0.95, 1.00, 1.00], + [0.90, 0.95, 1.00, 0.60], + [0.90, 0.95, 1.00, 0.90], + [0.50, 0.70, 0.90, 0.31], + [0.60, 0.75, 0.95, 0.80], + [0.70, 0.85, 1.00, 1.00], + [0.04, 0.12, 0.20, 0.97], + [0.65, 0.80, 0.95, 1.00], + [0.50, 0.65, 0.80, 0.50], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.95, 1.00, 1.00], + [0.50, 0.80, 1.00, 1.00], + [0.50, 0.80, 1.00, 1.00], + [0.50, 0.80, 1.00, 1.00], + [0.50, 0.70, 0.90, 0.40], + [0.20, 0.30, 0.45, 1.00], + [0.15, 0.25, 0.35, 1.00], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.95, 1.00, 0.06], + [0.50, 0.70, 0.90, 0.35], + [0.60, 0.40, 0.80, 1.00], + [0.60, 0.75, 0.95, 1.00], + [0.30, 0.40, 0.55, 0.56], + [0.20, 0.30, 0.45, 0.35], + [0.20, 0.30, 0.45, 0.35] + ] } } \ No newline at end of file diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json index dbeeb6d541..9986c66128 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -1,14 +1,100 @@ { "DisplayName": "Ocean Blue", - "Description": "Cool blue tones inspired by deep ocean waters", - "Version": "1.0.0", + "Description": "Cool blue tones inspired by deep ocean waters and maritime adventures", + "Version": "2.0.0", "Author": "Community Shaders Team", "Theme": { - "UseSimplePalette": true, + "UseSimplePalette": false, + "FontSize": 27.5, + "GlobalScale": 0.0, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.45, "Palette": { "Background": [0.1, 0.2, 0.4, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], "Border": [0.3, 0.5, 0.8, 0.8] - } + }, + "StatusPalette": { + "Disable": [0.4, 0.45, 0.5, 1.0], + "Error": [1.0, 0.4, 0.5, 1.0], + "Warning": [1.0, 0.8, 0.3, 1.0], + "RestartNeeded": [0.3, 0.8, 0.6, 1.0], + "CurrentHotkey": [0.6, 0.9, 1.0, 1.0], + "SuccessColor": [0.2, 0.7, 0.9, 1.0], + "InfoColor": [0.4, 0.7, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.85, 1.0, 1.0], + "ColorHovered": [0.8, 0.9, 1.0, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [15.0, 13.0], + "WindowRounding": 5.0, + "IndentSpacing": 8.5, + "FramePadding": [8.0, 5.0], + "CellPadding": [15.0, 4.0], + "ItemSpacing": [9.0, 8.0] + }, + "FullPalette": [ + [0.90, 0.95, 1.00, 0.90], + [0.60, 0.70, 0.85, 1.00], + [0.10, 0.20, 0.40, 0.90], + [0.00, 0.00, 0.00, 0.00], + [0.08, 0.16, 0.32, 0.85], + [0.50, 0.60, 0.80, 0.65], + [0.00, 0.00, 0.00, 0.00], + [0.05, 0.10, 0.20, 1.00], + [0.20, 0.30, 0.50, 0.40], + [0.30, 0.40, 0.60, 0.45], + [0.05, 0.10, 0.20, 0.83], + [0.08, 0.15, 0.25, 0.87], + [0.15, 0.25, 0.45, 0.90], + [0.10, 0.18, 0.35, 0.90], + [0.18, 0.28, 0.48, 0.90], + [0.25, 0.35, 0.55, 1.00], + [0.35, 0.45, 0.65, 1.00], + [0.45, 0.55, 0.75, 1.00], + [0.90, 0.95, 1.00, 1.00], + [0.70, 0.80, 0.90, 1.00], + [0.20, 0.30, 0.50, 1.00], + [0.30, 0.50, 0.80, 0.40], + [0.40, 0.60, 0.90, 0.67], + [0.50, 0.70, 1.00, 1.00], + [0.25, 0.55, 0.95, 1.00], + [0.35, 0.65, 1.00, 1.00], + [0.45, 0.75, 1.00, 1.00], + [0.40, 0.50, 0.70, 1.00], + [0.50, 0.60, 0.80, 1.00], + [0.70, 0.80, 0.95, 1.00], + [0.90, 0.95, 1.00, 1.00], + [0.90, 0.95, 1.00, 0.60], + [0.90, 0.95, 1.00, 0.90], + [0.30, 0.50, 0.80, 0.31], + [0.40, 0.60, 0.90, 0.80], + [0.50, 0.70, 1.00, 1.00], + [0.12, 0.18, 0.30, 0.97], + [0.35, 0.55, 0.85, 1.00], + [0.50, 0.60, 0.80, 0.50], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.95, 1.00, 1.00], + [0.60, 0.80, 1.00, 1.00], + [0.60, 0.80, 1.00, 1.00], + [0.60, 0.80, 1.00, 1.00], + [0.30, 0.50, 0.80, 0.40], + [0.20, 0.25, 0.40, 1.00], + [0.15, 0.20, 0.35, 1.00], + [0.00, 0.00, 0.00, 0.00], + [0.90, 0.95, 1.00, 0.06], + [0.30, 0.50, 0.80, 0.35], + [0.80, 0.40, 0.50, 1.00], + [0.40, 0.60, 0.90, 1.00], + [0.25, 0.35, 0.55, 0.56], + [0.18, 0.25, 0.40, 0.35], + [0.18, 0.25, 0.40, 0.35] + ] } } \ No newline at end of file diff --git a/src/Menu.cpp b/src/Menu.cpp index d8456d0c8f..eb9ccd8ff1 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -28,10 +28,9 @@ #include "Menu/MenuHeaderRenderer.h" #include "Menu/OverlayRenderer.h" #include "Menu/SettingsTabRenderer.h" -#include "Menu/ThemeManager.h" #include "ShaderCache.h" #include "State.h" -#include "ThemeManager.h" +#include "Menu/ThemeManager.h" #include "TruePBR.h" #include "Util.h" #include "Util.h" diff --git a/src/Menu.h b/src/Menu.h index fa3c0a6cd8..aa7baf06db 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -110,7 +110,7 @@ class Menu float FontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential - bool UseSimplePalette = true; // simple palette or full customization + bool UseSimplePalette = true; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds struct PaletteColors diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 7923d1b9ae..78cad24f4e 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -267,7 +267,7 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const std::string& label) if (label == "Unloaded Features") { Util::DrawSectionHeader(label.c_str(), true); } else { - // Use default separator text for other labels + // Use default separator text for other labels - should be themed via ImGuiCol_Separator ImGui::SeparatorText(label.c_str()); } } diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index a5825db925..3d05825d50 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -12,7 +12,6 @@ #include "Feature.h" #include "Globals.h" #include "Menu.h" -#include "Menu/ThemeManager.h" #include "Plugin.h" #include "SettingsOverrideManager.h" #include "State.h" diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 77a75bd9a7..15448ad955 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -144,8 +144,8 @@ void SettingsTabRenderer::RenderInterfaceTab() { if (ImGui::BeginTabItem("Interface")) { if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { - RenderUIOptionsTab(); - RenderSizesTab(); + RenderThemesTab(); + RenderStylingTab(); RenderColorsTab(); ImGui::EndTabBar(); } @@ -153,11 +153,87 @@ void SettingsTabRenderer::RenderInterfaceTab() } } -void SettingsTabRenderer::RenderUIOptionsTab() +void SettingsTabRenderer::RenderThemesTab() { - if (ImGui::BeginTabItem("UI Options")) { + if (ImGui::BeginTabItem("Themes")) { auto& themeSettings = globals::menu->GetSettings().Theme; + // Theme Preset Selection + ImGui::SeparatorText("Theme Preset"); + + // Get theme manager + auto themeManager = ThemeManager::GetSingleton(); + + // Get available themes (force discovery if not done) + if (!themeManager->IsDiscovered()) { + themeManager->DiscoverThemes(); + } + + const auto& themes = themeManager->GetThemes(); + + // Create dropdown items - using static storage to avoid dangling pointers + static std::vector displayNames; + static std::vector items; + + // Clear and rebuild the lists + displayNames.clear(); + items.clear(); + + displayNames.push_back("Custom"); // First item for custom theme + items.push_back(displayNames.back().c_str()); + + for (const auto& theme : themes) { + displayNames.push_back(theme.displayName); + items.push_back(displayNames.back().c_str()); + } + + // Find current selection index + int currentItem = 0; // Default to "Custom" + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + for (size_t i = 0; i < themes.size(); ++i) { + if (themes[i].name == globals::menu->GetSettings().SelectedThemePreset) { + currentItem = static_cast(i) + 1; // +1 for "Custom" + break; + } + } + } + + // Theme preset dropdown + if (ImGui::Combo("##ThemePreset", ¤tItem, items.data(), static_cast(items.size()))) { + if (currentItem == 0) { + // Custom theme selected + globals::menu->GetSettings().SelectedThemePreset = ""; + } else { + // Preset theme selected + std::string selectedTheme = themes[currentItem - 1].name; // -1 for "Custom" offset + if (globals::menu->LoadThemePreset(selectedTheme)) { + // Theme loaded successfully, update UI + themeSettings = globals::menu->GetSettings().Theme; + } + } + } + // Show theme description as tooltip + if (currentItem > 0 && currentItem - 1 < static_cast(themes.size())) { + const auto& selectedTheme = themes[currentItem - 1]; + if (!selectedTheme.description.empty()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", selectedTheme.description.c_str()); + } + } + } + + ImGui::SameLine(); + if (ImGui::Button("Refresh Themes")) { + themeManager->RefreshThemes(); + // Reset selection if current theme no longer exists + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); + if (!themeInfo) { + globals::menu->GetSettings().SelectedThemePreset = ""; + } + } + } + ImGui::SeparatorText("UI Elements"); ImGui::Checkbox("Use Icon Buttons in Header", &themeSettings.ShowActionIcons); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -175,9 +251,9 @@ void SettingsTabRenderer::RenderUIOptionsTab() } } -void SettingsTabRenderer::RenderSizesTab() +void SettingsTabRenderer::RenderStylingTab() { - if (ImGui::BeginTabItem("Sizes")) { + if (ImGui::BeginTabItem("Styling")) { auto& themeSettings = globals::menu->GetSettings().Theme; auto& style = themeSettings.Style; @@ -244,77 +320,6 @@ void SettingsTabRenderer::RenderColorsTab() auto& themeSettings = globals::menu->GetSettings().Theme; auto& colors = themeSettings.FullPalette; - // Theme Preset Selection - ImGui::SeparatorText("Theme Preset"); - - // Get theme manager - auto themeManager = ThemeManager::GetSingleton(); - - // Get available themes (force discovery if not done) - if (!themeManager->IsDiscovered()) { - themeManager->DiscoverThemes(); - } - - const auto& themes = themeManager->GetThemes(); - - // Create dropdown items - std::vector items; - std::vector displayNames; - items.push_back("Custom"); // First item for custom theme - displayNames.push_back("Custom"); - - for (const auto& theme : themes) { - displayNames.push_back(theme.displayName); - items.push_back(displayNames.back().c_str()); - } - - // Find current selection index - int currentItem = 0; // Default to "Custom" - if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { - for (size_t i = 0; i < themes.size(); ++i) { - if (themes[i].name == globals::menu->GetSettings().SelectedThemePreset) { - currentItem = static_cast(i) + 1; // +1 for "Custom" - break; - } - } - } - - // Theme preset dropdown - if (ImGui::Combo("##ThemePreset", ¤tItem, items.data(), static_cast(items.size()))) { - if (currentItem == 0) { - // Custom theme selected - globals::menu->GetSettings().SelectedThemePreset = ""; - } else { - // Preset theme selected - std::string selectedTheme = themes[currentItem - 1].name; // -1 for "Custom" offset - if (globals::menu->LoadThemePreset(selectedTheme)) { - // Theme loaded successfully, update UI - themeSettings = globals::menu->GetSettings().Theme; - } - } - } - - ImGui::SameLine(); - if (ImGui::Button("Refresh Themes")) { - themeManager->RefreshThemes(); - // Reset selection if current theme no longer exists - if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { - const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); - if (!themeInfo) { - globals::menu->GetSettings().SelectedThemePreset = ""; - } - } - } - - // Show theme description if available - if (currentItem > 0 && currentItem - 1 < static_cast(themes.size())) { - const auto& selectedTheme = themes[currentItem - 1]; - if (!selectedTheme.description.empty()) { - ImGui::SameLine(); - ImGui::Text("- %s", selectedTheme.description.c_str()); - } - } - ImGui::SeparatorText("Status"); ImGui::ColorEdit4("Disabled Text", (float*)&themeSettings.StatusPalette.Disable); @@ -333,17 +338,17 @@ void SettingsTabRenderer::RenderColorsTab() ImGui::SeparatorText("Palette"); - if (ImGui::RadioButton("Simple Palette", themeSettings.UseSimplePalette)) - themeSettings.UseSimplePalette = true; - ImGui::SameLine(); - if (ImGui::RadioButton("Full Palette", !themeSettings.UseSimplePalette)) - themeSettings.UseSimplePalette = false; - - if (themeSettings.UseSimplePalette) { + // Simple Colors Section - collapsed by default for clean interface + if (ImGui::CollapsingHeader("Simple", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit4("Background", (float*)&themeSettings.Palette.Background); ImGui::ColorEdit4("Text", (float*)&themeSettings.Palette.Text); ImGui::ColorEdit4("Border", (float*)&themeSettings.Palette.Border); - } else { + } + + // Advanced Colors Section - collapsed by default to avoid overwhelming users + if (ImGui::CollapsingHeader("Advanced")) { + ImGui::TextWrapped("Advanced color controls for detailed customization of all UI elements."); + static ImGuiTextFilter filter; filter.Draw("Filter colors", ImGui::GetFontSize() * 16); diff --git a/src/Menu/SettingsTabRenderer.h b/src/Menu/SettingsTabRenderer.h index f4d6ce67da..611144ee55 100644 --- a/src/Menu/SettingsTabRenderer.h +++ b/src/Menu/SettingsTabRenderer.h @@ -29,7 +29,7 @@ class SettingsTabRenderer static void RenderInterfaceTab(); // Interface sub-tabs - static void RenderUIOptionsTab(); - static void RenderSizesTab(); + static void RenderThemesTab(); + static void RenderStylingTab(); static void RenderColorsTab(); }; \ No newline at end of file diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 787899ba3c..ea6e944ed1 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -3,11 +3,37 @@ #include #include +#include +#include +#include + #include #include "RE/Skyrim.h" -#include "Util.h" +#include "../Utils/FileSystem.h" +#include "../Util.h" + +using namespace SKSE; + +namespace +{ + /** + * @brief Gets file modification time + */ + std::time_t GetFileModTime(const std::filesystem::path& filePath) + { + try { + auto fileTime = std::filesystem::last_write_time(filePath); + auto systemTime = std::chrono::time_point_cast( + fileTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::system_clock::to_time_t(systemTime); + } catch (...) { + return 0; + } + } +} +// Static UI helper methods void ThemeManager::SetupImGuiStyle(const Menu& menu) { auto& style = ImGui::GetStyle(); @@ -23,122 +49,314 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) style = styleCopy; style.HoverDelayNormal = themeSettings.TooltipHoverDelay; - if (themeSettings.UseSimplePalette) { - float hoveredAlpha{ 0.1f }; + // Always use the unified FullPalette system instead of switching between simple/full + // This ensures consistent behavior regardless of UI presentation mode + for (size_t i = 0; i < std::min(themeSettings.FullPalette.size(), static_cast(ImGuiCol_COUNT)); ++i) { + colors[i] = themeSettings.FullPalette[i]; + } + + // Apply simple palette overrides to the FullPalette for key colors + // This allows the simple palette controls to work by updating the FullPalette + colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; + colors[ImGuiCol_Text] = themeSettings.Palette.Text; + colors[ImGuiCol_Border] = themeSettings.Palette.Border; + colors[ImGuiCol_Separator] = themeSettings.Palette.Border; + colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.Border; + + // Apply derived colors based on simple palette + ImVec4 textDisabled = themeSettings.Palette.Text; + textDisabled.w = 0.3f; + colors[ImGuiCol_TextDisabled] = textDisabled; + + ImVec4 resizeGripHovered = themeSettings.Palette.Border; + resizeGripHovered.w = 0.1f; + colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; + colors[ImGuiCol_ResizeGripActive] = resizeGripHovered; +} - ImVec4 resizeGripHovered = themeSettings.Palette.Border; - resizeGripHovered.w = hoveredAlpha; +void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) +{ + auto& themeSettings = menu.GetTheme(); - ImVec4 textDisabled = themeSettings.Palette.Text; - textDisabled.w = 0.3f; + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); - ImVec4 header{ 1.0f, 1.0f, 1.0f, 0.15f }; - ImVec4 headerHovered = header; - headerHovered.w = hoveredAlpha; + ImFontConfig font_config; - ImVec4 tabHovered{ 0.2f, 0.2f, 0.2f, 1.0f }; + font_config.OversampleH = Constants::FCONF_OVERSAMPLE_H; + font_config.OversampleV = Constants::FCONF_OVERSAMPLE_V; + font_config.PixelSnapH = Constants::FCONF_PIXELSNAP_H; + font_config.RasterizerMultiply = Constants::FCONF_RASTERIZER_MULTIPLY; - ImVec4 sliderGrab{ 1.0f, 1.0f, 1.0f, 0.245f }; - ImVec4 sliderGrabActive{ 1.0f, 1.0f, 1.0f, 0.531f }; + float fontSize = themeSettings.FontSize; + fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); - ImVec4 scrollbarGrab{ 0.31f, 0.31f, 0.31f, 1.0f }; - ImVec4 scrollbarGrabHovered{ 0.41f, 0.41f, 0.41f, 1.0f }; - ImVec4 scrollbarGrabActive{ 0.51f, 0.51f, 0.51f, 1.0f }; + auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; + if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), + std::round(fontSize), &font_config)) { + logger::warn("ThemeManager::ReloadFont() - Failed to load custom font. Using default font."); + io.Fonts->AddFontDefault(); + } - colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; - colors[ImGuiCol_ChildBg] = ImVec4(); - colors[ImGuiCol_ScrollbarBg] = ImVec4(); - colors[ImGuiCol_TableHeaderBg] = ImVec4(); - colors[ImGuiCol_TableRowBg] = ImVec4(); - colors[ImGuiCol_TableRowBgAlt] = ImVec4(); + io.Fonts->Build(); - colors[ImGuiCol_Border] = themeSettings.Palette.Border; - colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; - colors[ImGuiCol_ResizeGrip] = colors[ImGuiCol_Border]; - colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; - colors[ImGuiCol_ResizeGripActive] = colors[ImGuiCol_ResizeGripHovered]; + ImGui_ImplDX11_InvalidateDeviceObjects(); - colors[ImGuiCol_Text] = themeSettings.Palette.Text; - colors[ImGuiCol_TextDisabled] = textDisabled; + io.FontGlobalScale = exp2(themeSettings.GlobalScale); - colors[ImGuiCol_FrameBg] = themeSettings.Palette.Background; - colors[ImGuiCol_FrameBgHovered] = headerHovered; - colors[ImGuiCol_FrameBgActive] = colors[ImGuiCol_FrameBg]; + cachedFontSize = themeSettings.FontSize; +} - colors[ImGuiCol_DockingEmptyBg] = themeSettings.Palette.Border; - colors[ImGuiCol_DockingPreview] = themeSettings.Palette.Border; +// Theme management methods +size_t ThemeManager::DiscoverThemes() +{ + if (discovered) { + return themes.size(); + } - colors[ImGuiCol_PlotHistogram] = themeSettings.Palette.Border; + themes.clear(); - colors[ImGuiCol_SliderGrab] = sliderGrab; - colors[ImGuiCol_SliderGrabActive] = sliderGrabActive; + auto themesDir = GetThemesDirectory(); + if (!std::filesystem::exists(themesDir)) { + logger::info("Themes directory does not exist: {}", themesDir.string()); + discovered = true; + return 0; + } - colors[ImGuiCol_Header] = header; - colors[ImGuiCol_HeaderActive] = colors[ImGuiCol_Header]; - colors[ImGuiCol_HeaderHovered] = headerHovered; + logger::info("Discovering themes in: {}", themesDir.string()); + + try { + for (const auto& entry : std::filesystem::directory_iterator(themesDir)) { + if (!entry.is_regular_file() || entry.path().extension() != ".json") { + continue; + } + + // Check file size + auto fileSize = entry.file_size(); + if (fileSize > MAX_FILE_SIZE) { + logger::warn("Theme file too large, skipping: {} ({}MB)", + entry.path().filename().string(), fileSize / (1024 * 1024)); + continue; + } + + if (themes.size() >= MAX_THEMES) { + logger::warn("Maximum number of themes ({}) reached, skipping remaining files", MAX_THEMES); + break; + } + + auto themeInfo = LoadThemeFile(entry.path()); + if (themeInfo && themeInfo->isValid) { + themes.push_back(std::move(*themeInfo)); + logger::info("Discovered theme: {} ({})", themes.back().name, themes.back().displayName); + } + } + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error discovering themes: {}", e.what()); + } - colors[ImGuiCol_Button] = ImVec4(); - colors[ImGuiCol_ButtonHovered] = headerHovered; - colors[ImGuiCol_ButtonActive] = ImVec4(); + // Sort themes alphabetically by display name + std::sort(themes.begin(), themes.end(), [](const ThemeInfo& a, const ThemeInfo& b) { + return a.displayName < b.displayName; + }); - colors[ImGuiCol_ScrollbarGrab] = scrollbarGrab; - colors[ImGuiCol_ScrollbarGrabHovered] = scrollbarGrabHovered; - colors[ImGuiCol_ScrollbarGrabActive] = scrollbarGrabActive; + discovered = true; + logger::info("Theme discovery complete. Found {} themes", themes.size()); + return themes.size(); +} - colors[ImGuiCol_TitleBg] = themeSettings.Palette.Background; - colors[ImGuiCol_TitleBgActive] = colors[ImGuiCol_TitleBg]; - colors[ImGuiCol_TitleBgCollapsed] = colors[ImGuiCol_TitleBg]; +std::vector ThemeManager::GetThemeNames() const +{ + std::vector names; + names.reserve(themes.size()); + + for (const auto& theme : themes) { + names.push_back(theme.name); + } + + return names; +} - colors[ImGuiCol_MenuBarBg] = colors[ImGuiCol_TitleBg]; +bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) +{ + if (!discovered) { + DiscoverThemes(); + } - colors[ImGuiCol_CheckMark] = themeSettings.Palette.Text; + if (themeName.empty()) { + // Empty theme name means use current/custom theme + return true; + } - colors[ImGuiCol_Tab] = themeSettings.FullPalette[ImGuiCol_Tab]; - colors[ImGuiCol_TabActive] = themeSettings.FullPalette[ImGuiCol_TabActive]; - colors[ImGuiCol_TabHovered] = tabHovered; - colors[ImGuiCol_TabUnfocused] = themeSettings.FullPalette[ImGuiCol_TabUnfocused]; - colors[ImGuiCol_TabUnfocusedActive] = themeSettings.FullPalette[ImGuiCol_TabUnfocusedActive]; + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - colors[ImGuiCol_PopupBg] = themeSettings.Palette.Background; + if (it == themes.end()) { + logger::warn("Theme not found: {}", themeName); + return false; + } - colors[ImGuiCol_TableBorderStrong] = colors[ImGuiCol_Border]; - colors[ImGuiCol_TableBorderLight] = colors[ImGuiCol_Border]; + if (!it->isValid) { + logger::warn("Theme is invalid: {}", themeName); + return false; + } - colors[ImGuiCol_TextSelectedBg] = header; - } else { - std::copy(themeSettings.FullPalette.begin(), themeSettings.FullPalette.end(), std::span(colors).begin()); + try { + if (it->themeData.contains("Theme") && it->themeData["Theme"].is_object()) { + themeSettings = it->themeData["Theme"]; + logger::info("Loaded theme: {} ({})", it->name, it->displayName); + return true; + } else { + logger::warn("Theme file missing 'Theme' object: {}", themeName); + return false; + } + } catch (const std::exception& e) { + logger::warn("Error loading theme {}: {}", themeName, e.what()); + return false; } } -void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) +const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const { - auto& themeSettings = menu.GetTheme(); + auto it = std::find_if(themes.begin(), themes.end(), + [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); + + return (it != themes.end()) ? &(*it) : nullptr; +} - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->Clear(); +void ThemeManager::RefreshThemes() +{ + discovered = false; + DiscoverThemes(); +} - ImFontConfig font_config; +std::filesystem::path ThemeManager::GetThemesDirectory() const +{ + return Util::PathHelpers::GetThemesPath(); +} - font_config.OversampleH = Constants::FCONF_OVERSAMPLE_H; - font_config.OversampleV = Constants::FCONF_OVERSAMPLE_V; - font_config.PixelSnapH = Constants::FCONF_PIXELSNAP_H; - font_config.RasterizerMultiply = Constants::FCONF_RASTERIZER_MULTIPLY; +void ThemeManager::CreateDefaultThemeFiles() +{ + auto themesDir = GetThemesDirectory(); + + try { + std::filesystem::create_directories(themesDir); + logger::info("Ensured themes directory exists: {}", themesDir.string()); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to create themes directory: {}", e.what()); + return; + } - float fontSize = themeSettings.FontSize; - fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + // Check if any theme files exist - if so, use those instead of creating defaults + bool hasThemes = false; + try { + for (const auto& entry : std::filesystem::directory_iterator(themesDir)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + hasThemes = true; + break; + } + } + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to check for existing themes: {}", e.what()); + } - auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; - if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), - std::round(fontSize), &font_config)) { - logger::warn("ThemeManager::ReloadFont() - Failed to load custom font. Using default font."); - io.Fonts->AddFontDefault(); + if (hasThemes) { + logger::info("Theme files already exist, skipping default creation"); + return; } - io.Fonts->Build(); + // Only create a minimal default theme if no themes exist at all (rare fallback) + auto defaultThemeFile = themesDir / "Default.json"; + try { + std::ofstream file(defaultThemeFile); + if (!file.is_open()) { + logger::warn("Failed to create default theme file: {}", defaultThemeFile.string()); + return; + } + + file << R"({ + "DisplayName": "Default Theme", + "Description": "Default community shaders theme", + "Version": "1.0", + "Author": "Community Shaders", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.05, 0.05, 0.05, 1.0], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.4, 0.4, 0.4, 1.0] + }, + "FontSize": 27.0, + "GlobalScale": 0.0, + "TooltipHoverDelay": 0.5 + } +})"; - ImGui_ImplDX11_InvalidateDeviceObjects(); + file.close(); + logger::info("Created default theme file: {}", defaultThemeFile.string()); + } catch (const std::exception& e) { + logger::warn("Failed to create default theme file: {}", e.what()); + } +} - io.FontGlobalScale = exp2(themeSettings.GlobalScale); +std::unique_ptr ThemeManager::LoadThemeFile(const std::filesystem::path& filePath) +{ + auto themeInfo = std::make_unique(); + themeInfo->name = filePath.stem().string(); + themeInfo->filePath = filePath.string(); + themeInfo->lastModified = GetFileModTime(filePath); + + try { + std::ifstream file(filePath); + if (!file.is_open()) { + logger::warn("Failed to open theme file: {}", filePath.string()); + return themeInfo; + } + + json data; + file >> data; + + if (!ValidateThemeData(data)) { + logger::warn("Invalid theme data in file: {}", filePath.string()); + return themeInfo; + } + + themeInfo->themeData = data; + + // Extract metadata + if (data.contains("DisplayName") && data["DisplayName"].is_string()) { + themeInfo->displayName = data["DisplayName"].get(); + } else { + themeInfo->displayName = themeInfo->name; + } + + if (data.contains("Description") && data["Description"].is_string()) { + themeInfo->description = data["Description"].get(); + } + + if (data.contains("Version") && data["Version"].is_string()) { + themeInfo->version = data["Version"].get(); + } + + if (data.contains("Author") && data["Author"].is_string()) { + themeInfo->author = data["Author"].get(); + } + + themeInfo->isValid = true; + + } catch (const std::exception& e) { + logger::warn("Error parsing theme file {}: {}", filePath.string(), e.what()); + } - cachedFontSize = themeSettings.FontSize; + return themeInfo; +} + +bool ThemeManager::ValidateThemeData(const json& themeData) const +{ + // Basic validation - ensure Theme object exists + if (!themeData.contains("Theme") || !themeData["Theme"].is_object()) { + return false; + } + + // Could add more detailed validation here if needed + return true; } \ No newline at end of file diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index e002719473..95d0f13e65 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -1,12 +1,40 @@ #pragma once +#include +#include +#include +#include #include +using json = nlohmann::json; + +/** + * @brief Manages hot-swappable theme system for Community Shaders + * + * This class handles discovery and loading of theme JSON files from the Themes directory, + * allowing users to create and modify themes without code changes. Similar to the + * SettingsOverrideManager but specifically for theme management. + * + * Theme files should be placed in: Data\SKSE\Plugins\CommunityShaders\Themes\ + * File format: {ThemeName}.json + */ class ThemeManager { public: - static void SetupImGuiStyle(const class Menu& menu); - static void ReloadFont(const class Menu& menu, float& cachedFontSize); + struct ThemeInfo + { + std::string name; // Filename without extension + std::string displayName; // Human-readable name from JSON + std::string description; // Theme description from JSON + std::string filePath; // Full path to theme file + json themeData; // Complete theme settings + bool isValid = false; // Whether theme loaded successfully + + // Metadata + std::string version; + std::string author; + std::time_t lastModified = 0; + }; struct Constants { @@ -43,4 +71,94 @@ class ThemeManager static constexpr float SEPARATOR_THICKNESS = 3.0f; static constexpr float UNDOCKED_ICON_ITEM_SPACING = 6.0f; }; + + static ThemeManager* GetSingleton() + { + static ThemeManager instance; + return &instance; + } + + // Static UI helper methods + static void SetupImGuiStyle(const class Menu& menu); + static void ReloadFont(const class Menu& menu, float& cachedFontSize); + + /** + * @brief Discovers all theme files in the themes directory + * @return Number of theme files discovered + */ + size_t DiscoverThemes(); + + /** + * @brief Gets list of all discovered themes + * @return Vector of theme information + */ + const std::vector& GetThemes() const { return themes; } + + /** + * @brief Gets theme names for dropdown display + * @return Vector of theme names + */ + std::vector GetThemeNames() const; + + /** + * @brief Loads a specific theme by name + * @param themeName Name of the theme to load + * @param themeSettings Output parameter for loaded theme settings + * @return True if theme was loaded successfully + */ + bool LoadTheme(const std::string& themeName, json& themeSettings); + + /** + * @brief Gets theme info by name + * @param themeName Name of the theme + * @return Pointer to theme info or nullptr if not found + */ + const ThemeInfo* GetThemeInfo(const std::string& themeName) const; + + /** + * @brief Refreshes theme discovery (for runtime updates) + */ + void RefreshThemes(); + + /** + * @brief Checks if themes have been discovered + */ + bool IsDiscovered() const { return discovered; } + + /** + * @brief Gets the themes directory path + */ + std::filesystem::path GetThemesDirectory() const; + + /** + * @brief Creates default theme files if they don't exist + */ + void CreateDefaultThemeFiles(); + +private: + ThemeManager() = default; + ~ThemeManager() = default; + ThemeManager(const ThemeManager&) = delete; + ThemeManager& operator=(const ThemeManager&) = delete; + + /** + * @brief Loads a single theme file + * @param filePath Path to the theme file + * @return Theme info if successful, nullptr otherwise + */ + std::unique_ptr LoadThemeFile(const std::filesystem::path& filePath); + + /** + * @brief Validates theme data structure + * @param themeData JSON data to validate + * @return True if theme data is valid + */ + bool ValidateThemeData(const json& themeData) const; + + std::vector themes; + bool discovered = false; + + // Constants + static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading + static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size }; \ No newline at end of file diff --git a/src/ThemeManager.cpp b/src/ThemeManager.cpp deleted file mode 100644 index 6f98b0f483..0000000000 --- a/src/ThemeManager.cpp +++ /dev/null @@ -1,384 +0,0 @@ -#include "ThemeManager.h" - -#include -#include -#include -#include - -#include "Utils/FileSystem.h" - -using namespace SKSE; - -namespace -{ - /** - * @brief Gets file modification time - */ - std::time_t GetFileModTime(const std::filesystem::path& filePath) - { - try { - auto fileTime = std::filesystem::last_write_time(filePath); - auto systemTime = std::chrono::time_point_cast( - fileTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); - return std::chrono::system_clock::to_time_t(systemTime); - } catch (...) { - return 0; - } - } -} - -size_t ThemeManager::DiscoverThemes() -{ - if (discovered) { - return themes.size(); - } - - themes.clear(); - - auto themesDir = GetThemesDirectory(); - if (!std::filesystem::exists(themesDir)) { - logger::info("Themes directory does not exist: {}", themesDir.string()); - discovered = true; - return 0; - } - - logger::info("Discovering themes in: {}", themesDir.string()); - - try { - for (const auto& entry : std::filesystem::directory_iterator(themesDir)) { - if (!entry.is_regular_file() || entry.path().extension() != ".json") { - continue; - } - - // Check file size - auto fileSize = entry.file_size(); - if (fileSize > MAX_FILE_SIZE) { - logger::warn("Theme file too large, skipping: {} ({}MB)", - entry.path().filename().string(), fileSize / (1024 * 1024)); - continue; - } - - if (themes.size() >= MAX_THEMES) { - logger::warn("Maximum number of themes ({}) reached, skipping remaining files", MAX_THEMES); - break; - } - - auto themeInfo = LoadThemeFile(entry.path()); - if (themeInfo && themeInfo->isValid) { - themes.push_back(std::move(*themeInfo)); - logger::info("Discovered theme: {} ({})", themes.back().name, themes.back().displayName); - } - } - } catch (const std::filesystem::filesystem_error& e) { - logger::warn("Error discovering themes: {}", e.what()); - } - - // Sort themes alphabetically by display name - std::sort(themes.begin(), themes.end(), [](const ThemeInfo& a, const ThemeInfo& b) { - return a.displayName < b.displayName; - }); - - discovered = true; - logger::info("Theme discovery complete. Found {} themes", themes.size()); - return themes.size(); -} - -std::vector ThemeManager::GetThemeNames() const -{ - std::vector names; - names.reserve(themes.size()); - - for (const auto& theme : themes) { - names.push_back(theme.name); - } - - return names; -} - -bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) -{ - if (!discovered) { - DiscoverThemes(); - } - - if (themeName.empty()) { - // Empty theme name means use current/custom theme - return true; - } - - auto it = std::find_if(themes.begin(), themes.end(), - [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - - if (it == themes.end()) { - logger::warn("Theme not found: {}", themeName); - return false; - } - - if (!it->isValid) { - logger::warn("Theme is invalid: {}", themeName); - return false; - } - - try { - if (it->themeData.contains("Theme") && it->themeData["Theme"].is_object()) { - themeSettings = it->themeData["Theme"]; - logger::info("Loaded theme: {} ({})", it->name, it->displayName); - return true; - } else { - logger::warn("Theme file missing 'Theme' object: {}", themeName); - return false; - } - } catch (const std::exception& e) { - logger::warn("Error loading theme {}: {}", themeName, e.what()); - return false; - } -} - -const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const -{ - auto it = std::find_if(themes.begin(), themes.end(), - [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - - return (it != themes.end()) ? &(*it) : nullptr; -} - -void ThemeManager::RefreshThemes() -{ - discovered = false; - DiscoverThemes(); -} - -std::filesystem::path ThemeManager::GetThemesDirectory() const -{ - return Util::PathHelpers::GetThemesPath(); -} - -void ThemeManager::CreateDefaultThemeFiles() -{ - auto themesDir = GetThemesDirectory(); - - try { - std::filesystem::create_directories(themesDir); - } catch (const std::filesystem::filesystem_error& e) { - logger::warn("Failed to create themes directory: {}", e.what()); - return; - } - - // Define default themes as JSON strings (what users would create) - struct DefaultTheme { - std::string name; - std::string content; - }; - - std::vector defaultThemes = { - {"Default", R"({ - "DisplayName": "Default Dark", - "Description": "The classic Community Shaders dark theme", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.09, 0.09, 0.09, 0.95], - "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [0.5, 0.5, 0.5, 0.8] - } - } -})"}, - - {"Light", R"({ - "DisplayName": "Light Mode", - "Description": "Clean light theme with dark text", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.9, 0.9, 0.9, 0.95], - "Text": [0.1, 0.1, 0.1, 1.0], - "Border": [0.3, 0.3, 0.3, 0.8] - } - } -})"}, - - {"Ocean", R"({ - "DisplayName": "Ocean Blue", - "Description": "Cool blue tones with subtle gradients", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.1, 0.2, 0.4, 0.9], - "Text": [0.9, 0.95, 1.0, 1.0], - "Border": [0.3, 0.5, 0.8, 0.8] - } - } -})"}, - - {"Forest", R"({ - "DisplayName": "Forest Green", - "Description": "Natural green theme inspired by Skyrim's forests", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.1, 0.3, 0.15, 0.9], - "Text": [0.9, 1.0, 0.9, 1.0], - "Border": [0.4, 0.7, 0.4, 0.8] - } - } -})"}, - - {"Mystic", R"({ - "DisplayName": "Mystic Purple", - "Description": "Magical purple theme with mystical vibes", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.2, 0.1, 0.3, 0.9], - "Text": [0.95, 0.9, 1.0, 1.0], - "Border": [0.6, 0.4, 0.8, 0.8] - } - } -})"}, - - {"Amber", R"({ - "DisplayName": "Warm Amber", - "Description": "Warm amber tones reminiscent of candlelight", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.2, 0.15, 0.05, 0.9], - "Text": [1.0, 0.9, 0.7, 1.0], - "Border": [0.8, 0.6, 0.3, 0.8] - } - } -})"}, - - {"HighContrast", R"({ - "DisplayName": "High Contrast", - "Description": "High contrast theme for accessibility", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.0, 0.0, 0.0, 0.95], - "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [1.0, 1.0, 1.0, 0.9] - } - } -})"}} - }; - - // Create theme files - for (const auto& theme : defaultThemes) { - auto themeFile = themesDir / (theme.name + ".json"); - - // Only create if it doesn't exist (don't overwrite user modifications) - if (std::filesystem::exists(themeFile)) { - continue; - } - - try { - std::ofstream file(themeFile); - if (!file.is_open()) { - logger::warn("Failed to create theme file: {}", themeFile.string()); - continue; - } - - file << theme.content; - file.close(); - - logger::info("Created default theme file: {}", theme.name); - } catch (const std::exception& e) { - logger::warn("Error creating theme file {}: {}", theme.name, e.what()); - } - } -} - -std::unique_ptr ThemeManager::LoadThemeFile(const std::filesystem::path& filePath) -{ - auto themeInfo = std::make_unique(); - themeInfo->name = filePath.stem().string(); - themeInfo->filePath = filePath.string(); - themeInfo->lastModified = GetFileModTime(filePath); - - try { - std::ifstream file(filePath); - if (!file.is_open()) { - logger::warn("Cannot open theme file: {}", filePath.string()); - return themeInfo; - } - - json themeJson; - file >> themeJson; - file.close(); - - if (!ValidateThemeData(themeJson)) { - logger::warn("Invalid theme data in: {}", filePath.string()); - return themeInfo; - } - - themeInfo->themeData = themeJson; - - // Extract metadata - if (themeJson.contains("DisplayName") && themeJson["DisplayName"].is_string()) { - themeInfo->displayName = themeJson["DisplayName"]; - } else { - themeInfo->displayName = themeInfo->name; // Fallback to filename - } - - if (themeJson.contains("Description") && themeJson["Description"].is_string()) { - themeInfo->description = themeJson["Description"]; - } - - if (themeJson.contains("Version") && themeJson["Version"].is_string()) { - themeInfo->version = themeJson["Version"]; - } - - if (themeJson.contains("Author") && themeJson["Author"].is_string()) { - themeInfo->author = themeJson["Author"]; - } - - themeInfo->isValid = true; - - } catch (const std::exception& e) { - logger::warn("Error loading theme file {}: {}", filePath.string(), e.what()); - } - - return themeInfo; -} - -bool ThemeManager::ValidateThemeData(const json& themeData) const -{ - try { - // Must have Theme object - if (!themeData.contains("Theme") || !themeData["Theme"].is_object()) { - return false; - } - - const auto& theme = themeData["Theme"]; - - // Basic validation - check for required structure - // This is a minimal check; the Menu system will handle detailed validation - if (theme.contains("UseSimplePalette") && theme["UseSimplePalette"].is_boolean()) { - if (theme["UseSimplePalette"] == true) { - // Simple palette should have Palette object - if (!theme.contains("Palette") || !theme["Palette"].is_object()) { - return false; - } - } - } - - return true; - } catch (...) { - return false; - } -} \ No newline at end of file diff --git a/src/ThemeManager.h b/src/ThemeManager.h deleted file mode 100644 index cae93c65dd..0000000000 --- a/src/ThemeManager.h +++ /dev/null @@ -1,123 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -using json = nlohmann::json; - -/** - * @brief Manages hot-swappable theme system for Community Shaders - * - * This class handles discovery and loading of theme JSON files from the Themes directory, - * allowing users to create and modify themes without code changes. Similar to the - * SettingsOverrideManager but specifically for theme management. - * - * Theme files should be placed in: Data\SKSE\Plugins\CommunityShaders\Themes\ - * File format: {ThemeName}.json - */ -class ThemeManager -{ -public: - struct ThemeInfo - { - std::string name; // Filename without extension - std::string displayName; // Human-readable name from JSON - std::string description; // Theme description from JSON - std::string filePath; // Full path to theme file - json themeData; // Complete theme settings - bool isValid = false; // Whether theme loaded successfully - - // Metadata - std::string version; - std::string author; - std::time_t lastModified = 0; - }; - - static ThemeManager* GetSingleton() - { - static ThemeManager instance; - return &instance; - } - - /** - * @brief Discovers all theme files in the themes directory - * @return Number of theme files discovered - */ - size_t DiscoverThemes(); - - /** - * @brief Gets list of all discovered themes - * @return Vector of theme information - */ - const std::vector& GetThemes() const { return themes; } - - /** - * @brief Gets theme names for dropdown display - * @return Vector of theme names - */ - std::vector GetThemeNames() const; - - /** - * @brief Loads a specific theme by name - * @param themeName Name of the theme to load - * @param themeSettings Output parameter for loaded theme settings - * @return True if theme was loaded successfully - */ - bool LoadTheme(const std::string& themeName, json& themeSettings); - - /** - * @brief Gets theme info by name - * @param themeName Name of the theme - * @return Pointer to theme info or nullptr if not found - */ - const ThemeInfo* GetThemeInfo(const std::string& themeName) const; - - /** - * @brief Refreshes theme discovery (for runtime updates) - */ - void RefreshThemes(); - - /** - * @brief Checks if themes have been discovered - */ - bool IsDiscovered() const { return discovered; } - - /** - * @brief Gets the themes directory path - */ - std::filesystem::path GetThemesDirectory() const; - - /** - * @brief Creates default theme files if they don't exist - */ - void CreateDefaultThemeFiles(); - -private: - ThemeManager() = default; - ~ThemeManager() = default; - ThemeManager(const ThemeManager&) = delete; - ThemeManager& operator=(const ThemeManager&) = delete; - - /** - * @brief Loads a single theme file - * @param filePath Path to the theme file - * @return Theme info if successful, nullptr otherwise - */ - std::unique_ptr LoadThemeFile(const std::filesystem::path& filePath); - - /** - * @brief Validates theme data structure - * @param themeData JSON data to validate - * @return True if theme data is valid - */ - bool ValidateThemeData(const json& themeData) const; - - std::vector themes; - bool discovered = false; - - // Constants - static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading - static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size -}; \ No newline at end of file diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index fff78789a3..e9ba3c9264 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -189,26 +189,6 @@ namespace Util } } -std::vector> Util::EnumerateDllVersions(const std::filesystem::path& dir) -{ - std::vector> result; - try { - for (const auto& entry : std::filesystem::directory_iterator(dir)) { - if (entry.is_regular_file() && entry.path().extension() == L".dll") { - const auto& path = entry.path(); - auto version = Util::GetDllVersion(path.c_str()); - auto name = path.filename().string(); - std::string versionStr = version ? Util::GetFormattedVersion(*version) : "Unknown"; - result.emplace_back(name, versionStr); - } - } - } catch (const std::filesystem::filesystem_error& e) { - // Log error but return empty vector to avoid crashing - logger::warn("Failed to enumerate DLL versions in {}: {}", dir.string(), e.what()); - } - return result; -} - std::vector Util::FileSystem::LoadJsonDiff(const std::filesystem::path& userPath, const std::filesystem::path& testPath) { std::vector diffEntries; diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 0d72632f2e..3f2dfec7d7 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -440,13 +440,17 @@ namespace Util hovered = ImGui::IsItemHovered(); // Draw the lines and text using Menu theme colors - auto& theme = globals::menu->GetTheme().FeatureHeading; + auto& palette = globals::menu->GetTheme().Palette; - // Get the color based on hover state - ImVec4 color = hovered ? theme.ColorHovered : theme.ColorDefault; - // If minimized, apply the minimized factor + // Use theme text color for category headers to match other text elements + ImVec4 color = palette.Text; + // If minimized, apply reduced alpha if (!isExpanded) { - color.w *= theme.MinimizedFactor; + color.w *= 0.7f; // 70% alpha when minimized + } + // If hovered, slightly dim the color + if (hovered) { + color.w *= 0.8f; // 80% alpha when hovered } ImU32 headerColor = ImGui::GetColorU32(color); @@ -498,7 +502,9 @@ namespace Util // Use Menu theme colors for consistent styling auto& theme = globals::menu->GetTheme().FeatureHeading; - ImVec4 color = useWhiteText ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : theme.ColorDefault; + auto& palette = globals::menu->GetTheme().Palette; + // When useWhiteText is true, use the theme's text color instead of hardcoded white + ImVec4 color = useWhiteText ? palette.Text : theme.ColorDefault; ImU32 headerColor = ImGui::GetColorU32(color); @@ -738,7 +744,9 @@ namespace Util // Custom style - always transparent background to avoid click blocking ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); ImVec4 bgColorActive = ImVec4(0.3f, 0.3f, 0.3f, 0.9f); - ImVec4 textColor = ImVec4(0.9f, 0.9f, 0.9f, 1.0f); + // Use theme text color instead of hardcoded color + auto& palette = globals::menu->GetTheme().Palette; + ImVec4 textColor = palette.Text; ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); @@ -764,7 +772,12 @@ namespace Util ImVec2 center = ImVec2(iconPos.x + iconSize * 0.46f, iconPos.y + iconSize * 0.5f); float radius = iconSize * 0.3f; - ImU32 placeholderColor = IM_COL32(140, 140, 140, 180); + + // Use themed text color with reduced alpha for search icon + auto& theme = globals::menu->GetTheme().Palette; + ImVec4 iconColor = theme.Text; + iconColor.w *= 0.7f; // Reduce alpha for subtler appearance + ImU32 placeholderColor = ImGui::GetColorU32(iconColor); // Draw circle drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 7c83016446..a738749576 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -6,7 +6,7 @@ #include "Menu.h" #include "ShaderCache.h" #include "State.h" -#include "ThemeManager.h" +#include "Menu/ThemeManager.h" #include "TruePBR.h" #include "ENB/ENBSeriesAPI.h" From 5fc47d064b8137c9892cf5767e263a023369f644 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 23 Sep 2025 17:28:45 +1000 Subject: [PATCH 04/29] Save settings fixes --- src/Menu.cpp | 8 ++ src/Menu.h | 9 ++ src/Menu/SettingsTabRenderer.cpp | 203 ++++++++++++++++++++++++++++--- src/Menu/ThemeManager.cpp | 86 +++++++++++++ src/Menu/ThemeManager.h | 11 ++ src/Utils/FileSystem.cpp | 5 + src/Utils/FileSystem.h | 6 + src/Utils/UI.cpp | 82 +++++++++++++ src/Utils/UI.h | 40 ++++++ 9 files changed, 430 insertions(+), 20 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index eb9ccd8ff1..de797908da 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -64,6 +64,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ColorHovered, MinimizedFactor) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::ThemeSettings::ScrollbarOpacitySettings, + Background, + Thumb, + ThumbHovered, + ThumbActive) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ImGuiStyle, WindowPadding, @@ -108,6 +115,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( UseSimplePalette, ShowActionIcons, TooltipHoverDelay, + ScrollbarOpacity, Palette, StatusPalette, FeatureHeading, diff --git a/src/Menu.h b/src/Menu.h index aa7baf06db..942b61fa4c 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -113,6 +113,15 @@ class Menu bool UseSimplePalette = true; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds + + // Scrollbar opacity settings + struct ScrollbarOpacitySettings + { + float Background = 1.0f; // Background of the scrollbar area + float Thumb = 1.0f; // The draggable thumb/grip + float ThumbHovered = 1.0f; // Thumb when hovered + float ThumbActive = 1.0f; // Thumb when being dragged + } ScrollbarOpacity; struct PaletteColors { ImVec4 Background{ 0.f, 0.f, 0.f, 0.5882353186607361f }; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 15448ad955..ca09e672ca 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1,8 +1,11 @@ #include "SettingsTabRenderer.h" #include +#include +#include #include #include +#include #include "Globals.h" #include "Menu.h" @@ -10,6 +13,8 @@ #include "ThemeManager.h" #include "Util.h" +using json = nlohmann::json; + void SettingsTabRenderer::RenderGeneralSettings( SettingsState& state, const std::function& keyIdToString) @@ -158,6 +163,13 @@ void SettingsTabRenderer::RenderThemesTab() if (ImGui::BeginTabItem("Themes")) { auto& themeSettings = globals::menu->GetSettings().Theme; + // Static variables for popup state and new theme creation + static bool showCreateThemePopup = false; + static bool isCreatingNewTheme = false; + static char newThemeName[128] = ""; + static char newThemeDisplayName[128] = ""; + static char newThemeDescription[256] = ""; + // Theme Preset Selection ImGui::SeparatorText("Theme Preset"); @@ -178,8 +190,9 @@ void SettingsTabRenderer::RenderThemesTab() // Clear and rebuild the lists displayNames.clear(); items.clear(); - - displayNames.push_back("Custom"); // First item for custom theme + + // Add "+ Create New" option at the top + displayNames.push_back("+ Create New"); items.push_back(displayNames.back().c_str()); for (const auto& theme : themes) { @@ -187,12 +200,25 @@ void SettingsTabRenderer::RenderThemesTab() items.push_back(displayNames.back().c_str()); } - // Find current selection index - int currentItem = 0; // Default to "Custom" - if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + // Find current selection index - default to "Default" if no theme selected + // Note: Add 1 to account for "+ Create New" option at index 0 + int currentItem = 1; // Default to first actual theme (Default Dark) + std::string currentThemePreset = globals::menu->GetSettings().SelectedThemePreset; + + // If no theme is selected, default to "Default" + if (currentThemePreset.empty()) { + currentThemePreset = "Default"; + globals::menu->GetSettings().SelectedThemePreset = "Default"; + } + + // If we're in create new mode, show that as selected + if (isCreatingNewTheme) { + currentItem = 0; // "+ Create New" + } else { + // Find the theme in the list (skip index 0 which is "+ Create New") for (size_t i = 0; i < themes.size(); ++i) { - if (themes[i].name == globals::menu->GetSettings().SelectedThemePreset) { - currentItem = static_cast(i) + 1; // +1 for "Custom" + if (themes[i].name == currentThemePreset) { + currentItem = static_cast(i + 1); // +1 for "+ Create New" offset break; } } @@ -201,20 +227,23 @@ void SettingsTabRenderer::RenderThemesTab() // Theme preset dropdown if (ImGui::Combo("##ThemePreset", ¤tItem, items.data(), static_cast(items.size()))) { if (currentItem == 0) { - // Custom theme selected - globals::menu->GetSettings().SelectedThemePreset = ""; - } else { - // Preset theme selected - std::string selectedTheme = themes[currentItem - 1].name; // -1 for "Custom" offset + // "+ Create New" selected + isCreatingNewTheme = true; + // Keep current theme settings as starting point + } else if (currentItem >= 1 && currentItem <= static_cast(themes.size())) { + // Actual theme selected (subtract 1 for "+ Create New" offset) + isCreatingNewTheme = false; + std::string selectedTheme = themes[currentItem - 1].name; if (globals::menu->LoadThemePreset(selectedTheme)) { // Theme loaded successfully, update UI themeSettings = globals::menu->GetSettings().Theme; } } } - // Show theme description as tooltip - if (currentItem > 0 && currentItem - 1 < static_cast(themes.size())) { - const auto& selectedTheme = themes[currentItem - 1]; + + // Show theme description as tooltip (only for actual themes, not "+ Create New") + if (currentItem >= 1 && currentItem <= static_cast(themes.size())) { + const auto& selectedTheme = themes[currentItem - 1]; // -1 for "+ Create New" offset if (!selectedTheme.description.empty()) { if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", selectedTheme.description.c_str()); @@ -225,15 +254,135 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::SameLine(); if (ImGui::Button("Refresh Themes")) { themeManager->RefreshThemes(); - // Reset selection if current theme no longer exists - if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { - const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); - if (!themeInfo) { - globals::menu->GetSettings().SelectedThemePreset = ""; + // Ensure a valid theme is still selected + const auto* themeInfo = themeManager->GetThemeInfo(globals::menu->GetSettings().SelectedThemePreset); + if (!themeInfo) { + globals::menu->GetSettings().SelectedThemePreset = "Default"; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Open Themes Folder")) { + std::filesystem::path themesPath = Util::PathHelpers::GetThemesRealPath(); + ShellExecuteA(NULL, "open", themesPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Opens the Themes folder where you can add custom theme files."); + } + + // Update Current Theme Button (only show for existing themes, not when creating new) + if (!isCreatingNewTheme) { + if (!currentThemePreset.empty() && currentThemePreset != "Default") { + ImGui::SameLine(); + if (ImGui::Button("Update Current Theme")) { + // Get current theme info + const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); + if (currentThemeInfo) { + // Use the existing SaveTheme method to serialize the theme settings + json currentThemeJson; + globals::menu->SaveTheme(currentThemeJson); + + // Overwrite the current theme with updated settings + if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], + currentThemeInfo->displayName, currentThemeInfo->description)) { + // Theme updated successfully + } + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Updates the currently selected theme (%s) with your current settings", currentThemePreset.c_str()); } } } + // Save Theme Button + ImGui::SeparatorText("Save Theme"); + if (ImGui::Button("Save Theme")) { + if (isCreatingNewTheme) { + // Show popup for new theme creation + showCreateThemePopup = true; + // Clear the input fields + memset(newThemeName, 0, sizeof(newThemeName)); + memset(newThemeDisplayName, 0, sizeof(newThemeDisplayName)); + memset(newThemeDescription, 0, sizeof(newThemeDescription)); + } else { + // Overwrite existing theme + if (!currentThemePreset.empty()) { + const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); + if (currentThemeInfo) { + // Use the existing SaveTheme method to serialize the theme settings + json currentThemeJson; + globals::menu->SaveTheme(currentThemeJson); + + // Overwrite the current theme with updated settings + if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], + currentThemeInfo->displayName, currentThemeInfo->description)) { + // Theme updated successfully + } + } + } + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + if (isCreatingNewTheme) { + ImGui::Text("Create a new theme with current settings"); + } else { + ImGui::Text("Overwrite the current theme with your settings"); + } + } + + // Create Theme Popup + if (showCreateThemePopup) { + ImGui::OpenPopup("Create New Theme"); + } + + // Popup modal for creating new theme + if (ImGui::BeginPopupModal("Create New Theme", &showCreateThemePopup, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Create a new theme with your current settings:"); + ImGui::Separator(); + + ImGui::InputText("Theme Name", newThemeName, sizeof(newThemeName)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("File name for the theme (without .json extension)"); + } + + ImGui::InputText("Display Name", newThemeDisplayName, sizeof(newThemeDisplayName)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Human-readable name shown in the dropdown"); + } + + ImGui::InputTextMultiline("Description", newThemeDescription, sizeof(newThemeDescription), ImVec2(400, 80)); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Optional description for the theme"); + } + + ImGui::Separator(); + + // Buttons + if (ImGui::Button("Create Theme") && strlen(newThemeName) > 0) { + // Use the existing SaveTheme method to serialize the theme settings + json currentThemeJson; + globals::menu->SaveTheme(currentThemeJson); + + std::string displayName = strlen(newThemeDisplayName) > 0 ? std::string(newThemeDisplayName) : std::string(newThemeName); + std::string description = strlen(newThemeDescription) > 0 ? std::string(newThemeDescription) : ""; + + if (themeManager->SaveTheme(std::string(newThemeName), currentThemeJson["Theme"], displayName, description)) { + // Theme created successfully, load it and exit create mode + globals::menu->LoadThemePreset(std::string(newThemeName)); + isCreatingNewTheme = false; + showCreateThemePopup = false; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + showCreateThemePopup = false; + } + + ImGui::EndPopup(); + } + ImGui::SeparatorText("UI Elements"); ImGui::Checkbox("Use Icon Buttons in Header", &themeSettings.ShowActionIcons); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -273,6 +422,20 @@ void SettingsTabRenderer::RenderStylingTab() ImGui::SliderFloat("Scrollbar Size", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); ImGui::SliderFloat("Grab Min Size", &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); + ImGui::SeparatorText("Scrollbar Opacity"); + ImGui::SliderFloat("Track Opacity", &themeSettings.ScrollbarOpacity.Background, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar)."); + ImGui::SliderFloat("Thumb Opacity", &themeSettings.ScrollbarOpacity.Thumb, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb (the draggable part)."); + ImGui::SliderFloat("Thumb Hovered Opacity", &themeSettings.ScrollbarOpacity.ThumbHovered, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb when hovered."); + ImGui::SliderFloat("Thumb Active Opacity", &themeSettings.ScrollbarOpacity.ThumbActive, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Controls the opacity of the scrollbar thumb when being dragged."); + ImGui::SeparatorText("Borders"); ImGui::SliderFloat("Window Border Size", &style.WindowBorderSize, 0.0f, 5.0f, "%.0f"); ImGui::SliderFloat("Child Border Size", &style.ChildBorderSize, 0.0f, 5.0f, "%.0f"); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index ea6e944ed1..547c0eab17 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -72,6 +72,40 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) resizeGripHovered.w = 0.1f; colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; colors[ImGuiCol_ResizeGripActive] = resizeGripHovered; + + // Auto-adjust text colors for better contrast on selection backgrounds + // This fixes white-on-white text issues in high contrast themes + auto calculateLuminance = [](const ImVec4& color) { + auto toLinear = [](float c) { return c <= 0.03928f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); }; + float r = toLinear(color.x), g = toLinear(color.y), b = toLinear(color.z); + return 0.2126f * r + 0.7152f * g + 0.0722f * b; + }; + + auto getContrastingTextColor = [&](const ImVec4& bgColor) { + float luminance = calculateLuminance(bgColor); + return luminance > 0.5f ? ImVec4(0.0f, 0.0f, 0.0f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + }; + + // Apply contrast-aware text for selection states + if (calculateLuminance(colors[ImGuiCol_HeaderActive]) > 0.5f) { + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black text on light selection + } + if (calculateLuminance(colors[ImGuiCol_HeaderHovered]) > 0.5f) { + // For hovered items, we can't directly change text color, but we can adjust the hover background + // to ensure better contrast with the current text color + float textLum = calculateLuminance(colors[ImGuiCol_Text]); + if (textLum > 0.5f) { // If text is light, darken the hover background + ImVec4 darkerHover = colors[ImGuiCol_HeaderHovered]; + darkerHover.x *= 0.3f; darkerHover.y *= 0.3f; darkerHover.z *= 0.3f; + colors[ImGuiCol_HeaderHovered] = darkerHover; + } + } + + // Apply scrollbar opacity settings + colors[ImGuiCol_ScrollbarBg].w = themeSettings.ScrollbarOpacity.Background; + colors[ImGuiCol_ScrollbarGrab].w = themeSettings.ScrollbarOpacity.Thumb; + colors[ImGuiCol_ScrollbarGrabHovered].w = themeSettings.ScrollbarOpacity.ThumbHovered; + colors[ImGuiCol_ScrollbarGrabActive].w = themeSettings.ScrollbarOpacity.ThumbActive; } void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) @@ -215,6 +249,58 @@ bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) } } +bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description) +{ + if (themeName.empty()) { + logger::warn("Cannot save theme with empty name"); + return false; + } + + // Create the full theme JSON structure + json fullTheme = { + {"DisplayName", displayName.empty() ? themeName : displayName}, + {"Description", description.empty() ? "Custom user theme" : description}, + {"Version", "1.0.0"}, + {"Author", "User"}, + {"Theme", themeSettings} + }; + + // Generate safe filename (remove invalid characters) + std::string safeFileName = themeName; + std::replace_if(safeFileName.begin(), safeFileName.end(), + [](char c) { return c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|'; }, + '_'); + + auto themesDir = GetThemesDirectory(); + auto filePath = themesDir / (safeFileName + ".json"); + + try { + // Ensure themes directory exists + std::filesystem::create_directories(themesDir); + + // Write the theme file + std::ofstream file(filePath); + if (!file.is_open()) { + logger::warn("Failed to create theme file: {}", filePath.string()); + return false; + } + + file << fullTheme.dump(4); // Pretty print with 4-space indentation + file.close(); + + logger::info("Saved theme: {} to {}", themeName, filePath.string()); + + // Refresh themes to include the new one + RefreshThemes(); + + return true; + } catch (const std::exception& e) { + logger::warn("Error saving theme {}: {}", themeName, e.what()); + return false; + } +} + const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const { auto it = std::find_if(themes.begin(), themes.end(), diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 95d0f13e65..6beacb47be 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -108,6 +108,17 @@ class ThemeManager */ bool LoadTheme(const std::string& themeName, json& themeSettings); + /** + * @brief Saves current theme settings to a new theme file + * @param themeName Name for the new theme file + * @param themeSettings Theme settings to save + * @param displayName Display name for the theme + * @param description Description for the theme + * @return True if theme was saved successfully + */ + bool SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description); + /** * @brief Gets theme info by name * @param themeName Name of the theme diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index e9ba3c9264..eb359bddf7 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -151,6 +151,11 @@ namespace Util return GetRootRealPath() / "Shaders"; } + std::filesystem::path GetThemesRealPath() + { + return GetRootRealPath() / "SKSE" / "Plugins" / "CommunityShaders" / "Themes"; + } + std::filesystem::path GetFeaturesRealPath() { return GetShadersRealPath() / "Features"; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index e74df643b4..268a183c30 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -160,6 +160,12 @@ namespace Util */ std::filesystem::path GetShadersRealPath(); + /** + * Returns the real path to the Themes directory containing theme JSON files. + * @return / "SKSE" / "Plugins" / "CommunityShaders" / "Themes" + */ + std::filesystem::path GetThemesRealPath(); + /** * Returns the real path to the Features directory containing feature INI files. * @return / "Shaders" / "Features" diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 3f2dfec7d7..610f0f3df2 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1205,5 +1205,87 @@ namespace Util return keyboard_keys_international[key]; } + + // Color utilities for contrast and readability + namespace ColorUtils + { + float CalculateLuminance(const ImVec4& color) + { + // Convert to linear RGB first (gamma correction) + auto toLinear = [](float c) { + return c <= 0.03928f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); + }; + + float r = toLinear(color.x); + float g = toLinear(color.y); + float b = toLinear(color.z); + + // Calculate relative luminance using WCAG formula + return 0.2126f * r + 0.7152f * g + 0.0722f * b; + } + + ImVec4 GetContrastingTextColor(const ImVec4& backgroundColor, float threshold) + { + float luminance = CalculateLuminance(backgroundColor); + + // If background is bright (high luminance), use black text + // If background is dark (low luminance), use white text + if (luminance > threshold) { + return ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black + } else { + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White + } + } + + float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2) + { + float lum1 = CalculateLuminance(color1); + float lum2 = CalculateLuminance(color2); + + // Ensure lighter color is in numerator + float lighter = std::max(lum1, lum2); + float darker = std::min(lum1, lum2); + + return (lighter + 0.05f) / (darker + 0.05f); + } + + bool ContrastSelectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size) + { + // Get current style colors for different states + ImGuiStyle& style = ImGui::GetStyle(); + + // We need to handle text color based on the selectable's background state + // For selected items, ImGui uses HeaderActive color which might be light + ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; + ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; + + // Calculate text colors for each state + ImVec4 selectedTextColor = GetContrastingTextColor(selectedBgColor, 0.5f); + ImVec4 hoveredTextColor = GetContrastingTextColor(hoveredBgColor, 0.5f); + ImVec4 normalTextColor = style.Colors[ImGuiCol_Text]; + + // If the item is selected, we know it will have the selected background + if (selected) { + ImGui::PushStyleColor(ImGuiCol_Text, selectedTextColor); + } else { + // For non-selected items, we'll use normal text unless we detect high contrast issues + // Check if hover/active backgrounds would cause contrast issues + float hoveredContrast = CalculateContrastRatio(normalTextColor, hoveredBgColor); + if (hoveredContrast < 3.0f) { // WCAG AA minimum is 4.5, but 3.0 for safety + ImGui::PushStyleColor(ImGuiCol_Text, hoveredTextColor); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, normalTextColor); + } + } + + // Create the selectable with the adjusted text color + bool result = ImGui::Selectable(label, selected, flags, size); + + // Restore original text color + ImGui::PopStyleColor(); + + return result; + } + } } } // namespace Util \ No newline at end of file diff --git a/src/Utils/UI.h b/src/Utils/UI.h index c589103bb7..0f97961b91 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -147,6 +147,46 @@ namespace Util bool m_treeNodeOpened; }; + /** + * Color utilities for contrast and readability + */ + namespace ColorUtils + { + /** + * Calculates the relative luminance of a color according to WCAG guidelines + * @param color ImVec4 color to calculate luminance for + * @return Luminance value between 0.0 (darkest) and 1.0 (brightest) + */ + float CalculateLuminance(const ImVec4& color); + + /** + * Determines the appropriate text color (black or white) for maximum contrast + * against the given background color + * @param backgroundColor Background color to test against + * @param threshold Luminance threshold for switching (default 0.5) + * @return Black color for light backgrounds, white color for dark backgrounds + */ + ImVec4 GetContrastingTextColor(const ImVec4& backgroundColor, float threshold = 0.5f); + + /** + * Calculates contrast ratio between two colors according to WCAG guidelines + * @param color1 First color + * @param color2 Second color + * @return Contrast ratio (1.0 = no contrast, 21.0 = maximum contrast) + */ + float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2); + + /** + * Creates a selectable item with automatic contrast-aware text coloring + * @param label Text to display + * @param selected Whether the item is currently selected + * @param flags Selectable flags (optional) + * @param size Size of the selectable area (optional) + * @return True if the selectable was clicked + */ + bool ContrastSelectable(const char* label, bool selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); + } + bool PercentageSlider(const char* label, float* data, float lb = 0.f, float ub = 100.f, const char* format = "%.1f %%"); ImVec2 GetNativeViewportSizeScaled(float scale); From 6442daa33058d9caadb773a29d591771b31ad1f6 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 23 Sep 2025 22:07:41 +1000 Subject: [PATCH 05/29] Font support, ctd on changing fonts. --- src/Menu.cpp | 8 +- src/Menu.h | 7 +- src/Menu/FeatureListRenderer.cpp | 24 +++--- src/Menu/MenuHeaderRenderer.cpp | 2 +- src/Menu/OverlayRenderer.cpp | 9 ++- src/Menu/SettingsTabRenderer.cpp | 127 ++++++++++++++++++++----------- src/Menu/ThemeManager.cpp | 6 +- src/Utils/UI.cpp | 115 ++++++++++++++++++++++++++-- src/Utils/UI.h | 37 +++++++++ 9 files changed, 264 insertions(+), 71 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index de797908da..8e93f669e4 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -111,6 +111,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings, FontSize, + FontName, GlobalScale, UseSimplePalette, ShowActionIcons, @@ -202,6 +203,8 @@ bool Menu::LoadThemePreset(const std::string& themeName) if (themeManager->LoadTheme(themeName, themeSettings)) { settings.Theme = themeSettings; settings.SelectedThemePreset = themeName; + // Update cached values for font reload detection + cachedFontName = settings.Theme.FontName; logger::info("Loaded theme preset: {}", themeName); return true; } else { @@ -251,13 +254,16 @@ void Menu::Init() fontSize = std::clamp(fontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE); - auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; + auto fontPath = Util::PathHelpers::GetFontsPath() / settings.Theme.FontName; if (!imgui_io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), std::round(fontSize), &font_config)) { logger::warn("Menu::Init() - Failed to load custom font. Using default font."); imgui_io.Fonts->AddFontDefault(); } + // Initialize cached values for reload detection + cachedFontName = settings.Theme.FontName; + imgui_io.FontGlobalScale = exp2(settings.Theme.GlobalScale); // Setup Platform/Renderer backends diff --git a/src/Menu.h b/src/Menu.h index 942b61fa4c..d5a93ba0dc 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -58,6 +58,10 @@ class Menu uint32_t priorShaderKey = VK_PRIOR; // used for blocking shaders in debugging uint32_t nextShaderKey = VK_NEXT; // used for blocking shaders in debugging + // Font caching (made public for ThemeManager and OverlayRenderer access) + float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading + std::string cachedFontName = "Jost-Regular.ttf"; // Tracks whether font file has changed and may require reloading + // Used for resetting input keys to solve alt-tab stuck issue std::atomic focusChanged = false; void OnFocusChanged(); @@ -108,6 +112,7 @@ class Menu struct ThemeSettings { float FontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; + std::string FontName = "Jost-Regular.ttf"; // Default font file name float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential bool UseSimplePalette = true; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. @@ -293,8 +298,6 @@ class Menu private: Settings settings; - float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading - std::string cachedIniPath; // io.IniFilename must point to a string that lives for the duration of the runtime // Menu navigation diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 78cad24f4e..5391f828fe 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -251,13 +251,15 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const BuiltInMenu& menu) if (isFeatureIssues) { auto& themeSettings = globals::menu->GetSettings().Theme; ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); - } - - if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) - selectedMenuRef = listId; - - if (isFeatureIssues) { + + if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) + selectedMenuRef = listId; + ImGui::PopStyleColor(); + } else { + // Use contrast-aware selectable for better text visibility + if (Util::ColorUtils::ContrastSelectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, 0))) + selectedMenuRef = listId; } } @@ -313,17 +315,11 @@ void FeatureListRenderer::ListMenuVisitor::operator()(Feature* feat) } } - // Set text color - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - - // Create selectable item - if (ImGui::Selectable(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { + // Create selectable item with contrast-adjusted semantic color + if (Util::ColorUtils::ContrastSelectableWithColor(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, textColor, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, 0))) { selectedMenuRef = listId; } - // Restore original text color - ImGui::PopStyleColor(); - // Display version if loaded if (isLoaded) { ImGui::SameLine(); diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 466862e426..4c7ab9b828 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -90,7 +90,7 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow if (ImGui::BeginTable("##ActionButtons", 4, ImGuiTableFlags_SizingStretchSame)) { // Save Settings Button ImGui::TableNextColumn(); - if (ImGui::Button("Save Settings", { -1, 0 })) { + if (Util::ButtonWithFeedback("Save Settings", { -1, 0 })) { globals::state->Save(); globals::state->SaveTheme(); } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index ac0c73dda8..12183972fe 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -81,8 +81,13 @@ bool OverlayRenderer::ShouldSkipRendering() void OverlayRenderer::HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize) { - // Reload font if user changed something - if (std::abs(cachedFontSize - currentFontSize) > ThemeManager::Constants::FONT_CACHE_EPSILON) { + auto& currentTheme = menu.GetTheme(); + + // Reload font if size changed or font file changed + bool fontSizeChanged = std::abs(cachedFontSize - currentFontSize) > ThemeManager::Constants::FONT_CACHE_EPSILON; + bool fontNameChanged = menu.cachedFontName != currentTheme.FontName; + + if (fontSizeChanged || fontNameChanged) { ThemeManager::ReloadFont(menu, cachedFontSize); } } diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index ca09e672ca..f1a84cc3d1 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -270,12 +270,21 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::Text("Opens the Themes folder where you can add custom theme files."); } - // Update Current Theme Button (only show for existing themes, not when creating new) - if (!isCreatingNewTheme) { - if (!currentThemePreset.empty() && currentThemePreset != "Default") { - ImGui::SameLine(); - if (ImGui::Button("Update Current Theme")) { - // Get current theme info + // Save/Update Theme Button (show based on context) + if (isCreatingNewTheme || (!currentThemePreset.empty() && currentThemePreset != "Default")) { + ImGui::SameLine(); + + const char* buttonText = isCreatingNewTheme ? "Save Theme" : "Update Theme"; + if (Util::ButtonWithFeedback(buttonText)) { + if (isCreatingNewTheme) { + // Show popup for new theme creation + showCreateThemePopup = true; + // Clear the input fields + memset(newThemeName, 0, sizeof(newThemeName)); + memset(newThemeDisplayName, 0, sizeof(newThemeDisplayName)); + memset(newThemeDescription, 0, sizeof(newThemeDescription)); + } else { + // Update existing theme const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); if (currentThemeInfo) { // Use the existing SaveTheme method to serialize the theme settings @@ -289,47 +298,17 @@ void SettingsTabRenderer::RenderThemesTab() } } } - if (auto _tt = Util::HoverTooltipWrapper()) { + } + if (auto _tt = Util::HoverTooltipWrapper()) { + if (isCreatingNewTheme) { + ImGui::Text("Create a new theme with current settings"); + } else { ImGui::Text("Updates the currently selected theme (%s) with your current settings", currentThemePreset.c_str()); } } } - // Save Theme Button - ImGui::SeparatorText("Save Theme"); - if (ImGui::Button("Save Theme")) { - if (isCreatingNewTheme) { - // Show popup for new theme creation - showCreateThemePopup = true; - // Clear the input fields - memset(newThemeName, 0, sizeof(newThemeName)); - memset(newThemeDisplayName, 0, sizeof(newThemeDisplayName)); - memset(newThemeDescription, 0, sizeof(newThemeDescription)); - } else { - // Overwrite existing theme - if (!currentThemePreset.empty()) { - const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); - if (currentThemeInfo) { - // Use the existing SaveTheme method to serialize the theme settings - json currentThemeJson; - globals::menu->SaveTheme(currentThemeJson); - - // Overwrite the current theme with updated settings - if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], - currentThemeInfo->displayName, currentThemeInfo->description)) { - // Theme updated successfully - } - } - } - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - if (isCreatingNewTheme) { - ImGui::Text("Create a new theme with current settings"); - } else { - ImGui::Text("Overwrite the current theme with your settings"); - } - } + // Create Theme Popup if (showCreateThemePopup) { @@ -359,7 +338,7 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::Separator(); // Buttons - if (ImGui::Button("Create Theme") && strlen(newThemeName) > 0) { + if (Util::ButtonWithFeedback("Create Theme") && strlen(newThemeName) > 0) { // Use the existing SaveTheme method to serialize the theme settings json currentThemeJson; globals::menu->SaveTheme(currentThemeJson); @@ -413,7 +392,67 @@ void SettingsTabRenderer::RenderStylingTab() auto& io = ImGui::GetIO(); io.FontGlobalScale = trueScale; } - ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f"); + + ImGui::SeparatorText("Font"); + if (ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { + // Font size changed, force reload + ThemeManager::ReloadFont(*globals::menu, globals::menu->cachedFontSize); + } + + // Font selection dropdown + static std::vector availableFonts; + static std::vector fontItems; + static bool fontsDiscovered = false; + + if (!fontsDiscovered) { + availableFonts = Util::DiscoverFonts(); + fontItems.clear(); + for (const auto& font : availableFonts) { + fontItems.push_back(font.c_str()); + } + fontsDiscovered = true; + } + + // Find current font index + int currentFontIndex = 0; + for (size_t i = 0; i < availableFonts.size(); ++i) { + if (availableFonts[i] == themeSettings.FontName) { + currentFontIndex = static_cast(i); + break; + } + } + + if (ImGui::Combo("Font", ¤tFontIndex, fontItems.data(), static_cast(fontItems.size()))) { + if (currentFontIndex >= 0 && currentFontIndex < static_cast(availableFonts.size())) { + themeSettings.FontName = availableFonts[currentFontIndex]; + // Force font reload by updating cached font size + ThemeManager::ReloadFont(*globals::menu, globals::menu->cachedFontSize); + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Select a custom font file (.ttf/.otf) from the Fonts folder.\nPlace custom fonts in: Interface/CommunityShaders/Fonts/"); + } + + if (ImGui::Button("Refresh Font List")) { + availableFonts = Util::DiscoverFonts(); + fontItems.clear(); + for (const auto& font : availableFonts) { + fontItems.push_back(font.c_str()); + } + // Update current selection + currentFontIndex = 0; + for (size_t i = 0; i < availableFonts.size(); ++i) { + if (availableFonts[i] == themeSettings.FontName) { + currentFontIndex = static_cast(i); + break; + } + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Refresh the list of available fonts after adding new font files."); + } + + ImGui::SeparatorText("Layout"); ImGui::SliderFloat2("Window Padding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Frame Padding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Item Spacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 547c0eab17..1e7c7f1cdf 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -125,10 +125,10 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) float fontSize = themeSettings.FontSize; fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); - auto fontPath = Util::PathHelpers::GetFontsPath() / "Jost-Regular.ttf"; + auto fontPath = Util::PathHelpers::GetFontsPath() / themeSettings.FontName; if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), std::round(fontSize), &font_config)) { - logger::warn("ThemeManager::ReloadFont() - Failed to load custom font. Using default font."); + logger::warn("ThemeManager::ReloadFont() - Failed to load custom font '{}'. Using default font.", themeSettings.FontName); io.Fonts->AddFontDefault(); } @@ -139,6 +139,8 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) io.FontGlobalScale = exp2(themeSettings.GlobalScale); cachedFontSize = themeSettings.FontSize; + // Also update cached font name in the menu instance + const_cast(menu).cachedFontName = themeSettings.FontName; } // Theme management methods diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 610f0f3df2..eaa87f7e44 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace Util @@ -1205,9 +1206,10 @@ namespace Util return keyboard_keys_international[key]; } + } // namespace Input - // Color utilities for contrast and readability - namespace ColorUtils + // Color utilities for contrast and readability + namespace ColorUtils { float CalculateLuminance(const ImVec4& color) { @@ -1243,8 +1245,8 @@ namespace Util float lum2 = CalculateLuminance(color2); // Ensure lighter color is in numerator - float lighter = std::max(lum1, lum2); - float darker = std::min(lum1, lum2); + float lighter = (std::max)(lum1, lum2); + float darker = (std::min)(lum1, lum2); return (lighter + 0.05f) / (darker + 0.05f); } @@ -1286,6 +1288,109 @@ namespace Util return result; } + + bool ContrastSelectableWithColor(const char* label, bool selected, const ImVec4& semanticTextColor, ImGuiSelectableFlags flags, const ImVec2& size) + { + // Get current style colors for different states + ImGuiStyle& style = ImGui::GetStyle(); + + // We need to handle text color based on the selectable's background state + // For selected items, ImGui uses HeaderActive color which might be light + ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; + ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; + + // Use the provided semantic color but ensure it has good contrast + ImVec4 textColor = semanticTextColor; + + // If the item is selected, we know it will have the selected background + if (selected) { + // Check contrast with selected background + float contrast = CalculateContrastRatio(semanticTextColor, selectedBgColor); + if (contrast < 3.0f) { + textColor = GetContrastingTextColor(selectedBgColor, 0.5f); + } + } else { + // Check contrast with potential hover background + float hoveredContrast = CalculateContrastRatio(semanticTextColor, hoveredBgColor); + if (hoveredContrast < 3.0f) { + textColor = GetContrastingTextColor(hoveredBgColor, 0.5f); + } + } + + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + + // Create the selectable with the adjusted text color + bool result = ImGui::Selectable(label, selected, flags, size); + + // Restore original text color + ImGui::PopStyleColor(); + + return result; + } + } // namespace ColorUtils + + bool ButtonWithFeedback(const char* label, const ImVec2& size, int feedbackDurationMs) + { + static std::unordered_map feedbackTimers; + + std::string buttonId = std::string(label); + auto now = std::chrono::steady_clock::now(); + + // Check if this button has active feedback + bool hasActiveFeedback = false; + auto it = feedbackTimers.find(buttonId); + if (it != feedbackTimers.end()) { + auto elapsed = std::chrono::duration_cast(now - it->second); + if (elapsed.count() < feedbackDurationMs) { + hasActiveFeedback = true; + } else { + // Feedback expired, remove it + feedbackTimers.erase(it); + } + } + + // Style the button differently if it has active feedback + bool styleChanged = false; + if (hasActiveFeedback) { + // Get success color from theme for feedback + ImVec4 successColor = globals::menu->GetTheme().StatusPalette.SuccessColor; + ImVec4 successHovered = ImVec4(successColor.x * 0.8f, successColor.y * 0.8f, successColor.z * 0.8f, successColor.w); + ImVec4 successActive = ImVec4(successColor.x * 0.6f, successColor.y * 0.6f, successColor.z * 0.6f, successColor.w); + + ImGui::PushStyleColor(ImGuiCol_Button, successColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, successHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, successActive); + styleChanged = true; + } + + bool clicked = ImGui::Button(label, size); + + if (styleChanged) { + ImGui::PopStyleColor(3); + } + + // If clicked, start the feedback timer + if (clicked) { + feedbackTimers[buttonId] = now; } + + return clicked; + } + + std::vector DiscoverFonts() + { + std::vector fonts; + + // Add some common system fonts as a basic implementation + fonts.push_back("Arial"); + fonts.push_back("Calibri"); + fonts.push_back("Consolas"); + fonts.push_back("Courier New"); + fonts.push_back("Georgia"); + fonts.push_back("Segoe UI"); + fonts.push_back("Times New Roman"); + fonts.push_back("Verdana"); + + return fonts; } -} // namespace Util \ No newline at end of file +} // namespace Util diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 0f97961b91..7ca212d406 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -109,6 +109,22 @@ namespace Util int m_pushedStyles; }; + /** + * Creates a button with visual feedback that briefly shows success colors when clicked. + * Useful for save/update operations where users need confirmation that the action worked. + * @param label Button text + * @param size Button size (optional) + * @param feedbackDurationMs How long to show feedback colors in milliseconds (default 1000ms) + * @return True if the button was clicked + */ + bool ButtonWithFeedback(const char* label, const ImVec2& size = ImVec2(0, 0), int feedbackDurationMs = 1000); + + /** + * Discovers available font files in the Fonts directory + * @return Vector of font file names (including .ttf extension) + */ + std::vector DiscoverFonts(); + /** * RAII wrapper for creating collapsible UI sections. * Automatically handles the TreeNode creation, styling, and cleanup. @@ -176,6 +192,15 @@ namespace Util */ float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2); + /** + * Adjusts a text color to ensure sufficient contrast against a background + * @param textColor The desired text color (semantic color) + * @param backgroundColor The background color to contrast against + * @param minimumRatio Minimum acceptable contrast ratio (default 3.0) + * @return Adjusted text color with sufficient contrast + */ + ImVec4 AdjustColorForContrast(const ImVec4& textColor, const ImVec4& backgroundColor, float minimumRatio = 3.0f); + /** * Creates a selectable item with automatic contrast-aware text coloring * @param label Text to display @@ -185,6 +210,18 @@ namespace Util * @return True if the selectable was clicked */ bool ContrastSelectable(const char* label, bool selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); + + /** + * Creates a selectable item with contrast-adjusted semantic text coloring + * Preserves the intent of semantic colors while ensuring adequate contrast + * @param label Text to display + * @param selected Whether the item is currently selected + * @param semanticTextColor The desired semantic color (will be adjusted for contrast) + * @param flags Selectable flags (optional) + * @param size Size of the selectable area (optional) + * @return True if the selectable was clicked + */ + bool ContrastSelectableWithColor(const char* label, bool selected, const ImVec4& semanticTextColor, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); } bool PercentageSlider(const char* label, float* data, float lb = 0.f, float ub = 100.f, const char* format = "%.1f %%"); From 69b36ea54d309a16dac8701c9bca9099813403e7 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 24 Sep 2025 10:53:15 +1000 Subject: [PATCH 06/29] font fixes --- src/Utils/UI.cpp | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index eaa87f7e44..5b79babdc1 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1381,15 +1381,39 @@ namespace Util { std::vector fonts; - // Add some common system fonts as a basic implementation - fonts.push_back("Arial"); - fonts.push_back("Calibri"); - fonts.push_back("Consolas"); - fonts.push_back("Courier New"); - fonts.push_back("Georgia"); - fonts.push_back("Segoe UI"); - fonts.push_back("Times New Roman"); - fonts.push_back("Verdana"); + try { + auto fontsPath = Util::PathHelpers::GetFontsPath(); + logger::debug("DiscoverFonts: Scanning fonts directory: {}", fontsPath.string()); + + // Check if fonts directory exists + if (!std::filesystem::exists(fontsPath)) { + logger::warn("DiscoverFonts: Fonts directory does not exist: {}", fontsPath.string()); + return fonts; + } + + // Scan for font files (.ttf and .otf) + for (const auto& entry : std::filesystem::directory_iterator(fontsPath)) { + if (entry.is_regular_file()) { + auto extension = entry.path().extension().string(); + // Convert to lowercase for comparison + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + + if (extension == ".ttf" || extension == ".otf") { + std::string fontFile = entry.path().filename().string(); + fonts.push_back(fontFile); + logger::debug("DiscoverFonts: Found font file: {}", fontFile); + } + } + } + + // Sort fonts alphabetically for better user experience + std::sort(fonts.begin(), fonts.end()); + logger::info("DiscoverFonts: Found {} font files", fonts.size()); + } + catch (const std::exception& e) { + logger::error("DiscoverFonts: Exception occurred while scanning fonts: {}", e.what()); + // Silently return empty vector on error + } return fonts; } From f4984daba14fb9cdd34965f747673184a63008b0 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 19:23:05 +1000 Subject: [PATCH 07/29] fixed font rendering --- src/Menu.cpp | 10 ++++ src/Menu.h | 4 ++ src/Menu/SettingsTabRenderer.cpp | 75 +++++++++++++++++--------- src/Menu/ThemeManager.cpp | 91 ++++++++++++++++++++++++++++++-- src/Utils/UI.cpp | 1 + 5 files changed, 151 insertions(+), 30 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 8e93f669e4..4f7b690ba9 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -312,6 +312,7 @@ void Menu::DrawSettings() OnFocusChanged(); focusChanged = false; } + ImGui::DockSpaceOverViewport(NULL, ImGuiDockNodeFlags_PassthruCentralNode); ImGui::SetNextWindowPos(Util::GetNativeViewportSizeScaled(0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); @@ -486,6 +487,15 @@ void Menu::DrawFooter() */ void Menu::DrawOverlay() { + // Process deferred font reload BEFORE any ImGui operations + // This is the safest place to do font atlas modifications + if (pendingFontReload) { + pendingFontReload = false; + settings.Theme.FontName = pendingFontName; + ThemeManager::ReloadFont(*this, cachedFontSize); + pendingFontName.clear(); + } + OverlayRenderer::RenderOverlay( *this, [this]() { ProcessInputEventQueue(); }, diff --git a/src/Menu.h b/src/Menu.h index d5a93ba0dc..111798e806 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -61,6 +61,10 @@ class Menu // Font caching (made public for ThemeManager and OverlayRenderer access) float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading std::string cachedFontName = "Jost-Regular.ttf"; // Tracks whether font file has changed and may require reloading + + // Deferred font reload system (public for SettingsTabRenderer access) + bool pendingFontReload = false; + std::string pendingFontName; // Used for resetting input keys to solve alt-tab stuck issue std::atomic focusChanged = false; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index f1a84cc3d1..e6ea3be7b5 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -395,21 +395,26 @@ void SettingsTabRenderer::RenderStylingTab() ImGui::SeparatorText("Font"); if (ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { - // Font size changed, force reload - ThemeManager::ReloadFont(*globals::menu, globals::menu->cachedFontSize); + // Font size changed, schedule deferred reload + globals::menu->pendingFontReload = true; + globals::menu->pendingFontName = themeSettings.FontName; // Keep current font name } // Font selection dropdown static std::vector availableFonts; - static std::vector fontItems; static bool fontsDiscovered = false; - if (!fontsDiscovered) { - availableFonts = Util::DiscoverFonts(); - fontItems.clear(); - for (const auto& font : availableFonts) { - fontItems.push_back(font.c_str()); + auto refreshFontList = [&]() { + try { + availableFonts = Util::DiscoverFonts(); + } catch (const std::exception&) { + // Failed to discover fonts, clear list + availableFonts.clear(); } + }; + + if (!fontsDiscovered) { + refreshFontList(); fontsDiscovered = true; } @@ -422,30 +427,50 @@ void SettingsTabRenderer::RenderStylingTab() } } - if (ImGui::Combo("Font", ¤tFontIndex, fontItems.data(), static_cast(fontItems.size()))) { - if (currentFontIndex >= 0 && currentFontIndex < static_cast(availableFonts.size())) { - themeSettings.FontName = availableFonts[currentFontIndex]; - // Force font reload by updating cached font size - ThemeManager::ReloadFont(*globals::menu, globals::menu->cachedFontSize); + // Use ImGui::Combo with safety checks to avoid crashes + const char* previewText = "None"; + if (!availableFonts.empty() && currentFontIndex >= 0 && currentFontIndex < static_cast(availableFonts.size())) { + previewText = availableFonts[currentFontIndex].c_str(); + } + + if (ImGui::BeginCombo("Font", previewText)) { + if (availableFonts.empty()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No fonts available"); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Place .ttf/.otf files in Fonts folder"); + } else { + for (int i = 0; i < static_cast(availableFonts.size()); ++i) { + const bool isSelected = (i == currentFontIndex); + if (ImGui::Selectable(availableFonts[i].c_str(), isSelected)) { + if (i != currentFontIndex && !availableFonts[i].empty()) { + // Validate font name before applying + const std::string& newFontName = availableFonts[i]; + auto fontPath = Util::PathHelpers::GetFontsPath() / newFontName; + + if (std::filesystem::exists(fontPath)) { + // Schedule deferred font reload (safe - will happen between frames) + globals::menu->pendingFontReload = true; + globals::menu->pendingFontName = newFontName; + } + } + } + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } } + ImGui::EndCombo(); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Select a custom font file (.ttf/.otf) from the Fonts folder.\nPlace custom fonts in: Interface/CommunityShaders/Fonts/"); } if (ImGui::Button("Refresh Font List")) { - availableFonts = Util::DiscoverFonts(); - fontItems.clear(); - for (const auto& font : availableFonts) { - fontItems.push_back(font.c_str()); - } - // Update current selection - currentFontIndex = 0; - for (size_t i = 0; i < availableFonts.size(); ++i) { - if (availableFonts[i] == themeSettings.FontName) { - currentFontIndex = static_cast(i); - break; - } + refreshFontList(); + // Reset current font index if it's out of bounds after refresh + if (currentFontIndex >= static_cast(availableFonts.size())) { + currentFontIndex = 0; } } if (auto _tt = Util::HoverTooltipWrapper()) { diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 1e7c7f1cdf..880eb3bb59 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -4,10 +4,12 @@ #include #include #include +#include #include #include #include +#include #include "RE/Skyrim.h" #include "../Utils/FileSystem.h" @@ -110,9 +112,43 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) { + // Static flag to prevent concurrent font reloads + static bool isReloading = false; + if (isReloading) { + logger::warn("ThemeManager::ReloadFont() - Font reload already in progress, skipping"); + return; + } + + isReloading = true; auto& themeSettings = menu.GetTheme(); + logger::info("ThemeManager::ReloadFont() - Starting font reload..."); + ImGuiIO& io = ImGui::GetIO(); + + // Additional safety checks: ensure ImGui is in a valid state + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (!ctx) { + logger::error("ThemeManager::ReloadFont() - No valid ImGui context!"); + isReloading = false; + return; + } + + // Ensure we're not in the middle of a frame + if (ctx->WithinFrameScope) { + logger::error("ThemeManager::ReloadFont() - Cannot reload font within frame scope!"); + isReloading = false; + return; + } + + // Additional check: make sure font atlas exists + if (!io.Fonts) { + logger::error("ThemeManager::ReloadFont() - No font atlas available!"); + isReloading = false; + return; + } + + // Clear existing fonts from the atlas io.Fonts->Clear(); ImFontConfig font_config; @@ -125,22 +161,67 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) float fontSize = themeSettings.FontSize; fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); - auto fontPath = Util::PathHelpers::GetFontsPath() / themeSettings.FontName; - if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), - std::round(fontSize), &font_config)) { - logger::warn("ThemeManager::ReloadFont() - Failed to load custom font '{}'. Using default font.", themeSettings.FontName); + // Check if font name is empty or invalid + if (themeSettings.FontName.empty()) { + logger::info("ThemeManager::ReloadFont() - No custom font specified, using default font."); io.Fonts->AddFontDefault(); + } else { + auto fontPath = Util::PathHelpers::GetFontsPath() / themeSettings.FontName; + + // Check if font file exists before trying to load it + if (!std::filesystem::exists(fontPath)) { + logger::warn("ThemeManager::ReloadFont() - Font file '{}' does not exist. Using default font.", fontPath.string()); + io.Fonts->AddFontDefault(); + } else if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), + std::round(fontSize), &font_config)) { + logger::warn("ThemeManager::ReloadFont() - Failed to load custom font '{}'. Using default font.", themeSettings.FontName); + io.Fonts->AddFontDefault(); + } else { + logger::info("ThemeManager::ReloadFont() - Successfully loaded font '{}'", themeSettings.FontName); + } } - io.Fonts->Build(); + // Build the font atlas - this bakes all fonts into the texture + if (!io.Fonts->Build()) { + logger::error("ThemeManager::ReloadFont() - Failed to build font atlas!"); + isReloading = false; + return; + } + // Recreate device objects - this is where the crash was likely happening + // We need to be very careful about the order and ensure everything is valid + + // Important: We must ensure ImGui is not in the middle of any rendering operations + // The deferred execution should guarantee this, but let's be extra safe + + logger::debug("ThemeManager::ReloadFont() - Invalidating DX11 device objects..."); ImGui_ImplDX11_InvalidateDeviceObjects(); + + logger::debug("ThemeManager::ReloadFont() - Creating DX11 device objects..."); + if (!ImGui_ImplDX11_CreateDeviceObjects()) { + logger::error("ThemeManager::ReloadFont() - Failed to create device objects!"); + + // Emergency fallback: try to restore with default font + io.Fonts->Clear(); + io.Fonts->AddFontDefault(); + io.Fonts->Build(); + ImGui_ImplDX11_InvalidateDeviceObjects(); + ImGui_ImplDX11_CreateDeviceObjects(); + + isReloading = false; + return; + } + + logger::debug("ThemeManager::ReloadFont() - Device objects recreated successfully"); io.FontGlobalScale = exp2(themeSettings.GlobalScale); cachedFontSize = themeSettings.FontSize; // Also update cached font name in the menu instance const_cast(menu).cachedFontName = themeSettings.FontName; + + logger::info("ThemeManager::ReloadFont() - Font reload completed successfully"); + isReloading = false; } // Theme management methods diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 5b79babdc1..514518de4e 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1399,6 +1399,7 @@ namespace Util std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); if (extension == ".ttf" || extension == ".otf") { + // Use the full filename (including extension) for proper font loading std::string fontFile = entry.path().filename().string(); fonts.push_back(fontFile); logger::debug("DiscoverFonts: Found font file: {}", fontFile); From d6598aebb632d4622274c4b26e648f84076e53bd Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 19:52:37 +1000 Subject: [PATCH 08/29] Fixes --- .../CommunityShaders/Themes/Amber.json | 2 -- .../CommunityShaders/Themes/Default.json | 4 +--- .../CommunityShaders/Themes/DragonBlood.json | 4 +--- .../CommunityShaders/Themes/DwemerBronze.json | 4 +--- .../CommunityShaders/Themes/Forest.json | 4 +--- .../CommunityShaders/Themes/HighContrast.json | 4 +--- .../CommunityShaders/Themes/Light.json | 4 +--- .../CommunityShaders/Themes/Mystic.json | 4 +--- .../CommunityShaders/Themes/NordicFrost.json | 4 +--- .../CommunityShaders/Themes/Ocean.json | 4 +--- src/Menu.cpp | 24 ++++++++++++++++--- src/Menu/ThemeManager.cpp | 20 ++++++++++++++-- src/Menu/ThemeManager.h | 3 +++ src/Utils/UI.cpp | 1 + 14 files changed, 52 insertions(+), 34 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json index b978950063..5ca28e3aa2 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -5,8 +5,6 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 26.8, - "GlobalScale": -0.02, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, "Palette": { diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 12688f62ac..140cf7c7d5 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -1,12 +1,10 @@ { "DisplayName": "Default Dark", "Description": "The classic Community Shaders dark theme with comprehensive styling", - "Version": "2.0.0", + "Version": "1.0.0", "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 27.0, - "GlobalScale": 0.0, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, "Palette": { diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 11907b8b27..4bf268098e 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 28.0, - "GlobalScale": 0.1, "ShowActionIcons": true, - "TooltipHoverDelay": 0.4, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index ec7ea64ed1..b0f9305828 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 27.5, - "GlobalScale": 0.05, "ShowActionIcons": true, - "TooltipHoverDelay": 0.3, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.15, 0.12, 0.08, 0.9], "Text": [0.9, 0.75, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json index 2cbc5455be..7a507cd336 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 26.5, - "GlobalScale": -0.05, "ShowActionIcons": true, - "TooltipHoverDelay": 0.6, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.1, 0.3, 0.15, 0.9], "Text": [0.9, 1.0, 0.9, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index 99c6e018f4..f4715ef1ad 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 29.0, - "GlobalScale": 0.05, "ShowActionIcons": true, - "TooltipHoverDelay": 0.3, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.0, 0.0, 0.0, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 76027c7a30..075cc58910 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 26.0, - "GlobalScale": -0.1, "ShowActionIcons": true, - "TooltipHoverDelay": 0.4, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.9, 0.9, 0.9, 0.95], "Text": [0.1, 0.1, 0.1, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json index 1214ac3f1d..773060f269 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 27.8, - "GlobalScale": 0.08, "ShowActionIcons": true, - "TooltipHoverDelay": 0.55, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.2, 0.1, 0.3, 0.9], "Text": [0.95, 0.9, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index 9ef7af1325..b466a0c447 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 26.0, - "GlobalScale": -0.08, "ShowActionIcons": true, - "TooltipHoverDelay": 0.4, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json index 9986c66128..1d06c6d500 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -5,10 +5,8 @@ "Author": "Community Shaders Team", "Theme": { "UseSimplePalette": false, - "FontSize": 27.5, - "GlobalScale": 0.0, "ShowActionIcons": true, - "TooltipHoverDelay": 0.45, + "TooltipHoverDelay": 0.5, "Palette": { "Background": [0.1, 0.2, 0.4, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], diff --git a/src/Menu.cpp b/src/Menu.cpp index 4f7b690ba9..432dc27361 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -244,7 +244,8 @@ void Menu::Init() float fontSize = settings.Theme.FontSize; - if (std::round(fontSize) != std::round(ThemeManager::Constants::DEFAULT_FONT_SIZE)) { + // Use dynamic font sizing when FontSize equals the default (indicating theme doesn't override) + if (std::round(fontSize) == std::round(ThemeManager::Constants::DEFAULT_FONT_SIZE)) { if (globals::state->screenSize.y > 0) { fontSize = globals::state->screenSize.y * ThemeManager::Constants::DEFAULT_FONT_RATIO; } else { @@ -264,7 +265,14 @@ void Menu::Init() // Initialize cached values for reload detection cachedFontName = settings.Theme.FontName; - imgui_io.FontGlobalScale = exp2(settings.Theme.GlobalScale); + float globalScale = settings.Theme.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - ThemeManager::Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = ThemeManager::Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + imgui_io.FontGlobalScale = exp2(globalScale); // Setup Platform/Renderer backends ImGui_ImplWin32_Init(desc.OutputWindow); @@ -313,6 +321,9 @@ void Menu::DrawSettings() focusChanged = false; } + // Apply theme styling with universal contrast enhancement + ThemeManager::SetupImGuiStyle(*this); + ImGui::DockSpaceOverViewport(NULL, ImGuiDockNodeFlags_PassthruCentralNode); ImGui::SetNextWindowPos(Util::GetNativeViewportSizeScaled(0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); @@ -336,7 +347,14 @@ void Menu::DrawSettings() bool isDocked = ImGui::IsWindowDocked(); wasDocked = isDocked; - const float uiScale = exp2(settings.Theme.GlobalScale); // Get current UI scale + float globalScale = settings.Theme.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - ThemeManager::Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = ThemeManager::Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + const float uiScale = exp2(globalScale); // Get current UI scale // Check if we can show icons - require setting enabled and at least some icons loaded (for undocked) // For docked mode, always show icons if textures are available bool canShowIcons = settings.Theme.ShowActionIcons && diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 880eb3bb59..4dc06ec0b0 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,15 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // rescale here auto styleCopy = themeSettings.Style; - styleCopy.ScaleAllSizes(exp2(themeSettings.GlobalScale)); + + float globalScale = themeSettings.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + styleCopy.ScaleAllSizes(exp2(globalScale)); styleCopy.MouseCursorScale = 1.f; style = styleCopy; style.HoverDelayNormal = themeSettings.TooltipHoverDelay; @@ -214,7 +223,14 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) logger::debug("ThemeManager::ReloadFont() - Device objects recreated successfully"); - io.FontGlobalScale = exp2(themeSettings.GlobalScale); + float globalScale = themeSettings.GlobalScale; + + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default + if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { + globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 + } + + io.FontGlobalScale = exp2(globalScale); cachedFontSize = themeSettings.FontSize; // Also update cached font name in the menu instance diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 6beacb47be..0f87a2c801 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -45,6 +45,9 @@ class ThemeManager static constexpr float MAX_FONT_SIZE = 108.0f; // 5.0% @ 2160px height static constexpr float DEFAULT_FONT_SIZE = 27.0f; + // Global scale constants + static constexpr float DEFAULT_GLOBAL_SCALE = 0.0f; // Default global scale for built-in themes + // Font configuration constants static constexpr int FCONF_OVERSAMPLE_H = 3; // ImGui default = 2 static constexpr int FCONF_OVERSAMPLE_V = 2; // ImGui default = 1 diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 514518de4e..8f5af6a89e 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include From 9d57cd0ca0311a6cc4fcedcc36646e808e8d274b Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 20:25:42 +1000 Subject: [PATCH 09/29] Fixes x2 --- src/Menu.cpp | 22 ++++++++++ src/Menu/MenuHeaderRenderer.cpp | 2 +- src/Menu/SettingsTabRenderer.cpp | 4 +- src/Menu/ThemeManager.cpp | 42 +++++++++++++----- src/Utils/UI.cpp | 75 +++++++++++++++++++++++--------- src/Utils/UI.h | 15 +++++-- 6 files changed, 121 insertions(+), 39 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 432dc27361..0157e5aa39 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -175,11 +175,25 @@ void Menu::LoadTheme(json& o_json) { if (o_json["Theme"].is_object()) { settings.Theme = o_json["Theme"]; + + // Validate the loaded font and fallback to default if necessary + if (!Util::ValidateFont(settings.Theme.FontName)) { + logger::warn("Font '{}' not found, falling back to default font '{}'", + settings.Theme.FontName, ThemeSettings{}.FontName); + settings.Theme.FontName = ThemeSettings{}.FontName; + } } } void Menu::SaveTheme(json& o_json) { + // Validate font before saving and fallback to default if necessary + if (!Util::ValidateFont(settings.Theme.FontName)) { + logger::warn("Font '{}' not found during save, falling back to default font '{}'", + settings.Theme.FontName, ThemeSettings{}.FontName); + settings.Theme.FontName = ThemeSettings{}.FontName; + } + o_json["Theme"] = settings.Theme; } @@ -202,6 +216,14 @@ bool Menu::LoadThemePreset(const std::string& themeName) if (themeManager->LoadTheme(themeName, themeSettings)) { settings.Theme = themeSettings; + + // Validate the loaded font and fallback to default if necessary + if (!Util::ValidateFont(settings.Theme.FontName)) { + logger::warn("Font '{}' from theme '{}' not found, falling back to default font '{}'", + settings.Theme.FontName, themeName, ThemeSettings{}.FontName); + settings.Theme.FontName = ThemeSettings{}.FontName; + } + settings.SelectedThemePreset = themeName; // Update cached values for font reload detection cachedFontName = settings.Theme.FontName; diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 4c7ab9b828..046a3ba4d5 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -90,7 +90,7 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow if (ImGui::BeginTable("##ActionButtons", 4, ImGuiTableFlags_SizingStretchSame)) { // Save Settings Button ImGui::TableNextColumn(); - if (Util::ButtonWithFeedback("Save Settings", { -1, 0 })) { + if (Util::ButtonWithFlash("Save Settings", { -1, 0 })) { globals::state->Save(); globals::state->SaveTheme(); } diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index e6ea3be7b5..f05e46ad8d 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -275,7 +275,7 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::SameLine(); const char* buttonText = isCreatingNewTheme ? "Save Theme" : "Update Theme"; - if (Util::ButtonWithFeedback(buttonText)) { + if (Util::ButtonWithFlash(buttonText)) { if (isCreatingNewTheme) { // Show popup for new theme creation showCreateThemePopup = true; @@ -338,7 +338,7 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::Separator(); // Buttons - if (Util::ButtonWithFeedback("Create Theme") && strlen(newThemeName) > 0) { + if (Util::ButtonWithFlash("Create Theme") && strlen(newThemeName) > 0) { // Use the existing SaveTheme method to serialize the theme settings json currentThemeJson; globals::menu->SaveTheme(currentThemeJson); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 4dc06ec0b0..21f3214eee 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -97,19 +97,39 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) return luminance > 0.5f ? ImVec4(0.0f, 0.0f, 0.0f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); }; - // Apply contrast-aware text for selection states + // Helper function to adjust background color for better contrast with text + auto adjustBackgroundForContrast = [&](ImVec4& backgroundColor, float textLuminance) { + float bgLuminance = calculateLuminance(backgroundColor); + + if (bgLuminance > 0.5f && textLuminance > 0.5f) { + // Both background and text are light - darken the background + backgroundColor.x *= 0.4f; + backgroundColor.y *= 0.4f; + backgroundColor.z *= 0.4f; + } else if (bgLuminance < 0.5f && textLuminance < 0.5f) { + // Both background and text are dark - lighten the background + backgroundColor.x = std::min(1.0f, backgroundColor.x + 0.3f); + backgroundColor.y = std::min(1.0f, backgroundColor.y + 0.3f); + backgroundColor.z = std::min(1.0f, backgroundColor.z + 0.3f); + } + }; + + // Apply contrast-aware adjustments for headers and tabs + float textLum = calculateLuminance(colors[ImGuiCol_Text]); + + // Apply contrast adjustments for all header and tab backgrounds using unified logic + adjustBackgroundForContrast(colors[ImGuiCol_Header], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_HeaderHovered], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_HeaderActive], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_Tab], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_TabActive], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_TabHovered], textLum); + + // Apply contrast-aware text for selection states (TextSelectedBg is used when text is selected) if (calculateLuminance(colors[ImGuiCol_HeaderActive]) > 0.5f) { colors[ImGuiCol_TextSelectedBg] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black text on light selection - } - if (calculateLuminance(colors[ImGuiCol_HeaderHovered]) > 0.5f) { - // For hovered items, we can't directly change text color, but we can adjust the hover background - // to ensure better contrast with the current text color - float textLum = calculateLuminance(colors[ImGuiCol_Text]); - if (textLum > 0.5f) { // If text is light, darken the hover background - ImVec4 darkerHover = colors[ImGuiCol_HeaderHovered]; - darkerHover.x *= 0.3f; darkerHover.y *= 0.3f; darkerHover.z *= 0.3f; - colors[ImGuiCol_HeaderHovered] = darkerHover; - } + } else { + colors[ImGuiCol_TextSelectedBg] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text on dark selection } // Apply scrollbar opacity settings diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 8f5af6a89e..01aeb45cf7 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,3 +1,5 @@ +#include "PCH.h" + #include "UI.h" #include "Menu.h" @@ -1330,37 +1332,43 @@ namespace Util } } // namespace ColorUtils - bool ButtonWithFeedback(const char* label, const ImVec2& size, int feedbackDurationMs) + bool ButtonWithFlash(const char* label, const ImVec2& size, int flashDurationMs) { - static std::unordered_map feedbackTimers; + static std::unordered_map flashTimers; std::string buttonId = std::string(label); auto now = std::chrono::steady_clock::now(); - // Check if this button has active feedback - bool hasActiveFeedback = false; - auto it = feedbackTimers.find(buttonId); - if (it != feedbackTimers.end()) { + // Check if this button has active flash + bool hasActiveFlash = false; + auto it = flashTimers.find(buttonId); + if (it != flashTimers.end()) { auto elapsed = std::chrono::duration_cast(now - it->second); - if (elapsed.count() < feedbackDurationMs) { - hasActiveFeedback = true; + if (elapsed.count() < flashDurationMs) { + hasActiveFlash = true; } else { - // Feedback expired, remove it - feedbackTimers.erase(it); + // Flash expired, remove it + flashTimers.erase(it); } } - // Style the button differently if it has active feedback + // Style the button with flash effect if active. bool styleChanged = false; - if (hasActiveFeedback) { - // Get success color from theme for feedback - ImVec4 successColor = globals::menu->GetTheme().StatusPalette.SuccessColor; - ImVec4 successHovered = ImVec4(successColor.x * 0.8f, successColor.y * 0.8f, successColor.z * 0.8f, successColor.w); - ImVec4 successActive = ImVec4(successColor.x * 0.6f, successColor.y * 0.6f, successColor.z * 0.6f, successColor.w); + if (hasActiveFlash) { + // Use subtle white overlay similar to action icon hover effect + ImVec4 normalButton = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImVec4 flashColor = ImVec4( + normalButton.x + 0.2f, // Brighten slightly + normalButton.y + 0.2f, + normalButton.z + 0.2f, + normalButton.w + ); + ImVec4 flashHovered = ImVec4(flashColor.x * 1.1f, flashColor.y * 1.1f, flashColor.z * 1.1f, flashColor.w); + ImVec4 flashActive = ImVec4(flashColor.x * 0.9f, flashColor.y * 0.9f, flashColor.z * 0.9f, flashColor.w); - ImGui::PushStyleColor(ImGuiCol_Button, successColor); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, successHovered); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, successActive); + ImGui::PushStyleColor(ImGuiCol_Button, flashColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, flashHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, flashActive); styleChanged = true; } @@ -1370,9 +1378,9 @@ namespace Util ImGui::PopStyleColor(3); } - // If clicked, start the feedback timer + // If clicked, start the flash timer if (clicked) { - feedbackTimers[buttonId] = now; + flashTimers[buttonId] = now; } return clicked; @@ -1419,4 +1427,29 @@ namespace Util return fonts; } + + bool ValidateFont(const std::string& fontName) + { + if (fontName.empty()) { + return false; + } + + try { + auto fontsPath = Util::PathHelpers::GetFontsPath(); + auto fontPath = fontsPath / fontName; + + // Check if the font file exists and is a regular file + if (std::filesystem::exists(fontPath) && std::filesystem::is_regular_file(fontPath)) { + // Validate extension is .ttf or .otf + auto extension = fontPath.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + return extension == ".ttf" || extension == ".otf"; + } + } + catch (const std::exception& e) { + logger::error("ValidateFont: Exception occurred while validating font '{}': {}", fontName, e.what()); + } + + return false; + } } // namespace Util diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 7ca212d406..b61c69c034 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -109,15 +109,15 @@ namespace Util int m_pushedStyles; }; + /** - * Creates a button with visual feedback that briefly shows success colors when clicked. - * Useful for save/update operations where users need confirmation that the action worked. + * Button with simple flash feedback (matches action icon hover effect style) * @param label Button text * @param size Button size (optional) - * @param feedbackDurationMs How long to show feedback colors in milliseconds (default 1000ms) + * @param flashDurationMs How long to show flash effect in milliseconds (default 200ms) * @return True if the button was clicked */ - bool ButtonWithFeedback(const char* label, const ImVec2& size = ImVec2(0, 0), int feedbackDurationMs = 1000); + bool ButtonWithFlash(const char* label, const ImVec2& size = ImVec2(0, 0), int flashDurationMs = 200); /** * Discovers available font files in the Fonts directory @@ -125,6 +125,13 @@ namespace Util */ std::vector DiscoverFonts(); + /** + * Validates if a font file exists in the Fonts directory + * @param fontName The font file name to check (including .ttf extension) + * @return True if the font exists, false otherwise + */ + bool ValidateFont(const std::string& fontName); + /** * RAII wrapper for creating collapsible UI sections. * Automatically handles the TreeNode creation, styling, and cleanup. From 0e9ed1562355c59bf7622d63e9399dabe4ccb9f1 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 20:26:24 +1000 Subject: [PATCH 10/29] Update Menu.cpp --- src/Menu.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Menu.cpp b/src/Menu.cpp index 0157e5aa39..60a94fc8fc 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -286,6 +286,7 @@ void Menu::Init() // Initialize cached values for reload detection cachedFontName = settings.Theme.FontName; + cachedFontSize = fontSize; // Update cached size to match the actually loaded font size float globalScale = settings.Theme.GlobalScale; From 9d07b6cafd773575ebb24e99ca4c066aa673ca6a Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 20:37:16 +1000 Subject: [PATCH 11/29] Update Default.json --- .../CommunityShaders/Themes/Default.json | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 140cf7c7d5..c2a4fa83bc 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -26,16 +26,47 @@ "ColorHovered": [0.6, 0.6, 0.6, 1.0], "MinimizedFactor": 0.7 }, + "ScrollbarOpacity": { + "Background": 0.0, + "Thumb": 0.5, + "ThumbHovered": 0.75, + "ThumbActive": 0.9 + }, "Style": { "WindowBorderSize": 2.0, "ChildBorderSize": 0.0, "FrameBorderSize": 1.0, - "WindowPadding": [12.0, 12.0], - "WindowRounding": 4.0, + "WindowPadding": [8.0, 8.0], + "WindowRounding": 12.0, "IndentSpacing": 8.0, - "FramePadding": [6.0, 4.0], - "CellPadding": [12.0, 4.0], - "ItemSpacing": [8.0, 8.0] + "FramePadding": [8.0, 4.0], + "CellPadding": [8.0, 2.0], + "ItemSpacing": [4.0, 8.0], + "FrameRounding": 4.0, + "TabRounding": 4.0, + "ScrollbarRounding": 9.0, + "ScrollbarSize": 12.0, + "GrabRounding": 3.0, + "GrabMinSize": 12.0, + "ItemInnerSpacing": [4.0, 4.0], + "ButtonTextAlign": [0.5, 0.5], + "SelectableTextAlign": [0.0, 0.0], + "SeparatorTextAlign": [0.0, 0.5], + "SeparatorTextPadding": [20.0, 3.0], + "SeparatorTextBorderSize": 3.0, + "WindowMinSize": [32.0, 32.0], + "ChildRounding": 0.0, + "PopupRounding": 0.0, + "PopupBorderSize": 1.0, + "TabBorderSize": 0.0, + "TabBarBorderSize": 1.0, + "TabMinWidthForCloseButton": 0.0, + "ColorButtonPosition": 0, + "ColumnsMinSpacing": 6.0, + "DockingSeparatorSize": 2.0, + "LogSliderDeadzone": 4.0, + "MouseCursorScale": 1.0, + "TableAngledHeadersAngle": 0.611 }, "FullPalette": [ [0.90, 0.90, 0.90, 0.90], @@ -59,12 +90,12 @@ [1.00, 1.00, 1.00, 1.00], [0.70, 0.70, 0.70, 1.00], [0.26, 0.26, 0.26, 1.00], - [0.26, 0.59, 0.98, 0.40], - [0.26, 0.59, 0.98, 0.67], - [0.26, 0.59, 0.98, 1.00], - [0.06, 0.53, 0.98, 1.00], - [0.26, 0.59, 0.98, 1.00], - [0.26, 0.59, 0.98, 1.00], + [0.26, 0.59, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.20], + [0.26, 0.59, 0.98, 0.59], + [0.06, 0.53, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.20], + [0.26, 0.59, 0.98, 0.59], [0.50, 0.50, 0.50, 1.00], [0.70, 0.60, 0.60, 1.00], [0.90, 0.70, 0.70, 1.00], From f10af11be1ae8b3466b70e5708a3edf90f82a119 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 21:02:41 +1000 Subject: [PATCH 12/29] Update Menu.h --- src/Menu.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Menu.h b/src/Menu.h index 111798e806..d192109f4c 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -33,10 +33,10 @@ class Menu void LoadTheme(json& o_json); void SaveTheme(json& o_json); - // Multi-theme support (deprecated - use ThemeManager) + // Multi-theme support std::vector DiscoverThemes(); bool LoadThemePreset(const std::string& themeName); - void CreateDefaultThemes(); // Deprecated - creates JSON files instead + void CreateDefaultThemes(); void Init(); void DrawSettings(); From 3dcec76f734091ad9a8c59b2c3b8fdcd4756ead6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:15:11 +0000 Subject: [PATCH 13/29] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../CommunityShaders/Themes/Amber.json | 194 ++++++------- .../CommunityShaders/Themes/Default.json | 256 ++++++++--------- .../CommunityShaders/Themes/DragonBlood.json | 194 ++++++------- .../CommunityShaders/Themes/DwemerBronze.json | 194 ++++++------- .../CommunityShaders/Themes/Forest.json | 194 ++++++------- .../CommunityShaders/Themes/HighContrast.json | 194 ++++++------- .../CommunityShaders/Themes/Light.json | 194 ++++++------- .../CommunityShaders/Themes/Mystic.json | 194 ++++++------- .../CommunityShaders/Themes/NordicFrost.json | 194 ++++++------- .../CommunityShaders/Themes/Ocean.json | 194 ++++++------- .../Plugins/CommunityShaders/Themes/README.md | 90 +++--- src/Menu.cpp | 37 ++- src/Menu.h | 18 +- src/Menu/FeatureListRenderer.cpp | 4 +- src/Menu/MenuHeaderRenderer.cpp | 4 +- src/Menu/OverlayRenderer.cpp | 4 +- src/Menu/SettingsTabRenderer.cpp | 61 ++-- src/Menu/ThemeManager.cpp | 98 +++---- src/Menu/ThemeManager.h | 22 +- src/State.cpp | 6 +- src/Utils/UI.cpp | 262 +++++++++--------- src/Utils/UI.h | 3 +- src/XSEPlugin.cpp | 6 +- 23 files changed, 1299 insertions(+), 1318 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json index 5ca28e3aa2..75bd0a4039 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -1,98 +1,98 @@ { - "DisplayName": "Warm Amber", - "Description": "Cozy amber tones reminiscent of hearth fires and candlelight in Nordic taverns", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.2, 0.15, 0.05, 0.9], - "Text": [1.0, 0.9, 0.7, 1.0], - "Border": [0.8, 0.6, 0.3, 0.8] - }, - "StatusPalette": { - "Disable": [0.5, 0.4, 0.3, 1.0], - "Error": [1.0, 0.4, 0.2, 1.0], - "Warning": [1.0, 0.7, 0.1, 1.0], - "RestartNeeded": [0.8, 0.9, 0.3, 1.0], - "CurrentHotkey": [1.0, 0.8, 0.4, 1.0], - "SuccessColor": [0.6, 0.8, 0.3, 1.0], - "InfoColor": [0.7, 0.8, 0.9, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.9, 0.8, 0.6, 1.0], - "ColorHovered": [1.0, 0.9, 0.7, 1.0], - "MinimizedFactor": 0.65 - }, - "Style": { - "WindowBorderSize": 2.0, - "ChildBorderSize": 1.0, - "FrameBorderSize": 1.5, - "WindowPadding": [14.0, 12.0], - "WindowRounding": 5.0, - "IndentSpacing": 8.0, - "FramePadding": [7.0, 5.0], - "CellPadding": [12.0, 5.0], - "ItemSpacing": [8.0, 8.0] - }, - "FullPalette": [ - [1.00, 0.90, 0.70, 0.90], - [0.80, 0.70, 0.50, 1.00], - [0.20, 0.15, 0.05, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.18, 0.13, 0.04, 0.85], - [0.70, 0.60, 0.40, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.15, 0.10, 0.03, 1.00], - [0.40, 0.30, 0.15, 0.40], - [0.50, 0.40, 0.20, 0.45], - [0.15, 0.10, 0.03, 0.83], - [0.18, 0.13, 0.05, 0.87], - [0.30, 0.22, 0.10, 0.90], - [0.22, 0.16, 0.06, 0.90], - [0.35, 0.26, 0.12, 0.90], - [0.45, 0.35, 0.18, 1.00], - [0.55, 0.45, 0.25, 1.00], - [0.65, 0.55, 0.35, 1.00], - [1.00, 0.90, 0.70, 1.00], - [0.80, 0.70, 0.50, 1.00], - [0.40, 0.30, 0.15, 1.00], - [0.80, 0.60, 0.30, 0.40], - [0.90, 0.70, 0.40, 0.67], - [1.00, 0.80, 0.50, 1.00], - [0.95, 0.65, 0.25, 1.00], - [1.00, 0.75, 0.35, 1.00], - [1.00, 0.85, 0.45, 1.00], - [0.70, 0.55, 0.30, 1.00], - [0.80, 0.65, 0.40, 1.00], - [0.95, 0.80, 0.55, 1.00], - [1.00, 0.90, 0.70, 1.00], - [1.00, 0.90, 0.70, 0.60], - [1.00, 0.90, 0.70, 0.90], - [0.80, 0.60, 0.30, 0.31], - [0.90, 0.70, 0.40, 0.80], - [1.00, 0.80, 0.50, 1.00], - [0.18, 0.13, 0.04, 0.97], - [0.85, 0.70, 0.40, 1.00], - [0.70, 0.55, 0.30, 0.50], - [0.00, 0.00, 0.00, 0.00], - [1.00, 0.90, 0.70, 1.00], - [1.00, 0.80, 0.30, 1.00], - [1.00, 0.80, 0.30, 1.00], - [1.00, 0.80, 0.30, 1.00], - [0.80, 0.60, 0.30, 0.40], - [0.35, 0.26, 0.12, 1.00], - [0.28, 0.20, 0.09, 1.00], - [0.00, 0.00, 0.00, 0.00], - [1.00, 0.90, 0.70, 0.06], - [0.80, 0.60, 0.30, 0.35], - [0.90, 0.50, 0.30, 1.00], - [0.90, 0.70, 0.40, 1.00], - [0.50, 0.40, 0.20, 0.56], - [0.35, 0.26, 0.12, 0.35], - [0.35, 0.26, 0.12, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Warm Amber", + "Description": "Cozy amber tones reminiscent of hearth fires and candlelight in Nordic taverns", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.2, 0.15, 0.05, 0.9], + "Text": [1.0, 0.9, 0.7, 1.0], + "Border": [0.8, 0.6, 0.3, 0.8] + }, + "StatusPalette": { + "Disable": [0.5, 0.4, 0.3, 1.0], + "Error": [1.0, 0.4, 0.2, 1.0], + "Warning": [1.0, 0.7, 0.1, 1.0], + "RestartNeeded": [0.8, 0.9, 0.3, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.4, 1.0], + "SuccessColor": [0.6, 0.8, 0.3, 1.0], + "InfoColor": [0.7, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.9, 0.8, 0.6, 1.0], + "ColorHovered": [1.0, 0.9, 0.7, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [14.0, 12.0], + "WindowRounding": 5.0, + "IndentSpacing": 8.0, + "FramePadding": [7.0, 5.0], + "CellPadding": [12.0, 5.0], + "ItemSpacing": [8.0, 8.0] + }, + "FullPalette": [ + [1.0, 0.9, 0.7, 0.9], + [0.8, 0.7, 0.5, 1.0], + [0.2, 0.15, 0.05, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.18, 0.13, 0.04, 0.85], + [0.7, 0.6, 0.4, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.15, 0.1, 0.03, 1.0], + [0.4, 0.3, 0.15, 0.4], + [0.5, 0.4, 0.2, 0.45], + [0.15, 0.1, 0.03, 0.83], + [0.18, 0.13, 0.05, 0.87], + [0.3, 0.22, 0.1, 0.9], + [0.22, 0.16, 0.06, 0.9], + [0.35, 0.26, 0.12, 0.9], + [0.45, 0.35, 0.18, 1.0], + [0.55, 0.45, 0.25, 1.0], + [0.65, 0.55, 0.35, 1.0], + [1.0, 0.9, 0.7, 1.0], + [0.8, 0.7, 0.5, 1.0], + [0.4, 0.3, 0.15, 1.0], + [0.8, 0.6, 0.3, 0.4], + [0.9, 0.7, 0.4, 0.67], + [1.0, 0.8, 0.5, 1.0], + [0.95, 0.65, 0.25, 1.0], + [1.0, 0.75, 0.35, 1.0], + [1.0, 0.85, 0.45, 1.0], + [0.7, 0.55, 0.3, 1.0], + [0.8, 0.65, 0.4, 1.0], + [0.95, 0.8, 0.55, 1.0], + [1.0, 0.9, 0.7, 1.0], + [1.0, 0.9, 0.7, 0.6], + [1.0, 0.9, 0.7, 0.9], + [0.8, 0.6, 0.3, 0.31], + [0.9, 0.7, 0.4, 0.8], + [1.0, 0.8, 0.5, 1.0], + [0.18, 0.13, 0.04, 0.97], + [0.85, 0.7, 0.4, 1.0], + [0.7, 0.55, 0.3, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.9, 0.7, 1.0], + [1.0, 0.8, 0.3, 1.0], + [1.0, 0.8, 0.3, 1.0], + [1.0, 0.8, 0.3, 1.0], + [0.8, 0.6, 0.3, 0.4], + [0.35, 0.26, 0.12, 1.0], + [0.28, 0.2, 0.09, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.9, 0.7, 0.06], + [0.8, 0.6, 0.3, 0.35], + [0.9, 0.5, 0.3, 1.0], + [0.9, 0.7, 0.4, 1.0], + [0.5, 0.4, 0.2, 0.56], + [0.35, 0.26, 0.12, 0.35], + [0.35, 0.26, 0.12, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index c2a4fa83bc..84c66b7ffd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -1,129 +1,129 @@ { - "DisplayName": "Default Dark", - "Description": "The classic Community Shaders dark theme with comprehensive styling", - "Version": "1.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.09, 0.09, 0.09, 0.95], - "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [0.5, 0.5, 0.5, 0.8] - }, - "StatusPalette": { - "Disable": [0.5, 0.5, 0.5, 1.0], - "Error": [1.0, 0.4, 0.4, 1.0], - "Warning": [1.0, 0.6, 0.2, 1.0], - "RestartNeeded": [0.4, 1.0, 0.4, 1.0], - "CurrentHotkey": [1.0, 1.0, 0.0, 1.0], - "SuccessColor": [0.0, 1.0, 0.0, 1.0], - "InfoColor": [0.2, 0.6, 1.0, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.8, 0.8, 0.8, 1.0], - "ColorHovered": [0.6, 0.6, 0.6, 1.0], - "MinimizedFactor": 0.7 - }, - "ScrollbarOpacity": { - "Background": 0.0, - "Thumb": 0.5, - "ThumbHovered": 0.75, - "ThumbActive": 0.9 - }, - "Style": { - "WindowBorderSize": 2.0, - "ChildBorderSize": 0.0, - "FrameBorderSize": 1.0, - "WindowPadding": [8.0, 8.0], - "WindowRounding": 12.0, - "IndentSpacing": 8.0, - "FramePadding": [8.0, 4.0], - "CellPadding": [8.0, 2.0], - "ItemSpacing": [4.0, 8.0], - "FrameRounding": 4.0, - "TabRounding": 4.0, - "ScrollbarRounding": 9.0, - "ScrollbarSize": 12.0, - "GrabRounding": 3.0, - "GrabMinSize": 12.0, - "ItemInnerSpacing": [4.0, 4.0], - "ButtonTextAlign": [0.5, 0.5], - "SelectableTextAlign": [0.0, 0.0], - "SeparatorTextAlign": [0.0, 0.5], - "SeparatorTextPadding": [20.0, 3.0], - "SeparatorTextBorderSize": 3.0, - "WindowMinSize": [32.0, 32.0], - "ChildRounding": 0.0, - "PopupRounding": 0.0, - "PopupBorderSize": 1.0, - "TabBorderSize": 0.0, - "TabBarBorderSize": 1.0, - "TabMinWidthForCloseButton": 0.0, - "ColorButtonPosition": 0, - "ColumnsMinSpacing": 6.0, - "DockingSeparatorSize": 2.0, - "LogSliderDeadzone": 4.0, - "MouseCursorScale": 1.0, - "TableAngledHeadersAngle": 0.611 - }, - "FullPalette": [ - [0.90, 0.90, 0.90, 0.90], - [0.60, 0.60, 0.60, 1.00], - [0.09, 0.09, 0.09, 0.95], - [0.00, 0.00, 0.00, 0.00], - [0.05, 0.05, 0.10, 0.85], - [0.70, 0.70, 0.70, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.00, 0.00, 0.00, 1.00], - [0.26, 0.26, 0.26, 0.40], - [0.40, 0.40, 0.40, 0.45], - [0.00, 0.00, 0.00, 0.83], - [0.00, 0.00, 0.00, 0.87], - [0.20, 0.20, 0.30, 0.90], - [0.02, 0.02, 0.03, 0.90], - [0.20, 0.22, 0.27, 0.90], - [0.28, 0.28, 0.28, 1.00], - [0.42, 0.42, 0.42, 1.00], - [0.56, 0.56, 0.56, 1.00], - [1.00, 1.00, 1.00, 1.00], - [0.70, 0.70, 0.70, 1.00], - [0.26, 0.26, 0.26, 1.00], - [0.26, 0.59, 0.98, 0.39], - [0.26, 0.59, 0.98, 0.20], - [0.26, 0.59, 0.98, 0.59], - [0.06, 0.53, 0.98, 0.39], - [0.26, 0.59, 0.98, 0.20], - [0.26, 0.59, 0.98, 0.59], - [0.50, 0.50, 0.50, 1.00], - [0.70, 0.60, 0.60, 1.00], - [0.90, 0.70, 0.70, 1.00], - [1.00, 1.00, 1.00, 1.00], - [1.00, 1.00, 1.00, 0.60], - [1.00, 1.00, 1.00, 0.90], - [0.26, 0.59, 0.98, 0.31], - [0.26, 0.59, 0.98, 0.80], - [0.26, 0.59, 0.98, 1.00], - [0.15, 0.15, 0.15, 0.97], - [0.26, 0.59, 0.98, 1.00], - [0.70, 0.60, 0.60, 0.50], - [0.00, 0.00, 0.00, 0.00], - [1.00, 1.00, 1.00, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.26, 0.59, 0.98, 0.40], - [0.26, 0.26, 0.26, 1.00], - [0.19, 0.19, 0.19, 1.00], - [0.00, 0.00, 0.00, 0.00], - [1.00, 1.00, 1.00, 0.06], - [0.26, 0.59, 0.98, 0.35], - [0.80, 0.50, 0.50, 1.00], - [0.26, 0.59, 0.98, 1.00], - [0.30, 0.30, 0.30, 0.56], - [0.20, 0.20, 0.20, 0.35], - [0.20, 0.20, 0.20, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Default Dark", + "Description": "The classic Community Shaders dark theme with comprehensive styling", + "Version": "1.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.09, 0.09, 0.09, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.5, 0.5, 0.5, 0.8] + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.4, 0.4, 1.0], + "Warning": [1.0, 0.6, 0.2, 1.0], + "RestartNeeded": [0.4, 1.0, 0.4, 1.0], + "CurrentHotkey": [1.0, 1.0, 0.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.2, 0.6, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.8, 0.8, 1.0], + "ColorHovered": [0.6, 0.6, 0.6, 1.0], + "MinimizedFactor": 0.7 + }, + "ScrollbarOpacity": { + "Background": 0.0, + "Thumb": 0.5, + "ThumbHovered": 0.75, + "ThumbActive": 0.9 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 0.0, + "FrameBorderSize": 1.0, + "WindowPadding": [8.0, 8.0], + "WindowRounding": 12.0, + "IndentSpacing": 8.0, + "FramePadding": [8.0, 4.0], + "CellPadding": [8.0, 2.0], + "ItemSpacing": [4.0, 8.0], + "FrameRounding": 4.0, + "TabRounding": 4.0, + "ScrollbarRounding": 9.0, + "ScrollbarSize": 12.0, + "GrabRounding": 3.0, + "GrabMinSize": 12.0, + "ItemInnerSpacing": [4.0, 4.0], + "ButtonTextAlign": [0.5, 0.5], + "SelectableTextAlign": [0.0, 0.0], + "SeparatorTextAlign": [0.0, 0.5], + "SeparatorTextPadding": [20.0, 3.0], + "SeparatorTextBorderSize": 3.0, + "WindowMinSize": [32.0, 32.0], + "ChildRounding": 0.0, + "PopupRounding": 0.0, + "PopupBorderSize": 1.0, + "TabBorderSize": 0.0, + "TabBarBorderSize": 1.0, + "TabMinWidthForCloseButton": 0.0, + "ColorButtonPosition": 0, + "ColumnsMinSpacing": 6.0, + "DockingSeparatorSize": 2.0, + "LogSliderDeadzone": 4.0, + "MouseCursorScale": 1.0, + "TableAngledHeadersAngle": 0.611 + }, + "FullPalette": [ + [0.9, 0.9, 0.9, 0.9], + [0.6, 0.6, 0.6, 1.0], + [0.09, 0.09, 0.09, 0.95], + [0.0, 0.0, 0.0, 0.0], + [0.05, 0.05, 0.1, 0.85], + [0.7, 0.7, 0.7, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.26, 0.26, 0.26, 0.4], + [0.4, 0.4, 0.4, 0.45], + [0.0, 0.0, 0.0, 0.83], + [0.0, 0.0, 0.0, 0.87], + [0.2, 0.2, 0.3, 0.9], + [0.02, 0.02, 0.03, 0.9], + [0.2, 0.22, 0.27, 0.9], + [0.28, 0.28, 0.28, 1.0], + [0.42, 0.42, 0.42, 1.0], + [0.56, 0.56, 0.56, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.7, 0.7, 0.7, 1.0], + [0.26, 0.26, 0.26, 1.0], + [0.26, 0.59, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.2], + [0.26, 0.59, 0.98, 0.59], + [0.06, 0.53, 0.98, 0.39], + [0.26, 0.59, 0.98, 0.2], + [0.26, 0.59, 0.98, 0.59], + [0.5, 0.5, 0.5, 1.0], + [0.7, 0.6, 0.6, 1.0], + [0.9, 0.7, 0.7, 1.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.6], + [1.0, 1.0, 1.0, 0.9], + [0.26, 0.59, 0.98, 0.31], + [0.26, 0.59, 0.98, 0.8], + [0.26, 0.59, 0.98, 1.0], + [0.15, 0.15, 0.15, 0.97], + [0.26, 0.59, 0.98, 1.0], + [0.7, 0.6, 0.6, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.26, 0.59, 0.98, 0.4], + [0.26, 0.26, 0.26, 1.0], + [0.19, 0.19, 0.19, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.06], + [0.26, 0.59, 0.98, 0.35], + [0.8, 0.5, 0.5, 1.0], + [0.26, 0.59, 0.98, 1.0], + [0.3, 0.3, 0.3, 0.56], + [0.2, 0.2, 0.2, 0.35], + [0.2, 0.2, 0.2, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 4bf268098e..53886801d5 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -1,98 +1,98 @@ { - "DisplayName": "Dragon Blood", - "Description": "Dark red theme inspired by dragon lore and ancient power", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.25, 0.05, 0.05, 0.9], - "Text": [1.0, 0.85, 0.85, 1.0], - "Border": [0.8, 0.3, 0.3, 0.8] - }, - "StatusPalette": { - "Disable": [0.4, 0.2, 0.2, 1.0], - "Error": [1.0, 0.1, 0.1, 1.0], - "Warning": [1.0, 0.5, 0.0, 1.0], - "RestartNeeded": [0.8, 0.6, 0.2, 1.0], - "CurrentHotkey": [1.0, 0.8, 0.2, 1.0], - "SuccessColor": [0.6, 0.8, 0.2, 1.0], - "InfoColor": [0.8, 0.4, 0.6, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.9, 0.6, 0.6, 1.0], - "ColorHovered": [1.0, 0.7, 0.7, 1.0], - "MinimizedFactor": 0.6 - }, - "Style": { - "WindowBorderSize": 3.0, - "ChildBorderSize": 1.0, - "FrameBorderSize": 2.0, - "WindowPadding": [16.0, 14.0], - "WindowRounding": 8.0, - "IndentSpacing": 10.0, - "FramePadding": [8.0, 6.0], - "CellPadding": [14.0, 6.0], - "ItemSpacing": [10.0, 10.0] - }, - "FullPalette": [ - [1.00, 0.85, 0.85, 0.90], - [0.80, 0.60, 0.60, 1.00], - [0.25, 0.05, 0.05, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.20, 0.03, 0.03, 0.85], - [0.60, 0.40, 0.40, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.15, 0.00, 0.00, 1.00], - [0.40, 0.15, 0.15, 0.40], - [0.50, 0.20, 0.20, 0.45], - [0.10, 0.00, 0.00, 0.83], - [0.15, 0.00, 0.00, 0.87], - [0.30, 0.10, 0.10, 0.90], - [0.25, 0.05, 0.05, 0.90], - [0.35, 0.15, 0.15, 0.90], - [0.45, 0.15, 0.15, 1.00], - [0.60, 0.25, 0.25, 1.00], - [0.75, 0.35, 0.35, 1.00], - [1.00, 0.85, 0.85, 1.00], - [0.80, 0.60, 0.60, 1.00], - [0.40, 0.15, 0.15, 1.00], - [0.80, 0.30, 0.30, 0.40], - [0.90, 0.40, 0.40, 0.67], - [1.00, 0.50, 0.50, 1.00], - [0.85, 0.25, 0.25, 1.00], - [0.90, 0.35, 0.35, 1.00], - [0.95, 0.45, 0.45, 1.00], - [0.60, 0.30, 0.30, 1.00], - [0.80, 0.40, 0.40, 1.00], - [0.95, 0.55, 0.55, 1.00], - [1.00, 0.85, 0.85, 1.00], - [1.00, 0.85, 0.85, 0.60], - [1.00, 0.85, 0.85, 0.90], - [0.80, 0.30, 0.30, 0.31], - [0.90, 0.40, 0.40, 0.80], - [1.00, 0.50, 0.50, 1.00], - [0.20, 0.05, 0.05, 0.97], - [0.85, 0.35, 0.35, 1.00], - [0.70, 0.30, 0.30, 0.50], - [0.00, 0.00, 0.00, 0.00], - [1.00, 0.85, 0.85, 1.00], - [1.00, 0.60, 0.20, 1.00], - [1.00, 0.60, 0.20, 1.00], - [1.00, 0.60, 0.20, 1.00], - [0.80, 0.30, 0.30, 0.40], - [0.40, 0.15, 0.15, 1.00], - [0.30, 0.10, 0.10, 1.00], - [0.00, 0.00, 0.00, 0.00], - [1.00, 0.85, 0.85, 0.06], - [0.80, 0.30, 0.30, 0.35], - [1.00, 0.30, 0.30, 1.00], - [0.90, 0.40, 0.40, 1.00], - [0.50, 0.20, 0.20, 0.56], - [0.35, 0.15, 0.15, 0.35], - [0.35, 0.15, 0.15, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Dragon Blood", + "Description": "Dark red theme inspired by dragon lore and ancient power", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.25, 0.05, 0.05, 0.9], + "Text": [1.0, 0.85, 0.85, 1.0], + "Border": [0.8, 0.3, 0.3, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.2, 0.2, 1.0], + "Error": [1.0, 0.1, 0.1, 1.0], + "Warning": [1.0, 0.5, 0.0, 1.0], + "RestartNeeded": [0.8, 0.6, 0.2, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.2, 1.0], + "SuccessColor": [0.6, 0.8, 0.2, 1.0], + "InfoColor": [0.8, 0.4, 0.6, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.9, 0.6, 0.6, 1.0], + "ColorHovered": [1.0, 0.7, 0.7, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 3.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 8.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 10.0] + }, + "FullPalette": [ + [1.0, 0.85, 0.85, 0.9], + [0.8, 0.6, 0.6, 1.0], + [0.25, 0.05, 0.05, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.2, 0.03, 0.03, 0.85], + [0.6, 0.4, 0.4, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.15, 0.0, 0.0, 1.0], + [0.4, 0.15, 0.15, 0.4], + [0.5, 0.2, 0.2, 0.45], + [0.1, 0.0, 0.0, 0.83], + [0.15, 0.0, 0.0, 0.87], + [0.3, 0.1, 0.1, 0.9], + [0.25, 0.05, 0.05, 0.9], + [0.35, 0.15, 0.15, 0.9], + [0.45, 0.15, 0.15, 1.0], + [0.6, 0.25, 0.25, 1.0], + [0.75, 0.35, 0.35, 1.0], + [1.0, 0.85, 0.85, 1.0], + [0.8, 0.6, 0.6, 1.0], + [0.4, 0.15, 0.15, 1.0], + [0.8, 0.3, 0.3, 0.4], + [0.9, 0.4, 0.4, 0.67], + [1.0, 0.5, 0.5, 1.0], + [0.85, 0.25, 0.25, 1.0], + [0.9, 0.35, 0.35, 1.0], + [0.95, 0.45, 0.45, 1.0], + [0.6, 0.3, 0.3, 1.0], + [0.8, 0.4, 0.4, 1.0], + [0.95, 0.55, 0.55, 1.0], + [1.0, 0.85, 0.85, 1.0], + [1.0, 0.85, 0.85, 0.6], + [1.0, 0.85, 0.85, 0.9], + [0.8, 0.3, 0.3, 0.31], + [0.9, 0.4, 0.4, 0.8], + [1.0, 0.5, 0.5, 1.0], + [0.2, 0.05, 0.05, 0.97], + [0.85, 0.35, 0.35, 1.0], + [0.7, 0.3, 0.3, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.85, 0.85, 1.0], + [1.0, 0.6, 0.2, 1.0], + [1.0, 0.6, 0.2, 1.0], + [1.0, 0.6, 0.2, 1.0], + [0.8, 0.3, 0.3, 0.4], + [0.4, 0.15, 0.15, 1.0], + [0.3, 0.1, 0.1, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.85, 0.85, 0.06], + [0.8, 0.3, 0.3, 0.35], + [1.0, 0.3, 0.3, 1.0], + [0.9, 0.4, 0.4, 1.0], + [0.5, 0.2, 0.2, 0.56], + [0.35, 0.15, 0.15, 0.35], + [0.35, 0.15, 0.15, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index b0f9305828..2657e7ed42 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -1,98 +1,98 @@ { - "DisplayName": "Dwemer Bronze", - "Description": "Ancient bronze theme inspired by lost Dwemer technology and metallic machinery", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.15, 0.12, 0.08, 0.9], - "Text": [0.9, 0.75, 0.5, 1.0], - "Border": [0.7, 0.5, 0.3, 0.8] - }, - "StatusPalette": { - "Disable": [0.4, 0.35, 0.25, 1.0], - "Error": [0.9, 0.3, 0.1, 1.0], - "Warning": [1.0, 0.7, 0.2, 1.0], - "RestartNeeded": [0.8, 0.8, 0.4, 1.0], - "CurrentHotkey": [1.0, 0.8, 0.3, 1.0], - "SuccessColor": [0.5, 0.7, 0.3, 1.0], - "InfoColor": [0.6, 0.7, 0.8, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.8, 0.65, 0.4, 1.0], - "ColorHovered": [0.95, 0.8, 0.55, 1.0], - "MinimizedFactor": 0.7 - }, - "Style": { - "WindowBorderSize": 3.0, - "ChildBorderSize": 2.0, - "FrameBorderSize": 2.0, - "WindowPadding": [16.0, 14.0], - "WindowRounding": 2.0, - "IndentSpacing": 10.0, - "FramePadding": [8.0, 6.0], - "CellPadding": [14.0, 6.0], - "ItemSpacing": [10.0, 9.0] - }, - "FullPalette": [ - [0.90, 0.75, 0.50, 0.90], - [0.70, 0.60, 0.40, 1.00], - [0.15, 0.12, 0.08, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.13, 0.10, 0.06, 0.85], - [0.60, 0.50, 0.35, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.10, 0.08, 0.05, 1.00], - [0.35, 0.28, 0.18, 0.40], - [0.45, 0.36, 0.23, 0.45], - [0.10, 0.08, 0.05, 0.83], - [0.13, 0.10, 0.07, 0.87], - [0.25, 0.20, 0.13, 0.90], - [0.18, 0.14, 0.09, 0.90], - [0.30, 0.24, 0.15, 0.90], - [0.40, 0.32, 0.20, 1.00], - [0.50, 0.42, 0.28, 1.00], - [0.60, 0.52, 0.38, 1.00], - [0.90, 0.75, 0.50, 1.00], - [0.70, 0.60, 0.40, 1.00], - [0.35, 0.28, 0.18, 1.00], - [0.70, 0.50, 0.30, 0.40], - [0.80, 0.60, 0.35, 0.67], - [0.90, 0.70, 0.45, 1.00], - [0.85, 0.55, 0.25, 1.00], - [0.90, 0.65, 0.35, 1.00], - [0.95, 0.75, 0.45, 1.00], - [0.65, 0.50, 0.30, 1.00], - [0.75, 0.60, 0.40, 1.00], - [0.85, 0.70, 0.50, 1.00], - [0.90, 0.75, 0.50, 1.00], - [0.90, 0.75, 0.50, 0.60], - [0.90, 0.75, 0.50, 0.90], - [0.70, 0.50, 0.30, 0.31], - [0.80, 0.60, 0.35, 0.80], - [0.90, 0.70, 0.45, 1.00], - [0.13, 0.10, 0.06, 0.97], - [0.75, 0.60, 0.35, 1.00], - [0.65, 0.50, 0.30, 0.50], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.75, 0.50, 1.00], - [0.90, 0.70, 0.30, 1.00], - [0.90, 0.70, 0.30, 1.00], - [0.90, 0.70, 0.30, 1.00], - [0.70, 0.50, 0.30, 0.40], - [0.30, 0.24, 0.15, 1.00], - [0.23, 0.18, 0.11, 1.00], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.75, 0.50, 0.06], - [0.70, 0.50, 0.30, 0.35], - [0.80, 0.45, 0.25, 1.00], - [0.80, 0.60, 0.35, 1.00], - [0.45, 0.36, 0.23, 0.56], - [0.30, 0.24, 0.15, 0.35], - [0.30, 0.24, 0.15, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Dwemer Bronze", + "Description": "Ancient bronze theme inspired by lost Dwemer technology and metallic machinery", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.15, 0.12, 0.08, 0.9], + "Text": [0.9, 0.75, 0.5, 1.0], + "Border": [0.7, 0.5, 0.3, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.35, 0.25, 1.0], + "Error": [0.9, 0.3, 0.1, 1.0], + "Warning": [1.0, 0.7, 0.2, 1.0], + "RestartNeeded": [0.8, 0.8, 0.4, 1.0], + "CurrentHotkey": [1.0, 0.8, 0.3, 1.0], + "SuccessColor": [0.5, 0.7, 0.3, 1.0], + "InfoColor": [0.6, 0.7, 0.8, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.65, 0.4, 1.0], + "ColorHovered": [0.95, 0.8, 0.55, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 3.0, + "ChildBorderSize": 2.0, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 2.0, + "IndentSpacing": 10.0, + "FramePadding": [8.0, 6.0], + "CellPadding": [14.0, 6.0], + "ItemSpacing": [10.0, 9.0] + }, + "FullPalette": [ + [0.9, 0.75, 0.5, 0.9], + [0.7, 0.6, 0.4, 1.0], + [0.15, 0.12, 0.08, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.13, 0.1, 0.06, 0.85], + [0.6, 0.5, 0.35, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.1, 0.08, 0.05, 1.0], + [0.35, 0.28, 0.18, 0.4], + [0.45, 0.36, 0.23, 0.45], + [0.1, 0.08, 0.05, 0.83], + [0.13, 0.1, 0.07, 0.87], + [0.25, 0.2, 0.13, 0.9], + [0.18, 0.14, 0.09, 0.9], + [0.3, 0.24, 0.15, 0.9], + [0.4, 0.32, 0.2, 1.0], + [0.5, 0.42, 0.28, 1.0], + [0.6, 0.52, 0.38, 1.0], + [0.9, 0.75, 0.5, 1.0], + [0.7, 0.6, 0.4, 1.0], + [0.35, 0.28, 0.18, 1.0], + [0.7, 0.5, 0.3, 0.4], + [0.8, 0.6, 0.35, 0.67], + [0.9, 0.7, 0.45, 1.0], + [0.85, 0.55, 0.25, 1.0], + [0.9, 0.65, 0.35, 1.0], + [0.95, 0.75, 0.45, 1.0], + [0.65, 0.5, 0.3, 1.0], + [0.75, 0.6, 0.4, 1.0], + [0.85, 0.7, 0.5, 1.0], + [0.9, 0.75, 0.5, 1.0], + [0.9, 0.75, 0.5, 0.6], + [0.9, 0.75, 0.5, 0.9], + [0.7, 0.5, 0.3, 0.31], + [0.8, 0.6, 0.35, 0.8], + [0.9, 0.7, 0.45, 1.0], + [0.13, 0.1, 0.06, 0.97], + [0.75, 0.6, 0.35, 1.0], + [0.65, 0.5, 0.3, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.75, 0.5, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.9, 0.7, 0.3, 1.0], + [0.7, 0.5, 0.3, 0.4], + [0.3, 0.24, 0.15, 1.0], + [0.23, 0.18, 0.11, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.75, 0.5, 0.06], + [0.7, 0.5, 0.3, 0.35], + [0.8, 0.45, 0.25, 1.0], + [0.8, 0.6, 0.35, 1.0], + [0.45, 0.36, 0.23, 0.56], + [0.3, 0.24, 0.15, 0.35], + [0.3, 0.24, 0.15, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json index 7a507cd336..0f5cf1ce8e 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -1,98 +1,98 @@ { - "DisplayName": "Forest Green", - "Description": "Natural green theme inspired by Skyrim's ancient forests and wilderness", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.1, 0.3, 0.15, 0.9], - "Text": [0.9, 1.0, 0.9, 1.0], - "Border": [0.4, 0.7, 0.4, 0.8] - }, - "StatusPalette": { - "Disable": [0.3, 0.4, 0.3, 1.0], - "Error": [0.8, 0.3, 0.2, 1.0], - "Warning": [0.9, 0.7, 0.2, 1.0], - "RestartNeeded": [0.6, 0.9, 0.3, 1.0], - "CurrentHotkey": [0.8, 1.0, 0.6, 1.0], - "SuccessColor": [0.2, 0.8, 0.3, 1.0], - "InfoColor": [0.4, 0.8, 0.9, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.7, 0.9, 0.7, 1.0], - "ColorHovered": [0.8, 1.0, 0.8, 1.0], - "MinimizedFactor": 0.65 - }, - "Style": { - "WindowBorderSize": 2.5, - "ChildBorderSize": 1.0, - "FrameBorderSize": 1.5, - "WindowPadding": [14.0, 12.0], - "WindowRounding": 6.0, - "IndentSpacing": 9.0, - "FramePadding": [7.0, 5.0], - "CellPadding": [13.0, 5.0], - "ItemSpacing": [9.0, 9.0] - }, - "FullPalette": [ - [0.90, 1.00, 0.90, 0.90], - [0.60, 0.80, 0.60, 1.00], - [0.10, 0.30, 0.15, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.08, 0.25, 0.12, 0.85], - [0.50, 0.70, 0.50, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.05, 0.15, 0.05, 1.00], - [0.20, 0.40, 0.25, 0.40], - [0.30, 0.50, 0.35, 0.45], - [0.05, 0.15, 0.08, 0.83], - [0.08, 0.20, 0.10, 0.87], - [0.15, 0.35, 0.20, 0.90], - [0.10, 0.25, 0.15, 0.90], - [0.18, 0.38, 0.22, 0.90], - [0.25, 0.45, 0.30, 1.00], - [0.35, 0.55, 0.40, 1.00], - [0.45, 0.65, 0.50, 1.00], - [0.90, 1.00, 0.90, 1.00], - [0.70, 0.85, 0.75, 1.00], - [0.20, 0.40, 0.25, 1.00], - [0.40, 0.70, 0.45, 0.40], - [0.50, 0.80, 0.55, 0.67], - [0.60, 0.90, 0.65, 1.00], - [0.35, 0.75, 0.40, 1.00], - [0.45, 0.85, 0.50, 1.00], - [0.55, 0.95, 0.60, 1.00], - [0.40, 0.60, 0.45, 1.00], - [0.50, 0.70, 0.55, 1.00], - [0.70, 0.90, 0.75, 1.00], - [0.90, 1.00, 0.90, 1.00], - [0.90, 1.00, 0.90, 0.60], - [0.90, 1.00, 0.90, 0.90], - [0.40, 0.70, 0.45, 0.31], - [0.50, 0.80, 0.55, 0.80], - [0.60, 0.90, 0.65, 1.00], - [0.12, 0.22, 0.15, 0.97], - [0.45, 0.75, 0.50, 1.00], - [0.50, 0.70, 0.55, 0.50], - [0.00, 0.00, 0.00, 0.00], - [0.90, 1.00, 0.90, 1.00], - [0.80, 0.90, 0.20, 1.00], - [0.80, 0.90, 0.20, 1.00], - [0.80, 0.90, 0.20, 1.00], - [0.40, 0.70, 0.45, 0.40], - [0.20, 0.35, 0.25, 1.00], - [0.15, 0.28, 0.20, 1.00], - [0.00, 0.00, 0.00, 0.00], - [0.90, 1.00, 0.90, 0.06], - [0.40, 0.70, 0.45, 0.35], - [0.70, 0.40, 0.30, 1.00], - [0.50, 0.80, 0.55, 1.00], - [0.25, 0.45, 0.30, 0.56], - [0.18, 0.35, 0.22, 0.35], - [0.18, 0.35, 0.22, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Forest Green", + "Description": "Natural green theme inspired by Skyrim's ancient forests and wilderness", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.1, 0.3, 0.15, 0.9], + "Text": [0.9, 1.0, 0.9, 1.0], + "Border": [0.4, 0.7, 0.4, 0.8] + }, + "StatusPalette": { + "Disable": [0.3, 0.4, 0.3, 1.0], + "Error": [0.8, 0.3, 0.2, 1.0], + "Warning": [0.9, 0.7, 0.2, 1.0], + "RestartNeeded": [0.6, 0.9, 0.3, 1.0], + "CurrentHotkey": [0.8, 1.0, 0.6, 1.0], + "SuccessColor": [0.2, 0.8, 0.3, 1.0], + "InfoColor": [0.4, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.9, 0.7, 1.0], + "ColorHovered": [0.8, 1.0, 0.8, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.5, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [14.0, 12.0], + "WindowRounding": 6.0, + "IndentSpacing": 9.0, + "FramePadding": [7.0, 5.0], + "CellPadding": [13.0, 5.0], + "ItemSpacing": [9.0, 9.0] + }, + "FullPalette": [ + [0.9, 1.0, 0.9, 0.9], + [0.6, 0.8, 0.6, 1.0], + [0.1, 0.3, 0.15, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.08, 0.25, 0.12, 0.85], + [0.5, 0.7, 0.5, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.05, 0.15, 0.05, 1.0], + [0.2, 0.4, 0.25, 0.4], + [0.3, 0.5, 0.35, 0.45], + [0.05, 0.15, 0.08, 0.83], + [0.08, 0.2, 0.1, 0.87], + [0.15, 0.35, 0.2, 0.9], + [0.1, 0.25, 0.15, 0.9], + [0.18, 0.38, 0.22, 0.9], + [0.25, 0.45, 0.3, 1.0], + [0.35, 0.55, 0.4, 1.0], + [0.45, 0.65, 0.5, 1.0], + [0.9, 1.0, 0.9, 1.0], + [0.7, 0.85, 0.75, 1.0], + [0.2, 0.4, 0.25, 1.0], + [0.4, 0.7, 0.45, 0.4], + [0.5, 0.8, 0.55, 0.67], + [0.6, 0.9, 0.65, 1.0], + [0.35, 0.75, 0.4, 1.0], + [0.45, 0.85, 0.5, 1.0], + [0.55, 0.95, 0.6, 1.0], + [0.4, 0.6, 0.45, 1.0], + [0.5, 0.7, 0.55, 1.0], + [0.7, 0.9, 0.75, 1.0], + [0.9, 1.0, 0.9, 1.0], + [0.9, 1.0, 0.9, 0.6], + [0.9, 1.0, 0.9, 0.9], + [0.4, 0.7, 0.45, 0.31], + [0.5, 0.8, 0.55, 0.8], + [0.6, 0.9, 0.65, 1.0], + [0.12, 0.22, 0.15, 0.97], + [0.45, 0.75, 0.5, 1.0], + [0.5, 0.7, 0.55, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.9, 1.0, 0.9, 1.0], + [0.8, 0.9, 0.2, 1.0], + [0.8, 0.9, 0.2, 1.0], + [0.8, 0.9, 0.2, 1.0], + [0.4, 0.7, 0.45, 0.4], + [0.2, 0.35, 0.25, 1.0], + [0.15, 0.28, 0.2, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 1.0, 0.9, 0.06], + [0.4, 0.7, 0.45, 0.35], + [0.7, 0.4, 0.3, 1.0], + [0.5, 0.8, 0.55, 1.0], + [0.25, 0.45, 0.3, 0.56], + [0.18, 0.35, 0.22, 0.35], + [0.18, 0.35, 0.22, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index f4715ef1ad..30780fdfc9 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -1,98 +1,98 @@ { - "DisplayName": "High Contrast", - "Description": "High contrast black and white theme for improved accessibility and visibility", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.0, 0.0, 0.0, 0.95], - "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [1.0, 1.0, 1.0, 0.9] - }, - "StatusPalette": { - "Disable": [0.5, 0.5, 0.5, 1.0], - "Error": [1.0, 0.0, 0.0, 1.0], - "Warning": [1.0, 1.0, 0.0, 1.0], - "RestartNeeded": [0.0, 1.0, 0.0, 1.0], - "CurrentHotkey": [0.0, 1.0, 1.0, 1.0], - "SuccessColor": [0.0, 1.0, 0.0, 1.0], - "InfoColor": [0.0, 0.5, 1.0, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [1.0, 1.0, 1.0, 1.0], - "ColorHovered": [0.8, 0.8, 0.8, 1.0], - "MinimizedFactor": 0.6 - }, - "Style": { - "WindowBorderSize": 4.0, - "ChildBorderSize": 2.0, - "FrameBorderSize": 2.0, - "WindowPadding": [18.0, 16.0], - "WindowRounding": 0.0, - "IndentSpacing": 12.0, - "FramePadding": [10.0, 6.0], - "CellPadding": [16.0, 8.0], - "ItemSpacing": [12.0, 12.0] - }, - "FullPalette": [ - [1.00, 1.00, 1.00, 0.90], - [0.80, 0.80, 0.80, 1.00], - [0.00, 0.00, 0.00, 0.95], - [0.00, 0.00, 0.00, 0.00], - [0.05, 0.05, 0.05, 0.85], - [1.00, 1.00, 1.00, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.00, 0.00, 0.00, 1.00], - [0.30, 0.30, 0.30, 0.40], - [0.50, 0.50, 0.50, 0.45], - [0.00, 0.00, 0.00, 0.83], - [0.00, 0.00, 0.00, 0.87], - [0.20, 0.20, 0.20, 0.90], - [0.00, 0.00, 0.00, 0.90], - [0.15, 0.15, 0.15, 0.90], - [0.30, 0.30, 0.30, 1.00], - [0.50, 0.50, 0.50, 1.00], - [0.70, 0.70, 0.70, 1.00], - [1.00, 1.00, 1.00, 1.00], - [0.80, 0.80, 0.80, 1.00], - [0.20, 0.20, 0.20, 1.00], - [0.60, 0.60, 0.60, 0.40], - [0.80, 0.80, 0.80, 0.67], - [1.00, 1.00, 1.00, 1.00], - [0.90, 0.90, 0.90, 1.00], - [0.85, 0.85, 0.85, 1.00], - [0.75, 0.75, 0.75, 1.00], - [0.60, 0.60, 0.60, 1.00], - [0.80, 0.80, 0.80, 1.00], - [0.90, 0.90, 0.90, 1.00], - [1.00, 1.00, 1.00, 1.00], - [1.00, 1.00, 1.00, 0.60], - [1.00, 1.00, 1.00, 0.90], - [0.60, 0.60, 0.60, 0.31], - [0.80, 0.80, 0.80, 0.80], - [1.00, 1.00, 1.00, 1.00], - [0.10, 0.10, 0.10, 0.97], - [0.85, 0.85, 0.85, 1.00], - [0.70, 0.70, 0.70, 0.50], - [0.00, 0.00, 0.00, 0.00], - [1.00, 1.00, 1.00, 1.00], - [1.00, 1.00, 0.00, 1.00], - [1.00, 1.00, 0.00, 1.00], - [1.00, 1.00, 0.00, 1.00], - [0.60, 0.60, 0.60, 0.40], - [0.20, 0.20, 0.20, 1.00], - [0.15, 0.15, 0.15, 1.00], - [0.00, 0.00, 0.00, 0.00], - [1.00, 1.00, 1.00, 0.06], - [0.60, 0.60, 0.60, 0.35], - [1.00, 0.00, 0.00, 1.00], - [0.80, 0.80, 0.80, 1.00], - [0.40, 0.40, 0.40, 0.56], - [0.25, 0.25, 0.25, 0.35], - [0.25, 0.25, 0.25, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "High Contrast", + "Description": "High contrast black and white theme for improved accessibility and visibility", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.0, 0.0, 0.0, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [1.0, 1.0, 1.0, 0.9] + }, + "StatusPalette": { + "Disable": [0.5, 0.5, 0.5, 1.0], + "Error": [1.0, 0.0, 0.0, 1.0], + "Warning": [1.0, 1.0, 0.0, 1.0], + "RestartNeeded": [0.0, 1.0, 0.0, 1.0], + "CurrentHotkey": [0.0, 1.0, 1.0, 1.0], + "SuccessColor": [0.0, 1.0, 0.0, 1.0], + "InfoColor": [0.0, 0.5, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [1.0, 1.0, 1.0, 1.0], + "ColorHovered": [0.8, 0.8, 0.8, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 4.0, + "ChildBorderSize": 2.0, + "FrameBorderSize": 2.0, + "WindowPadding": [18.0, 16.0], + "WindowRounding": 0.0, + "IndentSpacing": 12.0, + "FramePadding": [10.0, 6.0], + "CellPadding": [16.0, 8.0], + "ItemSpacing": [12.0, 12.0] + }, + "FullPalette": [ + [1.0, 1.0, 1.0, 0.9], + [0.8, 0.8, 0.8, 1.0], + [0.0, 0.0, 0.0, 0.95], + [0.0, 0.0, 0.0, 0.0], + [0.05, 0.05, 0.05, 0.85], + [1.0, 1.0, 1.0, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.3, 0.3, 0.3, 0.4], + [0.5, 0.5, 0.5, 0.45], + [0.0, 0.0, 0.0, 0.83], + [0.0, 0.0, 0.0, 0.87], + [0.2, 0.2, 0.2, 0.9], + [0.0, 0.0, 0.0, 0.9], + [0.15, 0.15, 0.15, 0.9], + [0.3, 0.3, 0.3, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.7, 0.7, 0.7, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.2, 0.2, 0.2, 1.0], + [0.6, 0.6, 0.6, 0.4], + [0.8, 0.8, 0.8, 0.67], + [1.0, 1.0, 1.0, 1.0], + [0.9, 0.9, 0.9, 1.0], + [0.85, 0.85, 0.85, 1.0], + [0.75, 0.75, 0.75, 1.0], + [0.6, 0.6, 0.6, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.9, 0.9, 0.9, 1.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.6], + [1.0, 1.0, 1.0, 0.9], + [0.6, 0.6, 0.6, 0.31], + [0.8, 0.8, 0.8, 0.8], + [1.0, 1.0, 1.0, 1.0], + [0.1, 0.1, 0.1, 0.97], + [0.85, 0.85, 0.85, 1.0], + [0.7, 0.7, 0.7, 0.5], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [0.6, 0.6, 0.6, 0.4], + [0.2, 0.2, 0.2, 1.0], + [0.15, 0.15, 0.15, 1.0], + [0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.06], + [0.6, 0.6, 0.6, 0.35], + [1.0, 0.0, 0.0, 1.0], + [0.8, 0.8, 0.8, 1.0], + [0.4, 0.4, 0.4, 0.56], + [0.25, 0.25, 0.25, 0.35], + [0.25, 0.25, 0.25, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 075cc58910..4f2a73a8ab 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -1,98 +1,98 @@ { - "DisplayName": "Light Mode", - "Description": "Clean bright theme with dark text for comfortable daytime use", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.9, 0.9, 0.9, 0.95], - "Text": [0.1, 0.1, 0.1, 1.0], - "Border": [0.3, 0.3, 0.3, 0.8] - }, - "StatusPalette": { - "Disable": [0.6, 0.6, 0.6, 1.0], - "Error": [0.8, 0.2, 0.2, 1.0], - "Warning": [0.9, 0.5, 0.1, 1.0], - "RestartNeeded": [0.2, 0.7, 0.2, 1.0], - "CurrentHotkey": [0.8, 0.6, 0.1, 1.0], - "SuccessColor": [0.1, 0.6, 0.1, 1.0], - "InfoColor": [0.2, 0.4, 0.8, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.3, 0.3, 0.3, 1.0], - "ColorHovered": [0.2, 0.2, 0.2, 1.0], - "MinimizedFactor": 0.7 - }, - "Style": { - "WindowBorderSize": 1.5, - "ChildBorderSize": 0.5, - "FrameBorderSize": 1.0, - "WindowPadding": [12.0, 10.0], - "WindowRounding": 3.0, - "IndentSpacing": 7.0, - "FramePadding": [6.0, 3.0], - "CellPadding": [10.0, 3.0], - "ItemSpacing": [7.0, 6.0] - }, - "FullPalette": [ - [0.10, 0.10, 0.10, 0.90], - [0.40, 0.40, 0.40, 1.00], - [0.90, 0.90, 0.90, 0.95], - [1.00, 1.00, 1.00, 0.00], - [0.95, 0.95, 0.95, 0.85], - [0.30, 0.30, 0.30, 0.65], - [1.00, 1.00, 1.00, 0.00], - [1.00, 1.00, 1.00, 1.00], - [0.70, 0.70, 0.70, 0.40], - [0.50, 0.50, 0.50, 0.45], - [1.00, 1.00, 1.00, 0.83], - [1.00, 1.00, 1.00, 0.87], - [0.80, 0.80, 0.80, 0.90], - [0.98, 0.98, 0.98, 0.90], - [0.85, 0.85, 0.85, 0.90], - [0.75, 0.75, 0.75, 1.00], - [0.58, 0.58, 0.58, 1.00], - [0.44, 0.44, 0.44, 1.00], - [0.10, 0.10, 0.10, 1.00], - [0.30, 0.30, 0.30, 1.00], - [0.70, 0.70, 0.70, 1.00], - [0.26, 0.59, 0.98, 0.40], - [0.26, 0.59, 0.98, 0.67], - [0.26, 0.59, 0.98, 1.00], - [0.06, 0.53, 0.98, 1.00], - [0.26, 0.59, 0.98, 1.00], - [0.26, 0.59, 0.98, 1.00], - [0.50, 0.50, 0.50, 1.00], - [0.40, 0.40, 0.40, 1.00], - [0.30, 0.30, 0.30, 1.00], - [0.10, 0.10, 0.10, 1.00], - [0.10, 0.10, 0.10, 0.60], - [0.10, 0.10, 0.10, 0.90], - [0.26, 0.59, 0.98, 0.31], - [0.26, 0.59, 0.98, 0.80], - [0.26, 0.59, 0.98, 1.00], - [0.85, 0.85, 0.85, 0.97], - [0.26, 0.59, 0.98, 1.00], - [0.30, 0.40, 0.40, 0.50], - [1.00, 1.00, 1.00, 0.00], - [0.10, 0.10, 0.10, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.90, 0.70, 0.00, 1.00], - [0.26, 0.59, 0.98, 0.40], - [0.74, 0.74, 0.74, 1.00], - [0.81, 0.81, 0.81, 1.00], - [1.00, 1.00, 1.00, 0.00], - [0.10, 0.10, 0.10, 0.06], - [0.26, 0.59, 0.98, 0.35], - [0.80, 0.20, 0.20, 1.00], - [0.26, 0.59, 0.98, 1.00], - [0.70, 0.70, 0.70, 0.56], - [0.80, 0.80, 0.80, 0.35], - [0.80, 0.80, 0.80, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Light Mode", + "Description": "Clean bright theme with dark text for comfortable daytime use", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.9, 0.9, 0.9, 0.95], + "Text": [0.1, 0.1, 0.1, 1.0], + "Border": [0.3, 0.3, 0.3, 0.8] + }, + "StatusPalette": { + "Disable": [0.6, 0.6, 0.6, 1.0], + "Error": [0.8, 0.2, 0.2, 1.0], + "Warning": [0.9, 0.5, 0.1, 1.0], + "RestartNeeded": [0.2, 0.7, 0.2, 1.0], + "CurrentHotkey": [0.8, 0.6, 0.1, 1.0], + "SuccessColor": [0.1, 0.6, 0.1, 1.0], + "InfoColor": [0.2, 0.4, 0.8, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.3, 0.3, 0.3, 1.0], + "ColorHovered": [0.2, 0.2, 0.2, 1.0], + "MinimizedFactor": 0.7 + }, + "Style": { + "WindowBorderSize": 1.5, + "ChildBorderSize": 0.5, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 10.0], + "WindowRounding": 3.0, + "IndentSpacing": 7.0, + "FramePadding": [6.0, 3.0], + "CellPadding": [10.0, 3.0], + "ItemSpacing": [7.0, 6.0] + }, + "FullPalette": [ + [0.1, 0.1, 0.1, 0.9], + [0.4, 0.4, 0.4, 1.0], + [0.9, 0.9, 0.9, 0.95], + [1.0, 1.0, 1.0, 0.0], + [0.95, 0.95, 0.95, 0.85], + [0.3, 0.3, 0.3, 0.65], + [1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [0.7, 0.7, 0.7, 0.4], + [0.5, 0.5, 0.5, 0.45], + [1.0, 1.0, 1.0, 0.83], + [1.0, 1.0, 1.0, 0.87], + [0.8, 0.8, 0.8, 0.9], + [0.98, 0.98, 0.98, 0.9], + [0.85, 0.85, 0.85, 0.9], + [0.75, 0.75, 0.75, 1.0], + [0.58, 0.58, 0.58, 1.0], + [0.44, 0.44, 0.44, 1.0], + [0.1, 0.1, 0.1, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.7, 0.7, 0.7, 1.0], + [0.26, 0.59, 0.98, 0.4], + [0.26, 0.59, 0.98, 0.67], + [0.26, 0.59, 0.98, 1.0], + [0.06, 0.53, 0.98, 1.0], + [0.26, 0.59, 0.98, 1.0], + [0.26, 0.59, 0.98, 1.0], + [0.5, 0.5, 0.5, 1.0], + [0.4, 0.4, 0.4, 1.0], + [0.3, 0.3, 0.3, 1.0], + [0.1, 0.1, 0.1, 1.0], + [0.1, 0.1, 0.1, 0.6], + [0.1, 0.1, 0.1, 0.9], + [0.26, 0.59, 0.98, 0.31], + [0.26, 0.59, 0.98, 0.8], + [0.26, 0.59, 0.98, 1.0], + [0.85, 0.85, 0.85, 0.97], + [0.26, 0.59, 0.98, 1.0], + [0.3, 0.4, 0.4, 0.5], + [1.0, 1.0, 1.0, 0.0], + [0.1, 0.1, 0.1, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.9, 0.7, 0.0, 1.0], + [0.26, 0.59, 0.98, 0.4], + [0.74, 0.74, 0.74, 1.0], + [0.81, 0.81, 0.81, 1.0], + [1.0, 1.0, 1.0, 0.0], + [0.1, 0.1, 0.1, 0.06], + [0.26, 0.59, 0.98, 0.35], + [0.8, 0.2, 0.2, 1.0], + [0.26, 0.59, 0.98, 1.0], + [0.7, 0.7, 0.7, 0.56], + [0.8, 0.8, 0.8, 0.35], + [0.8, 0.8, 0.8, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json index 773060f269..1841cc1c07 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -1,98 +1,98 @@ { - "DisplayName": "Mystic Purple", - "Description": "Magical purple theme with mystical vibes perfect for spell crafting and enchanting", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.2, 0.1, 0.3, 0.9], - "Text": [0.95, 0.9, 1.0, 1.0], - "Border": [0.6, 0.4, 0.8, 0.8] - }, - "StatusPalette": { - "Disable": [0.4, 0.3, 0.5, 1.0], - "Error": [1.0, 0.3, 0.6, 1.0], - "Warning": [1.0, 0.7, 0.4, 1.0], - "RestartNeeded": [0.6, 0.9, 0.7, 1.0], - "CurrentHotkey": [0.9, 0.8, 1.0, 1.0], - "SuccessColor": [0.7, 0.3, 0.9, 1.0], - "InfoColor": [0.8, 0.6, 1.0, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.8, 0.7, 0.95, 1.0], - "ColorHovered": [0.9, 0.8, 1.0, 1.0], - "MinimizedFactor": 0.6 - }, - "Style": { - "WindowBorderSize": 2.5, - "ChildBorderSize": 1.5, - "FrameBorderSize": 2.0, - "WindowPadding": [16.0, 14.0], - "WindowRounding": 7.0, - "IndentSpacing": 10.0, - "FramePadding": [9.0, 6.0], - "CellPadding": [15.0, 6.0], - "ItemSpacing": [10.0, 9.0] - }, - "FullPalette": [ - [0.95, 0.90, 1.00, 0.90], - [0.70, 0.60, 0.85, 1.00], - [0.20, 0.10, 0.30, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.18, 0.08, 0.25, 0.85], - [0.60, 0.50, 0.75, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.10, 0.05, 0.15, 1.00], - [0.35, 0.20, 0.50, 0.40], - [0.45, 0.25, 0.60, 0.45], - [0.10, 0.05, 0.15, 0.83], - [0.15, 0.08, 0.20, 0.87], - [0.25, 0.15, 0.40, 0.90], - [0.18, 0.10, 0.28, 0.90], - [0.30, 0.18, 0.45, 0.90], - [0.40, 0.25, 0.55, 1.00], - [0.50, 0.35, 0.65, 1.00], - [0.60, 0.45, 0.75, 1.00], - [0.95, 0.90, 1.00, 1.00], - [0.75, 0.65, 0.90, 1.00], - [0.35, 0.20, 0.50, 1.00], - [0.60, 0.40, 0.80, 0.40], - [0.70, 0.50, 0.90, 0.67], - [0.80, 0.60, 1.00, 1.00], - [0.65, 0.35, 0.95, 1.00], - [0.75, 0.45, 1.00, 1.00], - [0.85, 0.55, 1.00, 1.00], - [0.55, 0.40, 0.70, 1.00], - [0.65, 0.50, 0.80, 1.00], - [0.80, 0.65, 0.95, 1.00], - [0.95, 0.90, 1.00, 1.00], - [0.95, 0.90, 1.00, 0.60], - [0.95, 0.90, 1.00, 0.90], - [0.60, 0.40, 0.80, 0.31], - [0.70, 0.50, 0.90, 0.80], - [0.80, 0.60, 1.00, 1.00], - [0.15, 0.08, 0.22, 0.97], - [0.70, 0.45, 0.85, 1.00], - [0.60, 0.45, 0.75, 0.50], - [0.00, 0.00, 0.00, 0.00], - [0.95, 0.90, 1.00, 1.00], - [0.90, 0.70, 1.00, 1.00], - [0.90, 0.70, 1.00, 1.00], - [0.90, 0.70, 1.00, 1.00], - [0.60, 0.40, 0.80, 0.40], - [0.30, 0.18, 0.40, 1.00], - [0.25, 0.15, 0.35, 1.00], - [0.00, 0.00, 0.00, 0.00], - [0.95, 0.90, 1.00, 0.06], - [0.60, 0.40, 0.80, 0.35], - [0.90, 0.40, 0.60, 1.00], - [0.70, 0.50, 0.90, 1.00], - [0.40, 0.25, 0.55, 0.56], - [0.28, 0.18, 0.40, 0.35], - [0.28, 0.18, 0.40, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Mystic Purple", + "Description": "Magical purple theme with mystical vibes perfect for spell crafting and enchanting", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.2, 0.1, 0.3, 0.9], + "Text": [0.95, 0.9, 1.0, 1.0], + "Border": [0.6, 0.4, 0.8, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.3, 0.5, 1.0], + "Error": [1.0, 0.3, 0.6, 1.0], + "Warning": [1.0, 0.7, 0.4, 1.0], + "RestartNeeded": [0.6, 0.9, 0.7, 1.0], + "CurrentHotkey": [0.9, 0.8, 1.0, 1.0], + "SuccessColor": [0.7, 0.3, 0.9, 1.0], + "InfoColor": [0.8, 0.6, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.8, 0.7, 0.95, 1.0], + "ColorHovered": [0.9, 0.8, 1.0, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 2.5, + "ChildBorderSize": 1.5, + "FrameBorderSize": 2.0, + "WindowPadding": [16.0, 14.0], + "WindowRounding": 7.0, + "IndentSpacing": 10.0, + "FramePadding": [9.0, 6.0], + "CellPadding": [15.0, 6.0], + "ItemSpacing": [10.0, 9.0] + }, + "FullPalette": [ + [0.95, 0.9, 1.0, 0.9], + [0.7, 0.6, 0.85, 1.0], + [0.2, 0.1, 0.3, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.18, 0.08, 0.25, 0.85], + [0.6, 0.5, 0.75, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.1, 0.05, 0.15, 1.0], + [0.35, 0.2, 0.5, 0.4], + [0.45, 0.25, 0.6, 0.45], + [0.1, 0.05, 0.15, 0.83], + [0.15, 0.08, 0.2, 0.87], + [0.25, 0.15, 0.4, 0.9], + [0.18, 0.1, 0.28, 0.9], + [0.3, 0.18, 0.45, 0.9], + [0.4, 0.25, 0.55, 1.0], + [0.5, 0.35, 0.65, 1.0], + [0.6, 0.45, 0.75, 1.0], + [0.95, 0.9, 1.0, 1.0], + [0.75, 0.65, 0.9, 1.0], + [0.35, 0.2, 0.5, 1.0], + [0.6, 0.4, 0.8, 0.4], + [0.7, 0.5, 0.9, 0.67], + [0.8, 0.6, 1.0, 1.0], + [0.65, 0.35, 0.95, 1.0], + [0.75, 0.45, 1.0, 1.0], + [0.85, 0.55, 1.0, 1.0], + [0.55, 0.4, 0.7, 1.0], + [0.65, 0.5, 0.8, 1.0], + [0.8, 0.65, 0.95, 1.0], + [0.95, 0.9, 1.0, 1.0], + [0.95, 0.9, 1.0, 0.6], + [0.95, 0.9, 1.0, 0.9], + [0.6, 0.4, 0.8, 0.31], + [0.7, 0.5, 0.9, 0.8], + [0.8, 0.6, 1.0, 1.0], + [0.15, 0.08, 0.22, 0.97], + [0.7, 0.45, 0.85, 1.0], + [0.6, 0.45, 0.75, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.95, 0.9, 1.0, 1.0], + [0.9, 0.7, 1.0, 1.0], + [0.9, 0.7, 1.0, 1.0], + [0.9, 0.7, 1.0, 1.0], + [0.6, 0.4, 0.8, 0.4], + [0.3, 0.18, 0.4, 1.0], + [0.25, 0.15, 0.35, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.95, 0.9, 1.0, 0.06], + [0.6, 0.4, 0.8, 0.35], + [0.9, 0.4, 0.6, 1.0], + [0.7, 0.5, 0.9, 1.0], + [0.4, 0.25, 0.55, 0.56], + [0.28, 0.18, 0.4, 0.35], + [0.28, 0.18, 0.4, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index b466a0c447..ef794a4e39 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -1,98 +1,98 @@ { - "DisplayName": "Nordic Frost", - "Description": "Cool blue-white theme reflecting the harsh Nordic climate and icy mountain peaks", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.05, 0.15, 0.25, 0.9], - "Text": [0.9, 0.95, 1.0, 1.0], - "Border": [0.6, 0.8, 1.0, 0.8] - }, - "StatusPalette": { - "Disable": [0.4, 0.45, 0.5, 1.0], - "Error": [1.0, 0.3, 0.4, 1.0], - "Warning": [1.0, 0.8, 0.3, 1.0], - "RestartNeeded": [0.7, 0.9, 0.4, 1.0], - "CurrentHotkey": [0.5, 0.8, 1.0, 1.0], - "SuccessColor": [0.4, 0.8, 0.6, 1.0], - "InfoColor": [0.6, 0.8, 0.9, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.7, 0.85, 0.95, 1.0], - "ColorHovered": [0.85, 0.95, 1.0, 1.0], - "MinimizedFactor": 0.6 - }, - "Style": { - "WindowBorderSize": 1.5, - "ChildBorderSize": 1.0, - "FrameBorderSize": 1.0, - "WindowPadding": [12.0, 10.0], - "WindowRounding": 8.0, - "IndentSpacing": 6.0, - "FramePadding": [6.0, 4.0], - "CellPadding": [10.0, 4.0], - "ItemSpacing": [6.0, 6.0] - }, - "FullPalette": [ - [0.90, 0.95, 1.00, 0.90], - [0.70, 0.80, 0.90, 1.00], - [0.05, 0.15, 0.25, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.04, 0.12, 0.20, 0.85], - [0.50, 0.65, 0.80, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.03, 0.08, 0.15, 1.00], - [0.25, 0.35, 0.50, 0.40], - [0.30, 0.40, 0.55, 0.45], - [0.03, 0.08, 0.15, 0.83], - [0.05, 0.10, 0.18, 0.87], - [0.15, 0.25, 0.40, 0.90], - [0.08, 0.18, 0.28, 0.90], - [0.20, 0.30, 0.45, 0.90], - [0.30, 0.40, 0.55, 1.00], - [0.40, 0.50, 0.65, 1.00], - [0.50, 0.60, 0.75, 1.00], - [0.90, 0.95, 1.00, 1.00], - [0.70, 0.80, 0.90, 1.00], - [0.25, 0.35, 0.50, 1.00], - [0.50, 0.70, 0.90, 0.40], - [0.60, 0.75, 0.95, 0.67], - [0.70, 0.85, 1.00, 1.00], - [0.60, 0.80, 1.00, 1.00], - [0.70, 0.85, 1.00, 1.00], - [0.80, 0.90, 1.00, 1.00], - [0.50, 0.65, 0.80, 1.00], - [0.60, 0.75, 0.90, 1.00], - [0.80, 0.90, 1.00, 1.00], - [0.90, 0.95, 1.00, 1.00], - [0.90, 0.95, 1.00, 0.60], - [0.90, 0.95, 1.00, 0.90], - [0.50, 0.70, 0.90, 0.31], - [0.60, 0.75, 0.95, 0.80], - [0.70, 0.85, 1.00, 1.00], - [0.04, 0.12, 0.20, 0.97], - [0.65, 0.80, 0.95, 1.00], - [0.50, 0.65, 0.80, 0.50], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.95, 1.00, 1.00], - [0.50, 0.80, 1.00, 1.00], - [0.50, 0.80, 1.00, 1.00], - [0.50, 0.80, 1.00, 1.00], - [0.50, 0.70, 0.90, 0.40], - [0.20, 0.30, 0.45, 1.00], - [0.15, 0.25, 0.35, 1.00], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.95, 1.00, 0.06], - [0.50, 0.70, 0.90, 0.35], - [0.60, 0.40, 0.80, 1.00], - [0.60, 0.75, 0.95, 1.00], - [0.30, 0.40, 0.55, 0.56], - [0.20, 0.30, 0.45, 0.35], - [0.20, 0.30, 0.45, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Nordic Frost", + "Description": "Cool blue-white theme reflecting the harsh Nordic climate and icy mountain peaks", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.05, 0.15, 0.25, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "Border": [0.6, 0.8, 1.0, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.45, 0.5, 1.0], + "Error": [1.0, 0.3, 0.4, 1.0], + "Warning": [1.0, 0.8, 0.3, 1.0], + "RestartNeeded": [0.7, 0.9, 0.4, 1.0], + "CurrentHotkey": [0.5, 0.8, 1.0, 1.0], + "SuccessColor": [0.4, 0.8, 0.6, 1.0], + "InfoColor": [0.6, 0.8, 0.9, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.85, 0.95, 1.0], + "ColorHovered": [0.85, 0.95, 1.0, 1.0], + "MinimizedFactor": 0.6 + }, + "Style": { + "WindowBorderSize": 1.5, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.0, + "WindowPadding": [12.0, 10.0], + "WindowRounding": 8.0, + "IndentSpacing": 6.0, + "FramePadding": [6.0, 4.0], + "CellPadding": [10.0, 4.0], + "ItemSpacing": [6.0, 6.0] + }, + "FullPalette": [ + [0.9, 0.95, 1.0, 0.9], + [0.7, 0.8, 0.9, 1.0], + [0.05, 0.15, 0.25, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.04, 0.12, 0.2, 0.85], + [0.5, 0.65, 0.8, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.03, 0.08, 0.15, 1.0], + [0.25, 0.35, 0.5, 0.4], + [0.3, 0.4, 0.55, 0.45], + [0.03, 0.08, 0.15, 0.83], + [0.05, 0.1, 0.18, 0.87], + [0.15, 0.25, 0.4, 0.9], + [0.08, 0.18, 0.28, 0.9], + [0.2, 0.3, 0.45, 0.9], + [0.3, 0.4, 0.55, 1.0], + [0.4, 0.5, 0.65, 1.0], + [0.5, 0.6, 0.75, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.7, 0.8, 0.9, 1.0], + [0.25, 0.35, 0.5, 1.0], + [0.5, 0.7, 0.9, 0.4], + [0.6, 0.75, 0.95, 0.67], + [0.7, 0.85, 1.0, 1.0], + [0.6, 0.8, 1.0, 1.0], + [0.7, 0.85, 1.0, 1.0], + [0.8, 0.9, 1.0, 1.0], + [0.5, 0.65, 0.8, 1.0], + [0.6, 0.75, 0.9, 1.0], + [0.8, 0.9, 1.0, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.9, 0.95, 1.0, 0.6], + [0.9, 0.95, 1.0, 0.9], + [0.5, 0.7, 0.9, 0.31], + [0.6, 0.75, 0.95, 0.8], + [0.7, 0.85, 1.0, 1.0], + [0.04, 0.12, 0.2, 0.97], + [0.65, 0.8, 0.95, 1.0], + [0.5, 0.65, 0.8, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.8, 1.0, 1.0], + [0.5, 0.7, 0.9, 0.4], + [0.2, 0.3, 0.45, 1.0], + [0.15, 0.25, 0.35, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 0.06], + [0.5, 0.7, 0.9, 0.35], + [0.6, 0.4, 0.8, 1.0], + [0.6, 0.75, 0.95, 1.0], + [0.3, 0.4, 0.55, 0.56], + [0.2, 0.3, 0.45, 0.35], + [0.2, 0.3, 0.45, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json index 1d06c6d500..acb79f726b 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -1,98 +1,98 @@ { - "DisplayName": "Ocean Blue", - "Description": "Cool blue tones inspired by deep ocean waters and maritime adventures", - "Version": "2.0.0", - "Author": "Community Shaders Team", - "Theme": { - "UseSimplePalette": false, - "ShowActionIcons": true, - "TooltipHoverDelay": 0.5, - "Palette": { - "Background": [0.1, 0.2, 0.4, 0.9], - "Text": [0.9, 0.95, 1.0, 1.0], - "Border": [0.3, 0.5, 0.8, 0.8] - }, - "StatusPalette": { - "Disable": [0.4, 0.45, 0.5, 1.0], - "Error": [1.0, 0.4, 0.5, 1.0], - "Warning": [1.0, 0.8, 0.3, 1.0], - "RestartNeeded": [0.3, 0.8, 0.6, 1.0], - "CurrentHotkey": [0.6, 0.9, 1.0, 1.0], - "SuccessColor": [0.2, 0.7, 0.9, 1.0], - "InfoColor": [0.4, 0.7, 1.0, 1.0] - }, - "FeatureHeading": { - "ColorDefault": [0.7, 0.85, 1.0, 1.0], - "ColorHovered": [0.8, 0.9, 1.0, 1.0], - "MinimizedFactor": 0.65 - }, - "Style": { - "WindowBorderSize": 2.0, - "ChildBorderSize": 1.0, - "FrameBorderSize": 1.5, - "WindowPadding": [15.0, 13.0], - "WindowRounding": 5.0, - "IndentSpacing": 8.5, - "FramePadding": [8.0, 5.0], - "CellPadding": [15.0, 4.0], - "ItemSpacing": [9.0, 8.0] - }, - "FullPalette": [ - [0.90, 0.95, 1.00, 0.90], - [0.60, 0.70, 0.85, 1.00], - [0.10, 0.20, 0.40, 0.90], - [0.00, 0.00, 0.00, 0.00], - [0.08, 0.16, 0.32, 0.85], - [0.50, 0.60, 0.80, 0.65], - [0.00, 0.00, 0.00, 0.00], - [0.05, 0.10, 0.20, 1.00], - [0.20, 0.30, 0.50, 0.40], - [0.30, 0.40, 0.60, 0.45], - [0.05, 0.10, 0.20, 0.83], - [0.08, 0.15, 0.25, 0.87], - [0.15, 0.25, 0.45, 0.90], - [0.10, 0.18, 0.35, 0.90], - [0.18, 0.28, 0.48, 0.90], - [0.25, 0.35, 0.55, 1.00], - [0.35, 0.45, 0.65, 1.00], - [0.45, 0.55, 0.75, 1.00], - [0.90, 0.95, 1.00, 1.00], - [0.70, 0.80, 0.90, 1.00], - [0.20, 0.30, 0.50, 1.00], - [0.30, 0.50, 0.80, 0.40], - [0.40, 0.60, 0.90, 0.67], - [0.50, 0.70, 1.00, 1.00], - [0.25, 0.55, 0.95, 1.00], - [0.35, 0.65, 1.00, 1.00], - [0.45, 0.75, 1.00, 1.00], - [0.40, 0.50, 0.70, 1.00], - [0.50, 0.60, 0.80, 1.00], - [0.70, 0.80, 0.95, 1.00], - [0.90, 0.95, 1.00, 1.00], - [0.90, 0.95, 1.00, 0.60], - [0.90, 0.95, 1.00, 0.90], - [0.30, 0.50, 0.80, 0.31], - [0.40, 0.60, 0.90, 0.80], - [0.50, 0.70, 1.00, 1.00], - [0.12, 0.18, 0.30, 0.97], - [0.35, 0.55, 0.85, 1.00], - [0.50, 0.60, 0.80, 0.50], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.95, 1.00, 1.00], - [0.60, 0.80, 1.00, 1.00], - [0.60, 0.80, 1.00, 1.00], - [0.60, 0.80, 1.00, 1.00], - [0.30, 0.50, 0.80, 0.40], - [0.20, 0.25, 0.40, 1.00], - [0.15, 0.20, 0.35, 1.00], - [0.00, 0.00, 0.00, 0.00], - [0.90, 0.95, 1.00, 0.06], - [0.30, 0.50, 0.80, 0.35], - [0.80, 0.40, 0.50, 1.00], - [0.40, 0.60, 0.90, 1.00], - [0.25, 0.35, 0.55, 0.56], - [0.18, 0.25, 0.40, 0.35], - [0.18, 0.25, 0.40, 0.35] - ] - } -} \ No newline at end of file + "DisplayName": "Ocean Blue", + "Description": "Cool blue tones inspired by deep ocean waters and maritime adventures", + "Version": "2.0.0", + "Author": "Community Shaders Team", + "Theme": { + "UseSimplePalette": false, + "ShowActionIcons": true, + "TooltipHoverDelay": 0.5, + "Palette": { + "Background": [0.1, 0.2, 0.4, 0.9], + "Text": [0.9, 0.95, 1.0, 1.0], + "Border": [0.3, 0.5, 0.8, 0.8] + }, + "StatusPalette": { + "Disable": [0.4, 0.45, 0.5, 1.0], + "Error": [1.0, 0.4, 0.5, 1.0], + "Warning": [1.0, 0.8, 0.3, 1.0], + "RestartNeeded": [0.3, 0.8, 0.6, 1.0], + "CurrentHotkey": [0.6, 0.9, 1.0, 1.0], + "SuccessColor": [0.2, 0.7, 0.9, 1.0], + "InfoColor": [0.4, 0.7, 1.0, 1.0] + }, + "FeatureHeading": { + "ColorDefault": [0.7, 0.85, 1.0, 1.0], + "ColorHovered": [0.8, 0.9, 1.0, 1.0], + "MinimizedFactor": 0.65 + }, + "Style": { + "WindowBorderSize": 2.0, + "ChildBorderSize": 1.0, + "FrameBorderSize": 1.5, + "WindowPadding": [15.0, 13.0], + "WindowRounding": 5.0, + "IndentSpacing": 8.5, + "FramePadding": [8.0, 5.0], + "CellPadding": [15.0, 4.0], + "ItemSpacing": [9.0, 8.0] + }, + "FullPalette": [ + [0.9, 0.95, 1.0, 0.9], + [0.6, 0.7, 0.85, 1.0], + [0.1, 0.2, 0.4, 0.9], + [0.0, 0.0, 0.0, 0.0], + [0.08, 0.16, 0.32, 0.85], + [0.5, 0.6, 0.8, 0.65], + [0.0, 0.0, 0.0, 0.0], + [0.05, 0.1, 0.2, 1.0], + [0.2, 0.3, 0.5, 0.4], + [0.3, 0.4, 0.6, 0.45], + [0.05, 0.1, 0.2, 0.83], + [0.08, 0.15, 0.25, 0.87], + [0.15, 0.25, 0.45, 0.9], + [0.1, 0.18, 0.35, 0.9], + [0.18, 0.28, 0.48, 0.9], + [0.25, 0.35, 0.55, 1.0], + [0.35, 0.45, 0.65, 1.0], + [0.45, 0.55, 0.75, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.7, 0.8, 0.9, 1.0], + [0.2, 0.3, 0.5, 1.0], + [0.3, 0.5, 0.8, 0.4], + [0.4, 0.6, 0.9, 0.67], + [0.5, 0.7, 1.0, 1.0], + [0.25, 0.55, 0.95, 1.0], + [0.35, 0.65, 1.0, 1.0], + [0.45, 0.75, 1.0, 1.0], + [0.4, 0.5, 0.7, 1.0], + [0.5, 0.6, 0.8, 1.0], + [0.7, 0.8, 0.95, 1.0], + [0.9, 0.95, 1.0, 1.0], + [0.9, 0.95, 1.0, 0.6], + [0.9, 0.95, 1.0, 0.9], + [0.3, 0.5, 0.8, 0.31], + [0.4, 0.6, 0.9, 0.8], + [0.5, 0.7, 1.0, 1.0], + [0.12, 0.18, 0.3, 0.97], + [0.35, 0.55, 0.85, 1.0], + [0.5, 0.6, 0.8, 0.5], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 1.0], + [0.6, 0.8, 1.0, 1.0], + [0.6, 0.8, 1.0, 1.0], + [0.6, 0.8, 1.0, 1.0], + [0.3, 0.5, 0.8, 0.4], + [0.2, 0.25, 0.4, 1.0], + [0.15, 0.2, 0.35, 1.0], + [0.0, 0.0, 0.0, 0.0], + [0.9, 0.95, 1.0, 0.06], + [0.3, 0.5, 0.8, 0.35], + [0.8, 0.4, 0.5, 1.0], + [0.4, 0.6, 0.9, 1.0], + [0.25, 0.35, 0.55, 0.56], + [0.18, 0.25, 0.4, 0.35], + [0.18, 0.25, 0.4, 0.35] + ] + } +} diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/README.md b/package/SKSE/Plugins/CommunityShaders/Themes/README.md index 05cdb8e706..23f6ff21a0 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/README.md +++ b/package/SKSE/Plugins/CommunityShaders/Themes/README.md @@ -16,47 +16,49 @@ Theme files use JSON format and should follow this structure: ```json { - "DisplayName": "My Custom Theme", - "Description": "A beautiful custom theme", - "Version": "1.0.0", - "Author": "Your Name", - "Theme": { - "UseSimplePalette": true, - "Palette": { - "Background": [0.1, 0.1, 0.1, 0.95], - "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [0.5, 0.5, 0.5, 0.8] - } - } + "DisplayName": "My Custom Theme", + "Description": "A beautiful custom theme", + "Version": "1.0.0", + "Author": "Your Name", + "Theme": { + "UseSimplePalette": true, + "Palette": { + "Background": [0.1, 0.1, 0.1, 0.95], + "Text": [1.0, 1.0, 1.0, 1.0], + "Border": [0.5, 0.5, 0.5, 0.8] + } + } } ``` ### Required Fields -- `Theme`: The main theme object containing all visual settings -- `Theme.UseSimplePalette`: Set to `true` for simple 3-color themes, `false` for full ImGui color palette control +- `Theme`: The main theme object containing all visual settings +- `Theme.UseSimplePalette`: Set to `true` for simple 3-color themes, `false` for full ImGui color palette control ### Optional Metadata -- `DisplayName`: Human-readable name shown in the dropdown (defaults to filename) -- `Description`: Brief description shown in the UI -- `Version`: Theme version number -- `Author`: Theme creator name +- `DisplayName`: Human-readable name shown in the dropdown (defaults to filename) +- `Description`: Brief description shown in the UI +- `Version`: Theme version number +- `Author`: Theme creator name ### Color Format Colors are specified as arrays of 4 floating-point values: `[red, green, blue, alpha]` -- Values range from 0.0 to 1.0 -- Alpha (transparency) typically ranges from 0.8 to 1.0 for UI elements + +- Values range from 0.0 to 1.0 +- Alpha (transparency) typically ranges from 0.8 to 1.0 for UI elements ## Simple vs Full Palette ### Simple Palette (`UseSimplePalette: true`) Uses only 3 colors for a clean, consistent look: -- `Background`: Main UI background color -- `Text`: Primary text color -- `Border`: Border and accent color + +- `Background`: Main UI background color +- `Text`: Primary text color +- `Border`: Border and accent color ### Full Palette (`UseSimplePalette: false`) @@ -72,25 +74,25 @@ Allows complete control over all ImGui colors. See existing theme files for exam ## Example Themes Included -- **Default**: Classic dark theme -- **Light**: Clean light mode for daytime use -- **Ocean**: Cool blue oceanic tones -- **Forest**: Natural green forest theme -- **Mystic**: Purple magical theme -- **Amber**: Warm candlelight theme -- **HighContrast**: Accessibility-focused high contrast -- **DragonBlood**: Dark red dragon-inspired theme -- **NordicFrost**: Cool Nordic blue-white theme -- **DwemerBronze**: Ancient bronze Dwemer technology theme +- **Default**: Classic dark theme +- **Light**: Clean light mode for daytime use +- **Ocean**: Cool blue oceanic tones +- **Forest**: Natural green forest theme +- **Mystic**: Purple magical theme +- **Amber**: Warm candlelight theme +- **HighContrast**: Accessibility-focused high contrast +- **DragonBlood**: Dark red dragon-inspired theme +- **NordicFrost**: Cool Nordic blue-white theme +- **DwemerBronze**: Ancient bronze Dwemer technology theme ## Hot-Swapping Features -- **Runtime Discovery**: New themes are discovered immediately with "Refresh Themes" -- **No Restart Required**: Themes apply instantly when selected -- **Live Editing**: Edit theme files and refresh to see changes immediately -- **Fallback Safety**: Invalid themes are safely ignored -- **File Size Limits**: Theme files are limited to 1MB for performance -- **Error Handling**: Malformed JSON files are logged but don't crash the system +- **Runtime Discovery**: New themes are discovered immediately with "Refresh Themes" +- **No Restart Required**: Themes apply instantly when selected +- **Live Editing**: Edit theme files and refresh to see changes immediately +- **Fallback Safety**: Invalid themes are safely ignored +- **File Size Limits**: Theme files are limited to 1MB for performance +- **Error Handling**: Malformed JSON files are logged but don't crash the system ## Sharing Themes @@ -98,8 +100,8 @@ Theme files are completely portable and can be shared between users. Simply copy ## Technical Notes -- Theme discovery is performed on-demand for performance -- Files are validated for basic JSON structure and required fields -- Theme loading uses the same robust error handling as the settings override system -- Maximum of 100 theme files can be loaded (prevent performance issues) -- File modification times are tracked for change detection \ No newline at end of file +- Theme discovery is performed on-demand for performance +- Files are validated for basic JSON structure and required fields +- Theme loading uses the same robust error handling as the settings override system +- Maximum of 100 theme files can be loaded (prevent performance issues) +- File modification times are tracked for change detection diff --git a/src/Menu.cpp b/src/Menu.cpp index eb62b33d8c..0f97ae83e0 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -8,11 +8,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -28,12 +28,11 @@ #include "Menu/MenuHeaderRenderer.h" #include "Menu/OverlayRenderer.h" #include "Menu/SettingsTabRenderer.h" +#include "Menu/ThemeManager.h" #include "ShaderCache.h" #include "State.h" -#include "Menu/ThemeManager.h" #include "TruePBR.h" #include "Util.h" -#include "Util.h" #include "Utils/UI.h" #include "Features/PerformanceOverlay.h" @@ -176,10 +175,10 @@ void Menu::LoadTheme(json& o_json) { if (o_json["Theme"].is_object()) { settings.Theme = o_json["Theme"]; - + // Validate the loaded font and fallback to default if necessary if (!Util::ValidateFont(settings.Theme.FontName)) { - logger::warn("Font '{}' not found, falling back to default font '{}'", + logger::warn("Font '{}' not found, falling back to default font '{}'", settings.Theme.FontName, ThemeSettings{}.FontName); settings.Theme.FontName = ThemeSettings{}.FontName; } @@ -190,11 +189,11 @@ void Menu::SaveTheme(json& o_json) { // Validate font before saving and fallback to default if necessary if (!Util::ValidateFont(settings.Theme.FontName)) { - logger::warn("Font '{}' not found during save, falling back to default font '{}'", + logger::warn("Font '{}' not found during save, falling back to default font '{}'", settings.Theme.FontName, ThemeSettings{}.FontName); settings.Theme.FontName = ThemeSettings{}.FontName; } - + o_json["Theme"] = settings.Theme; } @@ -214,17 +213,17 @@ bool Menu::LoadThemePreset(const std::string& themeName) auto themeManager = ThemeManager::GetSingleton(); json themeSettings; - + if (themeManager->LoadTheme(themeName, themeSettings)) { settings.Theme = themeSettings; - + // Validate the loaded font and fallback to default if necessary if (!Util::ValidateFont(settings.Theme.FontName)) { - logger::warn("Font '{}' from theme '{}' not found, falling back to default font '{}'", + logger::warn("Font '{}' from theme '{}' not found, falling back to default font '{}'", settings.Theme.FontName, themeName, ThemeSettings{}.FontName); settings.Theme.FontName = ThemeSettings{}.FontName; } - + settings.SelectedThemePreset = themeName; // Update cached values for font reload detection cachedFontName = settings.Theme.FontName; @@ -265,8 +264,7 @@ void Menu::Init() DXGI_SWAP_CHAIN_DESC desc{}; globals::d3d::swapChain->GetDesc(&desc); - - // Determine effective font size: user setting when >0, otherwise dynamic default by resolution + // Determine effective font size: user setting when >0, otherwise dynamic default by resolution float fontSize = ThemeManager::ResolveFontSize(*this); // Use dynamic font sizing when FontSize equals the default (indicating theme doesn't override) @@ -279,7 +277,6 @@ void Menu::Init() } fontSize = std::clamp(fontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE); - auto fontPath = Util::PathHelpers::GetFontsPath() / settings.Theme.FontName; if (!imgui_io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), @@ -293,7 +290,7 @@ void Menu::Init() cachedFontSize = fontSize; // Update cached size to match the actually loaded font size float globalScale = settings.Theme.GlobalScale; - + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default if (std::abs(globalScale - ThemeManager::Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { globalScale = ThemeManager::Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 @@ -350,10 +347,10 @@ void Menu::DrawSettings() OnFocusChanged(); focusChanged = false; } - + // Apply theme styling with universal contrast enhancement ThemeManager::SetupImGuiStyle(*this); - + ImGui::DockSpaceOverViewport(NULL, ImGuiDockNodeFlags_PassthruCentralNode); ImGui::SetNextWindowPos(Util::GetNativeViewportSizeScaled(0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); @@ -378,12 +375,12 @@ void Menu::DrawSettings() wasDocked = isDocked; float globalScale = settings.Theme.GlobalScale; - + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default if (std::abs(globalScale - ThemeManager::Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { globalScale = ThemeManager::Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 } - + const float uiScale = exp2(globalScale); // Get current UI scale // Check if we can show icons - require setting enabled and at least some icons loaded (for undocked) // For docked mode, always show icons if textures are available @@ -543,7 +540,7 @@ void Menu::DrawOverlay() ThemeManager::ReloadFont(*this, cachedFontSize); pendingFontName.clear(); } - + OverlayRenderer::RenderOverlay( *this, [this]() { ProcessInputEventQueue(); }, diff --git a/src/Menu.h b/src/Menu.h index 4fa6b96941..ef09141db3 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -60,8 +60,8 @@ class Menu // Font caching (made public for ThemeManager and OverlayRenderer access) float cachedFontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; // Tracks whether font has been modified and may require reloading - std::string cachedFontName = "Jost-Regular.ttf"; // Tracks whether font file has changed and may require reloading - + std::string cachedFontName = "Jost-Regular.ttf"; // Tracks whether font file has changed and may require reloading + // Deferred font reload system (public for SettingsTabRenderer access) bool pendingFontReload = false; std::string pendingFontName; @@ -116,20 +116,20 @@ class Menu struct ThemeSettings { float FontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; - std::string FontName = "Jost-Regular.ttf"; // Default font file name + std::string FontName = "Jost-Regular.ttf"; // Default font file name float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential bool UseSimplePalette = true; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds - + // Scrollbar opacity settings struct ScrollbarOpacitySettings { - float Background = 1.0f; // Background of the scrollbar area - float Thumb = 1.0f; // The draggable thumb/grip - float ThumbHovered = 1.0f; // Thumb when hovered - float ThumbActive = 1.0f; // Thumb when being dragged + float Background = 1.0f; // Background of the scrollbar area + float Thumb = 1.0f; // The draggable thumb/grip + float ThumbHovered = 1.0f; // Thumb when hovered + float ThumbActive = 1.0f; // Thumb when being dragged } ScrollbarOpacity; struct PaletteColors { @@ -236,7 +236,7 @@ class Menu uint32_t OverlayToggleKey = VK_F10; // Global overlay toggle key for all overlays bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed ThemeSettings Theme; - std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) + std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; const ThemeSettings& GetTheme() const { return settings.Theme; } // Provide read-only access to the Theme. Settings& GetSettings() { return settings; } // Provide access to settings for other components diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 5391f828fe..5d811992e2 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -251,10 +251,10 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const BuiltInMenu& menu) if (isFeatureIssues) { auto& themeSettings = globals::menu->GetSettings().Theme; ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); - + if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) selectedMenuRef = listId; - + ImGui::PopStyleColor(); } else { // Use contrast-aware selectable for better text visibility diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 046a3ba4d5..df0b066987 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -175,8 +175,8 @@ std::vector MenuHeaderRenderer::BuildActionIcons if (uiIcons.saveSettings.texture) { actionIcons.push_back({ uiIcons.saveSettings.texture, "Save Settings", - []() { - globals::state->Save(); + []() { + globals::state->Save(); globals::state->SaveTheme(); } }); } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index b80949acee..7d0536f855 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -82,11 +82,11 @@ bool OverlayRenderer::ShouldSkipRendering() void OverlayRenderer::HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize) { auto& currentTheme = menu.GetTheme(); - + // Reload font if size changed or font file changed bool fontSizeChanged = std::abs(cachedFontSize - currentFontSize) > ThemeManager::Constants::FONT_CACHE_EPSILON; bool fontNameChanged = menu.cachedFontName != currentTheme.FontName; - + if (fontSizeChanged || fontNameChanged) { ThemeManager::ReloadFont(menu, cachedFontSize); } diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 23e3936873..f907ea30cb 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -186,18 +186,18 @@ void SettingsTabRenderer::RenderThemesTab() // Get theme manager auto themeManager = ThemeManager::GetSingleton(); - + // Get available themes (force discovery if not done) if (!themeManager->IsDiscovered()) { themeManager->DiscoverThemes(); } - + const auto& themes = themeManager->GetThemes(); // Create dropdown items - using static storage to avoid dangling pointers static std::vector displayNames; static std::vector items; - + // Clear and rebuild the lists displayNames.clear(); items.clear(); @@ -213,23 +213,23 @@ void SettingsTabRenderer::RenderThemesTab() // Find current selection index - default to "Default" if no theme selected // Note: Add 1 to account for "+ Create New" option at index 0 - int currentItem = 1; // Default to first actual theme (Default Dark) + int currentItem = 1; // Default to first actual theme (Default Dark) std::string currentThemePreset = globals::menu->GetSettings().SelectedThemePreset; - + // If no theme is selected, default to "Default" if (currentThemePreset.empty()) { currentThemePreset = "Default"; globals::menu->GetSettings().SelectedThemePreset = "Default"; } - + // If we're in create new mode, show that as selected if (isCreatingNewTheme) { - currentItem = 0; // "+ Create New" + currentItem = 0; // "+ Create New" } else { // Find the theme in the list (skip index 0 which is "+ Create New") for (size_t i = 0; i < themes.size(); ++i) { if (themes[i].name == currentThemePreset) { - currentItem = static_cast(i + 1); // +1 for "+ Create New" offset + currentItem = static_cast(i + 1); // +1 for "+ Create New" offset break; } } @@ -251,10 +251,10 @@ void SettingsTabRenderer::RenderThemesTab() } } } - + // Show theme description as tooltip (only for actual themes, not "+ Create New") if (currentItem >= 1 && currentItem <= static_cast(themes.size())) { - const auto& selectedTheme = themes[currentItem - 1]; // -1 for "+ Create New" offset + const auto& selectedTheme = themes[currentItem - 1]; // -1 for "+ Create New" offset if (!selectedTheme.description.empty()) { if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", selectedTheme.description.c_str()); @@ -284,7 +284,7 @@ void SettingsTabRenderer::RenderThemesTab() // Save/Update Theme Button (show based on context) if (isCreatingNewTheme || (!currentThemePreset.empty() && currentThemePreset != "Default")) { ImGui::SameLine(); - + const char* buttonText = isCreatingNewTheme ? "Save Theme" : "Update Theme"; if (Util::ButtonWithFlash(buttonText)) { if (isCreatingNewTheme) { @@ -301,10 +301,10 @@ void SettingsTabRenderer::RenderThemesTab() // Use the existing SaveTheme method to serialize the theme settings json currentThemeJson; globals::menu->SaveTheme(currentThemeJson); - + // Overwrite the current theme with updated settings - if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], - currentThemeInfo->displayName, currentThemeInfo->description)) { + if (themeManager->SaveTheme(currentThemePreset, currentThemeJson["Theme"], + currentThemeInfo->displayName, currentThemeInfo->description)) { // Theme updated successfully } } @@ -319,8 +319,6 @@ void SettingsTabRenderer::RenderThemesTab() } } - - // Create Theme Popup if (showCreateThemePopup) { ImGui::OpenPopup("Create New Theme"); @@ -335,12 +333,12 @@ void SettingsTabRenderer::RenderThemesTab() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("File name for the theme (without .json extension)"); } - + ImGui::InputText("Display Name", newThemeDisplayName, sizeof(newThemeDisplayName)); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Human-readable name shown in the dropdown"); } - + ImGui::InputTextMultiline("Description", newThemeDescription, sizeof(newThemeDescription), ImVec2(400, 80)); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Optional description for the theme"); @@ -353,10 +351,10 @@ void SettingsTabRenderer::RenderThemesTab() // Use the existing SaveTheme method to serialize the theme settings json currentThemeJson; globals::menu->SaveTheme(currentThemeJson); - + std::string displayName = strlen(newThemeDisplayName) > 0 ? std::string(newThemeDisplayName) : std::string(newThemeName); std::string description = strlen(newThemeDescription) > 0 ? std::string(newThemeDescription) : ""; - + if (themeManager->SaveTheme(std::string(newThemeName), currentThemeJson["Theme"], displayName, description)) { // Theme created successfully, load it and exit create mode globals::menu->LoadThemePreset(std::string(newThemeName)); @@ -404,18 +402,17 @@ void SettingsTabRenderer::RenderStylingTab() io.FontGlobalScale = trueScale; } - ImGui::SeparatorText("Font"); if (ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { // Font size changed, schedule deferred reload globals::menu->pendingFontReload = true; globals::menu->pendingFontName = themeSettings.FontName; // Keep current font name } - + // Font selection dropdown static std::vector availableFonts; static bool fontsDiscovered = false; - + auto refreshFontList = [&]() { try { availableFonts = Util::DiscoverFonts(); @@ -424,12 +421,12 @@ void SettingsTabRenderer::RenderStylingTab() availableFonts.clear(); } }; - + if (!fontsDiscovered) { refreshFontList(); fontsDiscovered = true; } - + // Find current font index int currentFontIndex = 0; for (size_t i = 0; i < availableFonts.size(); ++i) { @@ -438,13 +435,13 @@ void SettingsTabRenderer::RenderStylingTab() break; } } - + // Use ImGui::Combo with safety checks to avoid crashes const char* previewText = "None"; if (!availableFonts.empty() && currentFontIndex >= 0 && currentFontIndex < static_cast(availableFonts.size())) { previewText = availableFonts[currentFontIndex].c_str(); } - + if (ImGui::BeginCombo("Font", previewText)) { if (availableFonts.empty()) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No fonts available"); @@ -457,7 +454,7 @@ void SettingsTabRenderer::RenderStylingTab() // Validate font name before applying const std::string& newFontName = availableFonts[i]; auto fontPath = Util::PathHelpers::GetFontsPath() / newFontName; - + if (std::filesystem::exists(fontPath)) { // Schedule deferred font reload (safe - will happen between frames) globals::menu->pendingFontReload = true; @@ -465,7 +462,7 @@ void SettingsTabRenderer::RenderStylingTab() } } } - + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) if (isSelected) { ImGui::SetItemDefaultFocus(); @@ -477,7 +474,7 @@ void SettingsTabRenderer::RenderStylingTab() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Select a custom font file (.ttf/.otf) from the Fonts folder.\nPlace custom fonts in: Interface/CommunityShaders/Fonts/"); } - + if (ImGui::Button("Refresh Font List")) { refreshFontList(); // Reset current font index if it's out of bounds after refresh @@ -491,7 +488,6 @@ void SettingsTabRenderer::RenderStylingTab() ImGui::SeparatorText("Layout"); - // Font size controls: Auto (resolution-based) or Manual bool useAutoFont = (themeSettings.FontSize <= 0.0f); if (ImGui::Checkbox("Use resolution-based font size", &useAutoFont)) { @@ -517,7 +513,6 @@ void SettingsTabRenderer::RenderStylingTab() ImGui::SliderFloat("Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f"); ImGui::EndDisabled(); - ImGui::SliderFloat2("Window Padding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Frame Padding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Item Spacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); @@ -615,7 +610,7 @@ void SettingsTabRenderer::RenderColorsTab() // Advanced Colors Section - collapsed by default to avoid overwhelming users if (ImGui::CollapsingHeader("Advanced")) { ImGui::TextWrapped("Advanced color controls for detailed customization of all UI elements."); - + static ImGuiTextFilter filter; filter.Draw("Filter colors", ImGui::GetFontSize() * 16); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 7e47e7aa9e..f3e6414fde 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -16,8 +16,8 @@ #include "State.h" #include "Util.h" -#include "../Utils/FileSystem.h" #include "../Util.h" +#include "../Utils/FileSystem.h" using namespace SKSE; @@ -50,14 +50,14 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // rescale here auto styleCopy = themeSettings.Style; - + float globalScale = themeSettings.GlobalScale; - + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 } - + styleCopy.ScaleAllSizes(exp2(globalScale)); styleCopy.MouseCursorScale = 1.f; style = styleCopy; @@ -68,7 +68,7 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) for (size_t i = 0; i < std::min(themeSettings.FullPalette.size(), static_cast(ImGuiCol_COUNT)); ++i) { colors[i] = themeSettings.FullPalette[i]; } - + // Apply simple palette overrides to the FullPalette for key colors // This allows the simple palette controls to work by updating the FullPalette colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; @@ -76,12 +76,12 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) colors[ImGuiCol_Border] = themeSettings.Palette.Border; colors[ImGuiCol_Separator] = themeSettings.Palette.Border; colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.Border; - + // Apply derived colors based on simple palette ImVec4 textDisabled = themeSettings.Palette.Text; textDisabled.w = 0.3f; colors[ImGuiCol_TextDisabled] = textDisabled; - + ImVec4 resizeGripHovered = themeSettings.Palette.Border; resizeGripHovered.w = 0.1f; colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; @@ -103,7 +103,7 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // Helper function to adjust background color for better contrast with text auto adjustBackgroundForContrast = [&](ImVec4& backgroundColor, float textLuminance) { float bgLuminance = calculateLuminance(backgroundColor); - + if (bgLuminance > 0.5f && textLuminance > 0.5f) { // Both background and text are light - darken the background backgroundColor.x *= 0.4f; @@ -119,15 +119,15 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // Apply contrast-aware adjustments for headers and tabs float textLum = calculateLuminance(colors[ImGuiCol_Text]); - + // Apply contrast adjustments for all header and tab backgrounds using unified logic adjustBackgroundForContrast(colors[ImGuiCol_Header], textLum); adjustBackgroundForContrast(colors[ImGuiCol_HeaderHovered], textLum); adjustBackgroundForContrast(colors[ImGuiCol_HeaderActive], textLum); adjustBackgroundForContrast(colors[ImGuiCol_Tab], textLum); - adjustBackgroundForContrast(colors[ImGuiCol_TabActive], textLum); + adjustBackgroundForContrast(colors[ImGuiCol_TabActive], textLum); adjustBackgroundForContrast(colors[ImGuiCol_TabHovered], textLum); - + // Apply contrast-aware text for selection states (TextSelectedBg is used when text is selected) if (calculateLuminance(colors[ImGuiCol_HeaderActive]) > 0.5f) { colors[ImGuiCol_TextSelectedBg] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black text on light selection @@ -150,14 +150,14 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) logger::warn("ThemeManager::ReloadFont() - Font reload already in progress, skipping"); return; } - + isReloading = true; auto& themeSettings = menu.GetTheme(); logger::info("ThemeManager::ReloadFont() - Starting font reload..."); ImGuiIO& io = ImGui::GetIO(); - + // Additional safety checks: ensure ImGui is in a valid state ImGuiContext* ctx = ImGui::GetCurrentContext(); if (!ctx) { @@ -165,21 +165,21 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) isReloading = false; return; } - + // Ensure we're not in the middle of a frame if (ctx->WithinFrameScope) { logger::error("ThemeManager::ReloadFont() - Cannot reload font within frame scope!"); isReloading = false; return; } - + // Additional check: make sure font atlas exists if (!io.Fonts) { logger::error("ThemeManager::ReloadFont() - No font atlas available!"); isReloading = false; return; } - + // Clear existing fonts from the atlas io.Fonts->Clear(); @@ -199,13 +199,13 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) io.Fonts->AddFontDefault(); } else { auto fontPath = Util::PathHelpers::GetFontsPath() / themeSettings.FontName; - + // Check if font file exists before trying to load it if (!std::filesystem::exists(fontPath)) { logger::warn("ThemeManager::ReloadFont() - Font file '{}' does not exist. Using default font.", fontPath.string()); io.Fonts->AddFontDefault(); } else if (!io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), - std::round(fontSize), &font_config)) { + std::round(fontSize), &font_config)) { logger::warn("ThemeManager::ReloadFont() - Failed to load custom font '{}'. Using default font.", themeSettings.FontName); io.Fonts->AddFontDefault(); } else { @@ -222,32 +222,32 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) // Recreate device objects - this is where the crash was likely happening // We need to be very careful about the order and ensure everything is valid - + // Important: We must ensure ImGui is not in the middle of any rendering operations // The deferred execution should guarantee this, but let's be extra safe - + logger::debug("ThemeManager::ReloadFont() - Invalidating DX11 device objects..."); ImGui_ImplDX11_InvalidateDeviceObjects(); - + logger::debug("ThemeManager::ReloadFont() - Creating DX11 device objects..."); if (!ImGui_ImplDX11_CreateDeviceObjects()) { logger::error("ThemeManager::ReloadFont() - Failed to create device objects!"); - + // Emergency fallback: try to restore with default font io.Fonts->Clear(); io.Fonts->AddFontDefault(); io.Fonts->Build(); ImGui_ImplDX11_InvalidateDeviceObjects(); ImGui_ImplDX11_CreateDeviceObjects(); - + isReloading = false; return; } - + logger::debug("ThemeManager::ReloadFont() - Device objects recreated successfully"); float globalScale = themeSettings.GlobalScale; - + // Use default global scale (0.0) for built-in themes when GlobalScale equals the default if (std::abs(globalScale - Constants::DEFAULT_GLOBAL_SCALE) < 0.001f) { globalScale = Constants::DEFAULT_GLOBAL_SCALE; // Ensure built-in themes stay at 0.0 @@ -258,7 +258,7 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) cachedFontSize = themeSettings.FontSize; // Also update cached font name in the menu instance const_cast(menu).cachedFontName = themeSettings.FontName; - + logger::info("ThemeManager::ReloadFont() - Font reload completed successfully"); isReloading = false; } @@ -290,7 +290,7 @@ size_t ThemeManager::DiscoverThemes() // Check file size auto fileSize = entry.file_size(); if (fileSize > MAX_FILE_SIZE) { - logger::warn("Theme file too large, skipping: {} ({}MB)", + logger::warn("Theme file too large, skipping: {} ({}MB)", entry.path().filename().string(), fileSize / (1024 * 1024)); continue; } @@ -324,11 +324,11 @@ std::vector ThemeManager::GetThemeNames() const { std::vector names; names.reserve(themes.size()); - + for (const auto& theme : themes) { names.push_back(theme.name); } - + return names; } @@ -343,7 +343,7 @@ bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) return true; } - auto it = std::find_if(themes.begin(), themes.end(), + auto it = std::find_if(themes.begin(), themes.end(), [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); if (it == themes.end()) { @@ -371,8 +371,8 @@ bool ThemeManager::LoadTheme(const std::string& themeName, json& themeSettings) } } -bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSettings, - const std::string& displayName, const std::string& description) +bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description) { if (themeName.empty()) { logger::warn("Cannot save theme with empty name"); @@ -381,18 +381,16 @@ bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSett // Create the full theme JSON structure json fullTheme = { - {"DisplayName", displayName.empty() ? themeName : displayName}, - {"Description", description.empty() ? "Custom user theme" : description}, - {"Version", "1.0.0"}, - {"Author", "User"}, - {"Theme", themeSettings} + { "DisplayName", displayName.empty() ? themeName : displayName }, + { "Description", description.empty() ? "Custom user theme" : description }, + { "Version", "1.0.0" }, + { "Author", "User" }, + { "Theme", themeSettings } }; // Generate safe filename (remove invalid characters) std::string safeFileName = themeName; - std::replace_if(safeFileName.begin(), safeFileName.end(), - [](char c) { return c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|'; }, - '_'); + std::replace_if(safeFileName.begin(), safeFileName.end(), [](char c) { return c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|'; }, '_'); auto themesDir = GetThemesDirectory(); auto filePath = themesDir / (safeFileName + ".json"); @@ -415,7 +413,7 @@ bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSett // Refresh themes to include the new one RefreshThemes(); - + return true; } catch (const std::exception& e) { logger::warn("Error saving theme {}: {}", themeName, e.what()); @@ -425,9 +423,9 @@ bool ThemeManager::SaveTheme(const std::string& themeName, const json& themeSett const ThemeManager::ThemeInfo* ThemeManager::GetThemeInfo(const std::string& themeName) const { - auto it = std::find_if(themes.begin(), themes.end(), + auto it = std::find_if(themes.begin(), themes.end(), [&themeName](const ThemeInfo& theme) { return theme.name == themeName; }); - + return (it != themes.end()) ? &(*it) : nullptr; } @@ -437,20 +435,18 @@ void ThemeManager::RefreshThemes() DiscoverThemes(); } - std::filesystem::path ThemeManager::GetThemesDirectory() const { return Util::PathHelpers::GetThemesPath(); } - // Compute effective font size (user value or dynamic default) - float fontSize = ResolveFontSize(menu); - +// Compute effective font size (user value or dynamic default) +float fontSize = ResolveFontSize(menu); void ThemeManager::CreateDefaultThemeFiles() { auto themesDir = GetThemesDirectory(); - + try { std::filesystem::create_directories(themesDir); logger::info("Ensured themes directory exists: {}", themesDir.string()); @@ -504,7 +500,6 @@ void ThemeManager::CreateDefaultThemeFiles() } })"; - file.close(); logger::info("Created default theme file: {}", defaultThemeFile.string()); } catch (const std::exception& e) { @@ -535,7 +530,7 @@ std::unique_ptr ThemeManager::LoadThemeFile(const std:: } themeInfo->themeData = data; - + // Extract metadata if (data.contains("DisplayName") && data["DisplayName"].is_string()) { themeInfo->displayName = data["DisplayName"].get(); @@ -556,7 +551,7 @@ std::unique_ptr ThemeManager::LoadThemeFile(const std:: } themeInfo->isValid = true; - + } catch (const std::exception& e) { logger::warn("Error parsing theme file {}: {}", filePath.string(), e.what()); } @@ -596,5 +591,4 @@ float ThemeManager::ResolveFontSize(const Menu& menu) logger::warn("ThemeManager::ResolveFontSize() - Falling back to DEFAULT_FONT_SIZE due to missing screen height."); } return std::clamp(dynamicSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); - } \ No newline at end of file diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index bf6f72793b..c6eadd9c44 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -1,10 +1,10 @@ #pragma once #include +#include #include #include #include -#include using json = nlohmann::json; @@ -21,16 +21,15 @@ using json = nlohmann::json; class ThemeManager { public: - struct ThemeInfo { - std::string name; // Filename without extension - std::string displayName; // Human-readable name from JSON - std::string description; // Theme description from JSON - std::string filePath; // Full path to theme file + std::string name; // Filename without extension + std::string displayName; // Human-readable name from JSON + std::string description; // Theme description from JSON + std::string filePath; // Full path to theme file json themeData; // Complete theme settings bool isValid = false; // Whether theme loaded successfully - + // Metadata std::string version; std::string author; @@ -43,7 +42,6 @@ class ThemeManager // default based on current screen resolution is returned; otherwise the user value. static float ResolveFontSize(const class Menu& menu); - struct Constants { // Font size constants @@ -54,7 +52,7 @@ class ThemeManager static constexpr float DEFAULT_FONT_SIZE = 27.0f; // Global scale constants - static constexpr float DEFAULT_GLOBAL_SCALE = 0.0f; // Default global scale for built-in themes + static constexpr float DEFAULT_GLOBAL_SCALE = 0.0f; // Default global scale for built-in themes // Font configuration constants static constexpr int FCONF_OVERSAMPLE_H = 3; // ImGui default = 2 @@ -127,8 +125,8 @@ class ThemeManager * @param description Description for the theme * @return True if theme was saved successfully */ - bool SaveTheme(const std::string& themeName, const json& themeSettings, - const std::string& displayName, const std::string& description); + bool SaveTheme(const std::string& themeName, const json& themeSettings, + const std::string& displayName, const std::string& description); /** * @brief Gets theme info by name @@ -181,6 +179,6 @@ class ThemeManager bool discovered = false; // Constants - static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading + static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size }; \ No newline at end of file diff --git a/src/State.cpp b/src/State.cpp index a02f5e9afd..31a769f068 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -831,7 +831,7 @@ float State::GetTotalSmoothedDrawCalls() const void State::LoadTheme() { auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); - + if (!std::filesystem::exists(themeConfigPath)) { logger::info("No theme config file found at: {}", themeConfigPath.string()); return; @@ -860,7 +860,7 @@ void State::LoadTheme() void State::SaveTheme() { auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); - + try { std::filesystem::create_directories(themeConfigPath.parent_path()); } catch (const std::filesystem::filesystem_error& e) { @@ -879,6 +879,6 @@ void State::SaveTheme() themeFile << std::setw(4) << themeSettings << std::endl; themeFile.close(); - + logger::info("Theme settings saved to: {}", themeConfigPath.string()); } diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index b56433080e..8ec6dd4c10 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,7 +1,7 @@ #include "PCH.h" -#include "UI.h" #include "Menu.h" +#include "UI.h" #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -776,11 +775,11 @@ namespace Util ImVec2 center = ImVec2(iconPos.x + iconSize * 0.46f, iconPos.y + iconSize * 0.5f); float radius = iconSize * 0.3f; - + // Use themed text color with reduced alpha for search icon auto& theme = globals::menu->GetTheme().Palette; ImVec4 iconColor = theme.Text; - iconColor.w *= 0.7f; // Reduce alpha for subtler appearance + iconColor.w *= 0.7f; // Reduce alpha for subtler appearance ImU32 placeholderColor = ImGui::GetColorU32(iconColor); // Draw circle @@ -1213,132 +1212,132 @@ namespace Util // Color utilities for contrast and readability namespace ColorUtils + { + float CalculateLuminance(const ImVec4& color) { - float CalculateLuminance(const ImVec4& color) - { - // Convert to linear RGB first (gamma correction) - auto toLinear = [](float c) { - return c <= 0.03928f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); - }; - - float r = toLinear(color.x); - float g = toLinear(color.y); - float b = toLinear(color.z); - - // Calculate relative luminance using WCAG formula - return 0.2126f * r + 0.7152f * g + 0.0722f * b; - } + // Convert to linear RGB first (gamma correction) + auto toLinear = [](float c) { + return c <= 0.03928f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); + }; - ImVec4 GetContrastingTextColor(const ImVec4& backgroundColor, float threshold) - { - float luminance = CalculateLuminance(backgroundColor); - - // If background is bright (high luminance), use black text - // If background is dark (low luminance), use white text - if (luminance > threshold) { - return ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black - } else { - return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White - } - } + float r = toLinear(color.x); + float g = toLinear(color.y); + float b = toLinear(color.z); + + // Calculate relative luminance using WCAG formula + return 0.2126f * r + 0.7152f * g + 0.0722f * b; + } - float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2) - { - float lum1 = CalculateLuminance(color1); - float lum2 = CalculateLuminance(color2); - - // Ensure lighter color is in numerator - float lighter = (std::max)(lum1, lum2); - float darker = (std::min)(lum1, lum2); - - return (lighter + 0.05f) / (darker + 0.05f); + ImVec4 GetContrastingTextColor(const ImVec4& backgroundColor, float threshold) + { + float luminance = CalculateLuminance(backgroundColor); + + // If background is bright (high luminance), use black text + // If background is dark (low luminance), use white text + if (luminance > threshold) { + return ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black + } else { + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White } + } + + float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2) + { + float lum1 = CalculateLuminance(color1); + float lum2 = CalculateLuminance(color2); + + // Ensure lighter color is in numerator + float lighter = (std::max)(lum1, lum2); + float darker = (std::min)(lum1, lum2); - bool ContrastSelectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size) - { - // Get current style colors for different states - ImGuiStyle& style = ImGui::GetStyle(); - - // We need to handle text color based on the selectable's background state - // For selected items, ImGui uses HeaderActive color which might be light - ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; - ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; - - // Calculate text colors for each state - ImVec4 selectedTextColor = GetContrastingTextColor(selectedBgColor, 0.5f); - ImVec4 hoveredTextColor = GetContrastingTextColor(hoveredBgColor, 0.5f); - ImVec4 normalTextColor = style.Colors[ImGuiCol_Text]; - - // If the item is selected, we know it will have the selected background - if (selected) { - ImGui::PushStyleColor(ImGuiCol_Text, selectedTextColor); + return (lighter + 0.05f) / (darker + 0.05f); + } + + bool ContrastSelectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size) + { + // Get current style colors for different states + ImGuiStyle& style = ImGui::GetStyle(); + + // We need to handle text color based on the selectable's background state + // For selected items, ImGui uses HeaderActive color which might be light + ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; + ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; + + // Calculate text colors for each state + ImVec4 selectedTextColor = GetContrastingTextColor(selectedBgColor, 0.5f); + ImVec4 hoveredTextColor = GetContrastingTextColor(hoveredBgColor, 0.5f); + ImVec4 normalTextColor = style.Colors[ImGuiCol_Text]; + + // If the item is selected, we know it will have the selected background + if (selected) { + ImGui::PushStyleColor(ImGuiCol_Text, selectedTextColor); + } else { + // For non-selected items, we'll use normal text unless we detect high contrast issues + // Check if hover/active backgrounds would cause contrast issues + float hoveredContrast = CalculateContrastRatio(normalTextColor, hoveredBgColor); + if (hoveredContrast < 3.0f) { // WCAG AA minimum is 4.5, but 3.0 for safety + ImGui::PushStyleColor(ImGuiCol_Text, hoveredTextColor); } else { - // For non-selected items, we'll use normal text unless we detect high contrast issues - // Check if hover/active backgrounds would cause contrast issues - float hoveredContrast = CalculateContrastRatio(normalTextColor, hoveredBgColor); - if (hoveredContrast < 3.0f) { // WCAG AA minimum is 4.5, but 3.0 for safety - ImGui::PushStyleColor(ImGuiCol_Text, hoveredTextColor); - } else { - ImGui::PushStyleColor(ImGuiCol_Text, normalTextColor); - } + ImGui::PushStyleColor(ImGuiCol_Text, normalTextColor); } - - // Create the selectable with the adjusted text color - bool result = ImGui::Selectable(label, selected, flags, size); - - // Restore original text color - ImGui::PopStyleColor(); - - return result; } - bool ContrastSelectableWithColor(const char* label, bool selected, const ImVec4& semanticTextColor, ImGuiSelectableFlags flags, const ImVec2& size) - { - // Get current style colors for different states - ImGuiStyle& style = ImGui::GetStyle(); - - // We need to handle text color based on the selectable's background state - // For selected items, ImGui uses HeaderActive color which might be light - ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; - ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; - - // Use the provided semantic color but ensure it has good contrast - ImVec4 textColor = semanticTextColor; - - // If the item is selected, we know it will have the selected background - if (selected) { - // Check contrast with selected background - float contrast = CalculateContrastRatio(semanticTextColor, selectedBgColor); - if (contrast < 3.0f) { - textColor = GetContrastingTextColor(selectedBgColor, 0.5f); - } - } else { - // Check contrast with potential hover background - float hoveredContrast = CalculateContrastRatio(semanticTextColor, hoveredBgColor); - if (hoveredContrast < 3.0f) { - textColor = GetContrastingTextColor(hoveredBgColor, 0.5f); - } + // Create the selectable with the adjusted text color + bool result = ImGui::Selectable(label, selected, flags, size); + + // Restore original text color + ImGui::PopStyleColor(); + + return result; + } + + bool ContrastSelectableWithColor(const char* label, bool selected, const ImVec4& semanticTextColor, ImGuiSelectableFlags flags, const ImVec2& size) + { + // Get current style colors for different states + ImGuiStyle& style = ImGui::GetStyle(); + + // We need to handle text color based on the selectable's background state + // For selected items, ImGui uses HeaderActive color which might be light + ImVec4 selectedBgColor = style.Colors[ImGuiCol_HeaderActive]; + ImVec4 hoveredBgColor = style.Colors[ImGuiCol_HeaderHovered]; + + // Use the provided semantic color but ensure it has good contrast + ImVec4 textColor = semanticTextColor; + + // If the item is selected, we know it will have the selected background + if (selected) { + // Check contrast with selected background + float contrast = CalculateContrastRatio(semanticTextColor, selectedBgColor); + if (contrast < 3.0f) { + textColor = GetContrastingTextColor(selectedBgColor, 0.5f); + } + } else { + // Check contrast with potential hover background + float hoveredContrast = CalculateContrastRatio(semanticTextColor, hoveredBgColor); + if (hoveredContrast < 3.0f) { + textColor = GetContrastingTextColor(hoveredBgColor, 0.5f); } - - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - - // Create the selectable with the adjusted text color - bool result = ImGui::Selectable(label, selected, flags, size); - - // Restore original text color - ImGui::PopStyleColor(); - - return result; } - } // namespace ColorUtils + + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + + // Create the selectable with the adjusted text color + bool result = ImGui::Selectable(label, selected, flags, size); + + // Restore original text color + ImGui::PopStyleColor(); + + return result; + } + } // namespace ColorUtils bool ButtonWithFlash(const char* label, const ImVec2& size, int flashDurationMs) { static std::unordered_map flashTimers; - + std::string buttonId = std::string(label); auto now = std::chrono::steady_clock::now(); - + // Check if this button has active flash bool hasActiveFlash = false; auto it = flashTimers.find(buttonId); @@ -1351,7 +1350,7 @@ namespace Util flashTimers.erase(it); } } - + // Style the button with flash effect if active. bool styleChanged = false; if (hasActiveFlash) { @@ -1361,52 +1360,51 @@ namespace Util normalButton.x + 0.2f, // Brighten slightly normalButton.y + 0.2f, normalButton.z + 0.2f, - normalButton.w - ); + normalButton.w); ImVec4 flashHovered = ImVec4(flashColor.x * 1.1f, flashColor.y * 1.1f, flashColor.z * 1.1f, flashColor.w); ImVec4 flashActive = ImVec4(flashColor.x * 0.9f, flashColor.y * 0.9f, flashColor.z * 0.9f, flashColor.w); - + ImGui::PushStyleColor(ImGuiCol_Button, flashColor); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, flashHovered); ImGui::PushStyleColor(ImGuiCol_ButtonActive, flashActive); styleChanged = true; } - + bool clicked = ImGui::Button(label, size); - + if (styleChanged) { ImGui::PopStyleColor(3); } - + // If clicked, start the flash timer if (clicked) { flashTimers[buttonId] = now; } - + return clicked; } std::vector DiscoverFonts() { std::vector fonts; - + try { auto fontsPath = Util::PathHelpers::GetFontsPath(); logger::debug("DiscoverFonts: Scanning fonts directory: {}", fontsPath.string()); - + // Check if fonts directory exists if (!std::filesystem::exists(fontsPath)) { logger::warn("DiscoverFonts: Fonts directory does not exist: {}", fontsPath.string()); return fonts; } - + // Scan for font files (.ttf and .otf) for (const auto& entry : std::filesystem::directory_iterator(fontsPath)) { if (entry.is_regular_file()) { auto extension = entry.path().extension().string(); // Convert to lowercase for comparison std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - + if (extension == ".ttf" || extension == ".otf") { // Use the full filename (including extension) for proper font loading std::string fontFile = entry.path().filename().string(); @@ -1415,16 +1413,15 @@ namespace Util } } } - + // Sort fonts alphabetically for better user experience std::sort(fonts.begin(), fonts.end()); logger::info("DiscoverFonts: Found {} font files", fonts.size()); - } - catch (const std::exception& e) { + } catch (const std::exception& e) { logger::error("DiscoverFonts: Exception occurred while scanning fonts: {}", e.what()); // Silently return empty vector on error } - + return fonts; } @@ -1437,7 +1434,7 @@ namespace Util try { auto fontsPath = Util::PathHelpers::GetFontsPath(); auto fontPath = fontsPath / fontName; - + // Check if the font file exists and is a regular file if (std::filesystem::exists(fontPath) && std::filesystem::is_regular_file(fontPath)) { // Validate extension is .ttf or .otf @@ -1445,11 +1442,10 @@ namespace Util std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); return extension == ".ttf" || extension == ".otf"; } - } - catch (const std::exception& e) { + } catch (const std::exception& e) { logger::error("ValidateFont: Exception occurred while validating font '{}': {}", fontName, e.what()); } - + return false; } } // namespace Util diff --git a/src/Utils/UI.h b/src/Utils/UI.h index b61c69c034..d6f1a1e964 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -109,7 +109,6 @@ namespace Util int m_pushedStyles; }; - /** * Button with simple flash feedback (matches action icon hover effect style) * @param label Button text @@ -194,7 +193,7 @@ namespace Util /** * Calculates contrast ratio between two colors according to WCAG guidelines * @param color1 First color - * @param color2 Second color + * @param color2 Second color * @return Contrast ratio (1.0 = no contrast, 21.0 = maximum contrast) */ float CalculateContrastRatio(const ImVec4& color1, const ImVec4& color2); diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index a738749576..e0cba703ea 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -4,9 +4,9 @@ #include "Globals.h" #include "Hooks.h" #include "Menu.h" +#include "Menu/ThemeManager.h" #include "ShaderCache.h" #include "State.h" -#include "Menu/ThemeManager.h" #include "TruePBR.h" #include "ENB/ENBSeriesAPI.h" @@ -163,12 +163,12 @@ bool Load() auto state = globals::state; state->Load(); state->LoadTheme(); // Load theme settings from SettingsTheme.json - + // Initialize theme system - create default themes and discover existing ones globals::menu->CreateDefaultThemes(); // Creates JSON files if they don't exist auto themeManager = ThemeManager::GetSingleton(); themeManager->DiscoverThemes(); // Discover all available themes - + auto log = spdlog::default_logger(); log->set_level(state->GetLogLevel()); From 38395cad8270ebfe62c596bde8b4f7bb2922a572 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 27 Sep 2025 21:24:10 +1000 Subject: [PATCH 14/29] build fixes --- src/Menu/ThemeManager.cpp | 7 +------ src/Menu/ThemeManager.h | 8 ++++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index f3e6414fde..70ffc6a4ab 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -16,6 +16,7 @@ #include "State.h" #include "Util.h" +#include "../Globals.h" #include "../Util.h" #include "../Utils/FileSystem.h" @@ -440,9 +441,6 @@ std::filesystem::path ThemeManager::GetThemesDirectory() const return Util::PathHelpers::GetThemesPath(); } -// Compute effective font size (user value or dynamic default) -float fontSize = ResolveFontSize(menu); - void ThemeManager::CreateDefaultThemeFiles() { auto themesDir = GetThemesDirectory(); @@ -568,9 +566,6 @@ bool ThemeManager::ValidateThemeData(const json& themeData) const // Could add more detailed validation here if needed return true; - - // Cache the effective size so we can detect changes accurately - cachedFontSize = fontSize; } float ThemeManager::ResolveFontSize(const Menu& menu) diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index c6eadd9c44..09d7f14dcb 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -36,11 +36,13 @@ class ThemeManager std::time_t lastModified = 0; }; - static void SetupImGuiStyle(const class Menu& menu); - static void ReloadFont(const class Menu& menu, float& cachedFontSize); // Returns the effective font size to use. If the user setting is <= 0, a dynamic // default based on current screen resolution is returned; otherwise the user value. static float ResolveFontSize(const class Menu& menu); + + // Static UI helper methods + static void SetupImGuiStyle(const class Menu& menu); + static void ReloadFont(const class Menu& menu, float& cachedFontSize); struct Constants { @@ -88,8 +90,6 @@ class ThemeManager } // Static UI helper methods - static void SetupImGuiStyle(const class Menu& menu); - static void ReloadFont(const class Menu& menu, float& cachedFontSize); /** * @brief Discovers all theme files in the themes directory From 70cfb31ae05cf8e2bc95667676f7fc75693a2456 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:24:32 +0000 Subject: [PATCH 15/29] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Menu/ThemeManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 09d7f14dcb..6d549db859 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -39,7 +39,7 @@ class ThemeManager // Returns the effective font size to use. If the user setting is <= 0, a dynamic // default based on current screen resolution is returned; otherwise the user value. static float ResolveFontSize(const class Menu& menu); - + // Static UI helper methods static void SetupImGuiStyle(const class Menu& menu); static void ReloadFont(const class Menu& menu, float& cachedFontSize); From 24a0428eee2215e0bcf6921ac8af8c24635407d6 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 18:53:10 +1000 Subject: [PATCH 16/29] fixes --- package/SKSE/Plugins/CommunityShaders/Themes/Default.json | 2 +- package/SKSE/Plugins/CommunityShaders/Themes/README.md | 2 +- src/Menu/SettingsTabRenderer.cpp | 4 ++++ src/Menu/ThemeManager.cpp | 5 ++--- src/SettingsOverrideManager.cpp | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 84c66b7ffd..14298c2152 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -1,6 +1,6 @@ { "DisplayName": "Default Dark", - "Description": "The classic Community Shaders dark theme with comprehensive styling", + "Description": "The classic Community Shaders dark theme with modern styling", "Version": "1.0.0", "Author": "Community Shaders Team", "Theme": { diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/README.md b/package/SKSE/Plugins/CommunityShaders/Themes/README.md index 23f6ff21a0..c7c350d364 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/README.md +++ b/package/SKSE/Plugins/CommunityShaders/Themes/README.md @@ -7,7 +7,7 @@ This directory contains JSON theme files that can be hot-swapped at runtime with The theme system automatically discovers `.json` files in this directory and makes them available in the Community Shaders menu. Simply: 1. Create or edit a `.json` theme file in this directory -2. Click "Refresh Themes" in the Colors tab of the Community Shaders menu +2. Click "Refresh Themes" in the Themes tab of the Community Shaders menu 3. Select your theme from the dropdown ## Theme File Format diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index f907ea30cb..9efc15bff3 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -202,6 +202,10 @@ void SettingsTabRenderer::RenderThemesTab() displayNames.clear(); items.clear(); + // Reserve capacity to prevent reallocations that would invalidate pointers + displayNames.reserve(themes.size() + 1); + items.reserve(themes.size() + 1); + // Add "+ Create New" option at the top displayNames.push_back("+ Create New"); items.push_back(displayNames.back().c_str()); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 70ffc6a4ab..9e84e6108e 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -191,8 +191,7 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) font_config.PixelSnapH = Constants::FCONF_PIXELSNAP_H; font_config.RasterizerMultiply = Constants::FCONF_RASTERIZER_MULTIPLY; - float fontSize = themeSettings.FontSize; - fontSize = std::clamp(fontSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); + float fontSize = ResolveFontSize(menu); // Check if font name is empty or invalid if (themeSettings.FontName.empty()) { @@ -256,7 +255,7 @@ void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) io.FontGlobalScale = exp2(globalScale); - cachedFontSize = themeSettings.FontSize; + cachedFontSize = fontSize; // Also update cached font name in the menu instance const_cast(menu).cachedFontName = themeSettings.FontName; diff --git a/src/SettingsOverrideManager.cpp b/src/SettingsOverrideManager.cpp index 146f63f61b..1aab846fbc 100644 --- a/src/SettingsOverrideManager.cpp +++ b/src/SettingsOverrideManager.cpp @@ -1,7 +1,7 @@ #include "SettingsOverrideManager.h" #include "FeatureIssues.h" -#include "Utils/FileSystem.h" +#include "Util.h" #include #include From 9e97656a7998dcdeff27c70f77c58a0c10f29e84 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 22:20:29 +1000 Subject: [PATCH 17/29] RAAAAHHHHH IT WORKS --- .../CommunityShaders/Themes/Default.json | 3 +- .../Menu/Shaders/GaussianBlur_Horizontal.hlsl | 75 ++ .../Menu/Shaders/GaussianBlur_Vertical.hlsl | 75 ++ src/Menu.cpp | 36 + src/Menu.h | 58 +- src/Menu/FeatureListRenderer.cpp | 120 +-- src/Menu/OverlayRenderer.cpp | 5 + src/Menu/SettingsTabRenderer.cpp | 6 + src/Menu/ThemeManager.cpp | 719 ++++++++++++++++++ src/Menu/ThemeManager.h | 35 +- src/State.cpp | 6 + src/Utils/UI.cpp | 82 +- src/Utils/UI.h | 9 + 13 files changed, 1147 insertions(+), 82 deletions(-) create mode 100644 src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl create mode 100644 src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 14298c2152..4c48d07e59 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -7,8 +7,9 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { - "Background": [0.09, 0.09, 0.09, 0.95], + "Background": [0.1, 0.1, 0.1, 1.0], "Text": [1.0, 1.0, 1.0, 1.0], "Border": [0.5, 0.5, 0.5, 0.8] }, diff --git a/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl new file mode 100644 index 0000000000..5c66f5616a --- /dev/null +++ b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl @@ -0,0 +1,75 @@ +// Horizontal Gaussian Blur Shader +// Based on Unrimp rendering engine's separable blur implementation +// Used for ImGui background blur effects + +// Uniforms +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_INPUT +{ + float4 Position : POSITION; + float2 TexCoord : TEXCOORD0; +}; + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +// Vertex Shader - Full screen triangle +VS_OUTPUT VS_Main(VS_INPUT input) +{ + VS_OUTPUT output; + output.Position = input.Position; + output.TexCoord = input.TexCoord; + return output; +} + +// Gaussian weight calculation based on Unrimp's implementation +float GaussianWeight(float offset) +{ + const float SIGMA = 0.5f; // Unrimp's SIGMA value + const float v = 2.0f * SIGMA * SIGMA; + return exp(-(offset * offset) / v) / (3.14159265f * v); +} + +// Pixel Shader - Horizontal Gaussian Blur +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); + float totalWeight = 0.0f; + + // Use configurable blur samples (default 7 like Unrimp's SHADOW_MAP_FILTER_SIZE) + const int samples = min(BlurParams.x, 15); // Cap at 15 for performance + const int halfSamples = samples / 2; + + // Sample horizontally + for (int i = -halfSamples; i <= halfSamples; ++i) + { + float2 sampleCoord = input.TexCoord + float2(i * TexelSize.x * TexelSize.z, 0.0f); + float weight = GaussianWeight(float(i)); + + // Sample the texture with proper bounds checking + if (sampleCoord.x >= 0.0f && sampleCoord.x <= 1.0f) + { + result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; + totalWeight += weight; + } + } + + // Normalize by total weight to maintain brightness + if (totalWeight > 0.0f) + { + result /= totalWeight; + } + + return result; +} \ No newline at end of file diff --git a/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl new file mode 100644 index 0000000000..29d05f7445 --- /dev/null +++ b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl @@ -0,0 +1,75 @@ +// Vertical Gaussian Blur Shader +// Based on Unrimp rendering engine's separable blur implementation +// Used for ImGui background blur effects - Second pass (vertical) + +// Uniforms +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_INPUT +{ + float4 Position : POSITION; + float2 TexCoord : TEXCOORD0; +}; + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +// Vertex Shader - Full screen triangle +VS_OUTPUT VS_Main(VS_INPUT input) +{ + VS_OUTPUT output; + output.Position = input.Position; + output.TexCoord = input.TexCoord; + return output; +} + +// Gaussian weight calculation based on Unrimp's implementation +float GaussianWeight(float offset) +{ + const float SIGMA = 0.5f; // Unrimp's SIGMA value + const float v = 2.0f * SIGMA * SIGMA; + return exp(-(offset * offset) / v) / (3.14159265f * v); +} + +// Pixel Shader - Vertical Gaussian Blur +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); + float totalWeight = 0.0f; + + // Use configurable blur samples (default 7 like Unrimp's SHADOW_MAP_FILTER_SIZE) + const int samples = min(BlurParams.x, 15); // Cap at 15 for performance + const int halfSamples = samples / 2; + + // Sample vertically + for (int i = -halfSamples; i <= halfSamples; ++i) + { + float2 sampleCoord = input.TexCoord + float2(0.0f, i * TexelSize.y * TexelSize.z); + float weight = GaussianWeight(float(i)); + + // Sample the texture with proper bounds checking + if (sampleCoord.y >= 0.0f && sampleCoord.y <= 1.0f) + { + result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; + totalWeight += weight; + } + } + + // Normalize by total weight to maintain brightness + if (totalWeight > 0.0f) + { + result /= totalWeight; + } + + return result; +} \ No newline at end of file diff --git a/src/Menu.cpp b/src/Menu.cpp index 0f97ae83e0..969d9ac058 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -115,6 +115,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( UseSimplePalette, ShowActionIcons, TooltipHoverDelay, + BackgroundBlur, ScrollbarOpacity, Palette, StatusPalette, @@ -141,6 +142,7 @@ Menu::~Menu() uiIcons.loadSettings.Release(); uiIcons.clearCache.Release(); uiIcons.logo.Release(); + uiIcons.featureSettingRevert.Release(); uiIcons.discord.Release(); uiIcons.characters.Release(); uiIcons.display.Release(); @@ -153,6 +155,9 @@ Menu::~Menu() uiIcons.materials.Release(); uiIcons.postProcessing.Release(); + // Clean up blur resources + ThemeManager::CleanupBlurResources(); + ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -164,6 +169,20 @@ Menu::~Menu() void Menu::Load(json& o_json) { settings = o_json; + + // Apply Default Dark theme on first launch if no theme is selected + if (!settings.FirstTimeSetupCompleted && settings.SelectedThemePreset.empty()) { + // Ensure default themes are created/available + CreateDefaultThemes(); + + // Load the Default Dark theme and mark it as selected to prevent override + if (LoadThemePreset("Default")) { + settings.SelectedThemePreset = "Default"; // Mark as selected to prevent State::LoadTheme override + logger::info("Applied Default Dark theme on first launch"); + } else { + logger::warn("Failed to load Default Dark theme on first launch"); + } + } } void Menu::Save(json& o_json) @@ -247,6 +266,23 @@ void Menu::Init() // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); + + // IMPORTANT: Immediately override ImGui's default styles with our Default.json theme + // This prevents hardcoded ImGui defaults from ever showing through + auto* themeManager = ThemeManager::GetSingleton(); + json defaultThemeSettings; + if (themeManager->LoadTheme("Default", defaultThemeSettings)) { + // Temporarily create a minimal theme structure to apply defaults + json tempSettings; + tempSettings["Theme"] = defaultThemeSettings; + LoadTheme(tempSettings); + logger::info("Applied Default.json theme immediately after ImGui context creation"); + } else { + logger::warn("Could not load Default.json theme - trying direct force application"); + // Last resort: Apply Default.json colors directly to ImGui + ThemeManager::ForceApplyDefaultTheme(); + } + auto& imgui_io = ImGui::GetIO(); imgui_io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable; imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_HasGamepad; diff --git a/src/Menu.h b/src/Menu.h index ef09141db3..eb3772062a 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -96,6 +96,7 @@ class Menu UIIcon clearCache; UIIcon logo; // New logo icon UIIcon search; // Search icon for search bars + UIIcon featureSettingRevert; // Feature revert settings icon // Social media/external link icons UIIcon discord; @@ -119,52 +120,59 @@ class Menu std::string FontName = "Jost-Regular.ttf"; // Default font file name float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential - bool UseSimplePalette = true; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. + bool UseSimplePalette = false; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds + float BackgroundBlur = 0.0f; // background blur effect intensity // Scrollbar opacity settings struct ScrollbarOpacitySettings { - float Background = 1.0f; // Background of the scrollbar area - float Thumb = 1.0f; // The draggable thumb/grip - float ThumbHovered = 1.0f; // Thumb when hovered - float ThumbActive = 1.0f; // Thumb when being dragged + float Background = 0.0f; // Background of the scrollbar area + float Thumb = 0.5f; // The draggable thumb/grip + float ThumbHovered = 0.75f; // Thumb when hovered + float ThumbActive = 0.9f; // Thumb when being dragged } ScrollbarOpacity; struct PaletteColors { - ImVec4 Background{ 0.f, 0.f, 0.f, 0.5882353186607361f }; - ImVec4 Text{ 1.f, 1.f, 1.f, 1.f }; - ImVec4 Border{ 0.5882353186607361f, 0.5882353186607361f, 0.5882353186607361f, 0.5882353186607361f }; + ImVec4 Background{ 0.10f, 0.10f, 0.10f, 0.80f }; + ImVec4 Text{ 1.0f, 1.0f, 1.0f, 1.0f }; + ImVec4 Border{ 0.5f, 0.5f, 0.5f, 0.8f }; } Palette; struct StatusPaletteColors { - ImVec4 Disable{ 0.5f, 0.5f, 0.5f, 1.f }; - ImVec4 Error{ 1.f, 0.5f, 0.5f, 1.f }; + ImVec4 Disable{ 0.5f, 0.5f, 0.5f, 1.0f }; + ImVec4 Error{ 1.0f, 0.4f, 0.4f, 1.0f }; ImVec4 Warning{ 1.0f, 0.6f, 0.2f, 1.0f }; - ImVec4 RestartNeeded{ 0.5f, 1.f, 0.5f, 1.f }; - ImVec4 CurrentHotkey{ 1.f, 1.f, 0.f, 1.f }; + ImVec4 RestartNeeded{ 0.4f, 1.0f, 0.4f, 1.0f }; + ImVec4 CurrentHotkey{ 1.0f, 1.0f, 0.0f, 1.0f }; ImVec4 SuccessColor{ 0.0f, 1.0f, 0.0f, 1.0f }; - ImVec4 InfoColor{ 0.0f, 0.5f, 1.0f, 1.0f }; + ImVec4 InfoColor{ 0.2f, 0.6f, 1.0f, 1.0f }; } StatusPalette; struct FeatureHeadingColors { - ImVec4 ColorDefault{ 0.47f, 0.47f, 0.47f, 1.00f }; // ~120, 120, 120 - ImVec4 ColorHovered{ 0.39f, 0.39f, 0.39f, 1.00f }; // ~100, 100, 100 - float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized + ImVec4 ColorDefault{ 0.8f, 0.8f, 0.8f, 1.0f }; + ImVec4 ColorHovered{ 0.6f, 0.6f, 0.6f, 1.0f }; + float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized } FeatureHeading; ImGuiStyle Style = []() { ImGuiStyle style = {}; - style.WindowBorderSize = 3.f; - style.ChildBorderSize = 0.f; - style.FrameBorderSize = 1.5f; - style.WindowPadding = { 16.f, 16.f }; - style.WindowRounding = 0.f; - style.IndentSpacing = 8.f; - style.FramePadding = { 4.0f, 4.0f }; - style.CellPadding = { 16.f, 2.f }; - style.ItemSpacing = { 8.f, 12.f }; + style.WindowBorderSize = 2.0f; + style.ChildBorderSize = 0.0f; + style.FrameBorderSize = 1.0f; + style.WindowPadding = { 8.0f, 8.0f }; + style.WindowRounding = 12.0f; + style.IndentSpacing = 8.0f; + style.FramePadding = { 8.0f, 4.0f }; + style.CellPadding = { 8.0f, 2.0f }; + style.ItemSpacing = { 4.0f, 8.0f }; + style.FrameRounding = 4.0f; + style.TabRounding = 4.0f; + style.ScrollbarRounding = 9.0f; + style.ScrollbarSize = 12.0f; + style.GrabRounding = 3.0f; + style.GrabMinSize = 12.0f; return std::move(style); }(); // Theme by @Maksasj, edited by FiveLimbedCat diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 5d811992e2..3c5f16fc13 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -434,6 +434,41 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettingsTab(Feature* fea ImGui::SeparatorText("Error"); ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); } + + // Restore Defaults icon at bottom right (when feature is not disabled and is loaded) + if (!isDisabled && isLoaded) { + // Position at bottom right of the child window + ImVec2 childSize = ImGui::GetWindowSize(); + // Scale icon with font size like other UI elements + float iconDimension = ImGui::GetFrameHeight() * 1.2f; // Larger for better visibility + ImVec2 iconSize = ImVec2(iconDimension, iconDimension); + ImGui::SetCursorPos(ImVec2(childSize.x - iconSize.x - 10.0f, childSize.y - iconSize.y - 10.0f)); auto& theme = globals::menu->GetTheme().Palette; + ImVec4 iconColor = theme.Text; + iconColor.w *= 0.7f; // Reduce alpha for subtler appearance + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // Transparent background + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.3f)); // Subtle hover + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.5f)); // Subtle active + + // Check if icon is available, fallback to text if not + auto& menu = *globals::menu; + if (menu.uiIcons.featureSettingRevert.texture) { + if (ImGui::ImageButton("##RestoreDefaults", menu.uiIcons.featureSettingRevert.texture, iconSize, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0), iconColor)) { + feat->RestoreDefaultSettings(); + } + } else { + // Fallback to small text button if icon not available + if (ImGui::Button("R##RestoreDefaults", iconSize)) { + feat->RestoreDefaultSettings(); + } + } + + ImGui::PopStyleColor(3); + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Restore default settings for this feature"); + } + } } ImGui::EndChild(); ImGui::EndTabItem(); @@ -521,24 +556,19 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* f const auto featureName = feat->GetShortName(); // Calculate button widths based on text content - const char* bootButtonText = isDisabled ? "Enable at Boot" : "Disable at Boot"; - const char* defaultsButtonText = "Restore Defaults"; const char* overrideButtonText = "Apply Override"; - float bootButtonWidth = ImGui::CalcTextSize(bootButtonText).x + buttonPadding; - float defaultsButtonWidth = ImGui::CalcTextSize(defaultsButtonText).x + buttonPadding; + // Toggle is more compact without label - just the toggle width + float bootToggleWidth = ImGui::GetFrameHeight() * 1.6f; float overrideButtonWidth = ImGui::CalcTextSize(overrideButtonText).x + buttonPadding; // Check if override is available for this feature auto overrideManager = SettingsOverrideManager::GetSingleton(); bool hasOverrides = overrideManager && overrideManager->HasFeatureOverrides(featureName); - float totalButtonWidth = bootButtonWidth; - if (!isDisabled && isLoaded) { - totalButtonWidth += defaultsButtonWidth + buttonSpacing; - if (hasOverrides) { - totalButtonWidth += overrideButtonWidth + buttonSpacing; - } + float totalButtonWidth = bootToggleWidth; + if (!isDisabled && isLoaded && hasOverrides) { + totalButtonWidth += overrideButtonWidth + buttonSpacing; } // Position buttons on the right side of the tab bar @@ -549,64 +579,48 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* f ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightOffset); } - // Disable/Enable at boot button - ImVec4 textColor; - if (isDisabled) { - textColor = themeSettings.StatusPalette.Disable; - } else if (!feat->failedLoadedMessage.empty()) { - textColor = themeSettings.StatusPalette.Error; - } else { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + // Enable/Disable at boot toggle + bool bootEnabled = !isDisabled; + + // Apply disabled styling if feature has failed to load + if (!feat->failedLoadedMessage.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); } - - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - if (ImGui::Button(bootButtonText, { bootButtonWidth, 0 })) { + + if (Util::FeatureToggle("##BootToggle", &bootEnabled)) { bool newState = feat->ToggleAtBootSetting(); logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); } - ImGui::PopStyleColor(); + + if (!feat->failedLoadedMessage.empty()) { + ImGui::PopStyleColor(); + } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Current State: %s\n" - "%s the feature settings at boot. " - "Restart will be required to reenable. " - "This is the same as deleting the ini file. " - "This should remove any performance impact for the feature.", - isDisabled ? "Disabled" : "Enabled", - isDisabled ? "Enable" : "Disable"); + "Toggle feature loading at boot.\n" + "Current state: %s\n" + "Restart required for changes to take effect.\n" + "Disabling removes performance impact.", + bootEnabled ? "Enabled" : "Disabled"); } - // Restore Defaults button (when feature is not disabled and is loaded) - if (!isDisabled && isLoaded) { + // Apply Override button (when feature has available overrides) + if (!isDisabled && isLoaded && hasOverrides) { ImGui::SameLine(); - if (ImGui::Button(defaultsButtonText, { defaultsButtonWidth, 0 })) { - feat->RestoreDefaultSettings(); + if (ImGui::Button(overrideButtonText, { overrideButtonWidth, 0 })) { + if (feat->ReapplyOverrideSettings()) { + logger::info("Successfully reapplied override settings for {}", featureName); + } else { + logger::warn("Failed to reapply override settings for {}", featureName); + } } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Restores the feature's settings back to their default values. " + "Reapplies override settings from mod override JSON files. " + "This will overwrite current settings with override values. " "You will still need to Save Settings to make these changes permanent."); } - - // Apply Override button (when feature has available overrides) - if (hasOverrides) { - ImGui::SameLine(); - if (ImGui::Button(overrideButtonText, { overrideButtonWidth, 0 })) { - if (feat->ReapplyOverrideSettings()) { - logger::info("Successfully reapplied override settings for {}", featureName); - } else { - logger::warn("Failed to reapply override settings for {}", featureName); - } - } - - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Reapplies override settings from mod override JSON files. " - "This will overwrite current settings with override values. " - "You will still need to Save Settings to make these changes permanent."); - } - } } } \ No newline at end of file diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 7d0536f855..8090a3d5d4 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -196,6 +196,11 @@ void OverlayRenderer::HandleABTesting() void OverlayRenderer::FinalizeImGuiFrame() { ImGui::Render(); + + // Apply background blur behind ImGui windows before rendering them + // This ensures blur is only applied to areas behind visible windows + ThemeManager::RenderBackgroundBlur(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); if (globals::features::vr.IsOpenVRCompatible()) { diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 9efc15bff3..49653dd338 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -388,6 +388,12 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); } + ImGui::SliderFloat("Background Blur", &themeSettings.BackgroundBlur, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Applies a blur effect to the background."); + } + ImGui::EndTabItem(); } } diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 9e84e6108e..3259c08254 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,21 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // Theme based on https://github.com/powerof3/DialogueHistory auto& themeSettings = menu.GetTheme(); + + // Safety check: If theme appears corrupted/empty, force reload Default.json + // This prevents fallback to ImGui's hardcoded defaults + bool isThemeCorrupted = (themeSettings.FullPalette.size() < ImGuiCol_COUNT / 2) || + (themeSettings.Palette.Background.w == 0.0f && themeSettings.Palette.Text.w == 0.0f); + + if (isThemeCorrupted) { + logger::warn("Theme appears corrupted, attempting to reload Default.json to prevent ImGui defaults"); + auto* nonConstMenu = const_cast(&menu); + if (nonConstMenu->LoadThemePreset("Default")) { + logger::info("Successfully reloaded Default.json theme"); + } else { + logger::error("Failed to reload Default.json - ImGui may show hardcoded defaults"); + } + } // rescale here auto styleCopy = themeSettings.Style; @@ -141,6 +157,180 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) colors[ImGuiCol_ScrollbarGrab].w = themeSettings.ScrollbarOpacity.Thumb; colors[ImGuiCol_ScrollbarGrabHovered].w = themeSettings.ScrollbarOpacity.ThumbHovered; colors[ImGuiCol_ScrollbarGrabActive].w = themeSettings.ScrollbarOpacity.ThumbActive; + + // Apply background blur effect + ApplyBackgroundBlur(themeSettings.BackgroundBlur, colors); +} + +void ThemeManager::ApplyBackgroundBlur(float blurIntensity, ImVec4* colors) +{ + if (blurIntensity <= 0.0f) { + isBlurEnabled = false; + currentBlurIntensity = 0.0f; + return; + } + + // Clamp blur intensity to valid range + blurIntensity = std::clamp(blurIntensity, 0.0f, 1.0f); + + // Store blur parameters for backdrop rendering + currentBlurIntensity = blurIntensity; + isBlurEnabled = true; + + // NOTE: Window transparency is now controlled by the background alpha setting + // The blur intensity only affects the backdrop effect strength, not window alpha + + // Optional: Enhance text contrast very slightly for better readability over backdrops + ImVec4& text = colors[ImGuiCol_Text]; + float contrastBoost = 1.0f + (blurIntensity * 0.05f); // Reduced from 0.15f + text.x = std::min(1.0f, text.x * contrastBoost); + text.y = std::min(1.0f, text.y * contrastBoost); + text.z = std::min(1.0f, text.z * contrastBoost); +} + +void ThemeManager::RenderBackgroundBlur() +{ + // This function should be called after ImGui::Render() but before presenting + // It renders blur behind visible ImGui windows only + + if (!isBlurEnabled || currentBlurIntensity <= 0.0f) { + return; + } + + // Get current theme to check if blur is enabled + auto menu = globals::menu; + if (!menu || !menu->IsEnabled) { + return; + } + + float blurIntensity = menu->GetTheme().BackgroundBlur; + if (blurIntensity <= 0.0f) { + return; + } + + // Update blur intensity from theme settings + currentBlurIntensity = blurIntensity; + + // Initialize blur shaders if needed + if (!InitializeBlurShaders()) { + return; + } + + auto device = globals::d3d::device; + auto context = globals::d3d::context; + if (!device || !context) { + return; + } + + // Get current render target + ID3D11RenderTargetView* currentRTV = nullptr; + context->OMGetRenderTargets(1, ¤tRTV, nullptr); + + if (!currentRTV) { + return; + } + + // Get render target texture and its dimensions + ID3D11Resource* currentRT = nullptr; + currentRTV->GetResource(¤tRT); + + ID3D11Texture2D* currentTexture = nullptr; + HRESULT hr = currentRT->QueryInterface(__uuidof(ID3D11Texture2D), (void**)¤tTexture); + + if (FAILED(hr) || !currentTexture) { + if (currentRT) currentRT->Release(); + if (currentRTV) currentRTV->Release(); + return; + } + + D3D11_TEXTURE2D_DESC texDesc; + currentTexture->GetDesc(&texDesc); + + // Create blur textures if needed + CreateBlurTextures(texDesc.Width, texDesc.Height, texDesc.Format); + + // Find ImGui windows that need blur + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (!ctx || ctx->Windows.Size == 0) { + currentTexture->Release(); + currentRT->Release(); + currentRTV->Release(); + return; + } + + // Apply blur behind each visible ImGui window + for (int i = 0; i < ctx->Windows.Size; i++) { + ImGuiWindow* window = ctx->Windows[i]; + if (!window || window->Hidden || !window->WasActive || window->SkipItems) { + continue; + } + + // Skip if window has no background (fully transparent) + if (window->Flags & ImGuiWindowFlags_NoBackground) { + continue; + } + + // Get window bounds + ImVec2 windowMin = window->Pos; + ImVec2 windowMax = ImVec2(window->Pos.x + window->Size.x, window->Pos.y + window->Size.y); + + // Perform blur for this window area + PerformGaussianBlur(currentTexture, currentRTV, windowMin, windowMax); + } + + // Cleanup + currentTexture->Release(); + currentRT->Release(); + currentRTV->Release(); +} + +void ThemeManager::ForceApplyDefaultTheme() +{ + // This function applies Default.json colors directly to ImGui, bypassing any hardcoded defaults + // It's used when the theme system fails or ImGui resets to defaults unexpectedly + + auto* themeManager = GetSingleton(); + json defaultThemeSettings; + + if (!themeManager->LoadTheme("Default", defaultThemeSettings)) { + logger::warn("ForceApplyDefaultTheme: Could not load Default.json theme"); + return; + } + + auto& style = ImGui::GetStyle(); + auto& colors = style.Colors; + + // Apply the Default.json theme's FullPalette directly to ImGui colors + if (defaultThemeSettings.contains("FullPalette") && defaultThemeSettings["FullPalette"].is_array()) { + auto& palette = defaultThemeSettings["FullPalette"]; + + for (size_t i = 0; i < std::min(palette.size(), static_cast(ImGuiCol_COUNT)); ++i) { + if (palette[i].is_array() && palette[i].size() >= 4) { + colors[i] = ImVec4( + palette[i][0].get(), + palette[i][1].get(), + palette[i][2].get(), + palette[i][3].get() + ); + } + } + logger::info("ForceApplyDefaultTheme: Applied Default.json colors directly to ImGui"); + } else { + logger::warn("ForceApplyDefaultTheme: Default.json missing FullPalette - applying basic dark theme"); + + // Fallback: Apply a basic dark theme that matches Default.json style + colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.05f, 0.05f, 1.0f); // Dark background + colors[ImGuiCol_Text] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text + colors[ImGuiCol_Border] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Gray border + colors[ImGuiCol_ChildBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.0f); // Slightly darker child background + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 1.0f); // Popup background + colors[ImGuiCol_Header] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Header background + colors[ImGuiCol_HeaderHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Header hover + colors[ImGuiCol_HeaderActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Header active + colors[ImGuiCol_Button] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Button background + colors[ImGuiCol_ButtonHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Button hover + colors[ImGuiCol_ButtonActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Button active + } } void ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) @@ -585,4 +775,533 @@ float ThemeManager::ResolveFontSize(const Menu& menu) logger::warn("ThemeManager::ResolveFontSize() - Falling back to DEFAULT_FONT_SIZE due to missing screen height."); } return std::clamp(dynamicSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); +} + +// Blur shader implementation +// https://github.com/cofenberg/unrimp/ +bool ThemeManager::InitializeBlurShaders() +{ + static bool initialized = false; + static bool initializationFailed = false; + + if (initialized || initializationFailed) { + return initialized; + } + + auto device = globals::d3d::device; + if (!device) { + initializationFailed = true; + return false; + } + + try { + // Baked-in HLSL shaders for reliable Gaussian blur implementation + // Based on Unrimp rendering engine's separable blur architecture + + const char* horizontalBlurShader = R"( +// Horizontal Gaussian Blur Shader - Baked into ThemeManager.cpp +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +// Vertex Shader - Fullscreen triangle (no input needed) +VS_OUTPUT VS_Main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + // Generate fullscreen triangle from vertex ID + output.TexCoord = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(output.TexCoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.Position.y = -output.Position.y; // Flip Y for correct orientation + return output; +} + +// Gaussian weight calculation based on Unrimp's implementation +float GaussianWeight(float offset) +{ + const float SIGMA = 0.5f; + const float v = 2.0f * SIGMA * SIGMA; + return exp(-(offset * offset) / v) / (3.14159265f * v); +} + +// Pixel Shader - Horizontal Gaussian Blur +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); + float totalWeight = 0.0f; + + const int samples = min(BlurParams.x, 15); + const int halfSamples = samples / 2; + + for (int i = -halfSamples; i <= halfSamples; ++i) + { + float2 sampleCoord = input.TexCoord + float2(i * TexelSize.x, 0.0f); + float weight = GaussianWeight(float(i)); + + if (sampleCoord.x >= 0.0f && sampleCoord.x <= 1.0f) + { + result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; + totalWeight += weight; + } + } + + if (totalWeight > 0.0f) + result /= totalWeight; + + return result; +} +)"; + + const char* verticalBlurShader = R"( +// Vertical Gaussian Blur Shader - Baked into ThemeManager.cpp +cbuffer BlurBuffer : register(b0) +{ + float4 TexelSize; // x = 1/width, y = 1/height, z = blur strength, w = unused + int4 BlurParams; // x = samples, y = unused, z = unused, w = unused +}; + +SamplerState LinearSampler : register(s0); +Texture2D InputTexture : register(t0); + +struct VS_OUTPUT +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; +}; + +// Vertex Shader - Fullscreen triangle (no input needed) +VS_OUTPUT VS_Main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + // Generate fullscreen triangle from vertex ID + output.TexCoord = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(output.TexCoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.Position.y = -output.Position.y; // Flip Y for correct orientation + return output; +} + +// Gaussian weight calculation based on Unrimp's implementation +float GaussianWeight(float offset) +{ + const float SIGMA = 0.5f; + const float v = 2.0f * SIGMA * SIGMA; + return exp(-(offset * offset) / v) / (3.14159265f * v); +} + +// Pixel Shader - Vertical Gaussian Blur +float4 PS_Main(VS_OUTPUT input) : SV_TARGET +{ + float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); + float totalWeight = 0.0f; + + const int samples = min(BlurParams.x, 15); + const int halfSamples = samples / 2; + + for (int i = -halfSamples; i <= halfSamples; ++i) + { + float2 sampleCoord = input.TexCoord + float2(0.0f, i * TexelSize.y); + float weight = GaussianWeight(float(i)); + + if (sampleCoord.y >= 0.0f && sampleCoord.y <= 1.0f) + { + result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; + totalWeight += weight; + } + } + + if (totalWeight > 0.0f) + result /= totalWeight; + + return result; +} +)"; + + // Compile vertex shader using D3DCompile with baked-in HLSL + ID3DBlob* vsBlob = nullptr; + ID3DBlob* errorBlob = nullptr; + + HRESULT hr = D3DCompile(horizontalBlurShader, strlen(horizontalBlurShader), + "InlineHorizontalBlurShader", nullptr, nullptr, + "VS_Main", "vs_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("Failed to compile baked Gaussian blur vertex shader: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + initializationFailed = true; + return false; + } + + hr = device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &blurVertexShader); + vsBlob->Release(); + + if (FAILED(hr)) { + logger::error("Failed to create Gaussian blur vertex shader"); + initializationFailed = true; + return false; + } + + // Compile horizontal blur pixel shader + ID3DBlob* hpsBlob = nullptr; + hr = D3DCompile(horizontalBlurShader, strlen(horizontalBlurShader), + "InlineHorizontalBlurShader", nullptr, nullptr, + "PS_Main", "ps_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &hpsBlob, &errorBlob); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("Failed to compile baked horizontal Gaussian blur pixel shader: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + initializationFailed = true; + return false; + } + + hr = device->CreatePixelShader(hpsBlob->GetBufferPointer(), hpsBlob->GetBufferSize(), nullptr, &blurHorizontalPixelShader); + hpsBlob->Release(); + + if (FAILED(hr)) { + logger::error("Failed to create horizontal Gaussian blur pixel shader"); + initializationFailed = true; + return false; + } + + // Compile vertical blur pixel shader + ID3DBlob* vpsBlob = nullptr; + hr = D3DCompile(verticalBlurShader, strlen(verticalBlurShader), + "InlineVerticalBlurShader", nullptr, nullptr, + "PS_Main", "ps_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vpsBlob, &errorBlob); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("Failed to compile baked vertical Gaussian blur pixel shader: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + initializationFailed = true; + return false; + } + + hr = device->CreatePixelShader(vpsBlob->GetBufferPointer(), vpsBlob->GetBufferSize(), nullptr, &blurVerticalPixelShader); + vpsBlob->Release(); + + if (FAILED(hr)) { + logger::error("Failed to create vertical Gaussian blur pixel shader"); + initializationFailed = true; + return false; + } + + // Create constant buffer for blur parameters based on Unrimp architecture + D3D11_BUFFER_DESC bufferDesc = {}; + bufferDesc.Usage = D3D11_USAGE_DEFAULT; + bufferDesc.ByteWidth = 32; // Match our BlurConstants struct: float4 texelSize + int4 blurParams + bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + + hr = device->CreateBuffer(&bufferDesc, nullptr, &blurConstantBuffer); + if (FAILED(hr)) { + logger::error("Failed to create blur constant buffer"); + initializationFailed = true; + return false; + } + + // Create sampler state for blur + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.MaxAnisotropy = 1; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + + hr = device->CreateSamplerState(&samplerDesc, &blurSamplerState); + if (FAILED(hr)) { + logger::error("Failed to create blur sampler state"); + initializationFailed = true; + return false; + } + + // Create blend state for proper alpha blending + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.AlphaToCoverageEnable = FALSE; + blendDesc.IndependentBlendEnable = FALSE; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + + hr = device->CreateBlendState(&blendDesc, &blurBlendState); + if (FAILED(hr)) { + logger::error("Failed to create blur blend state"); + initializationFailed = true; + return false; + } + + initialized = true; + logger::info("Gaussian blur shaders initialized successfully with Unrimp architecture"); + return true; + + } catch (const std::exception& e) { + logger::error("Exception during Gaussian blur shader initialization: {}", e.what()); + initializationFailed = true; + return false; + } catch (...) { + logger::error("Unknown exception during Gaussian blur shader initialization"); + initializationFailed = true; + return false; + } +} + +void ThemeManager::CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format) +{ + // Check if textures need to be recreated + if (blurTexture1 && blurTextureWidth == width && blurTextureHeight == height) { + return; + } + + // Clean up existing textures + if (blurTexture1) blurTexture1->Release(); blurTexture1 = nullptr; + if (blurTexture2) blurTexture2->Release(); blurTexture2 = nullptr; + if (blurRTV1) blurRTV1->Release(); blurRTV1 = nullptr; + if (blurRTV2) blurRTV2->Release(); blurRTV2 = nullptr; + if (blurSRV1) blurSRV1->Release(); blurSRV1 = nullptr; + if (blurSRV2) blurSRV2->Release(); blurSRV2 = nullptr; + + auto device = globals::d3d::device; + if (!device) return; + + // Use full resolution textures for better quality + UINT blurWidth = width; + UINT blurHeight = height; + + // Create intermediate blur textures + D3D11_TEXTURE2D_DESC textureDesc = {}; + textureDesc.Width = blurWidth; + textureDesc.Height = blurHeight; + textureDesc.MipLevels = 1; + textureDesc.ArraySize = 1; + textureDesc.Format = format; + textureDesc.SampleDesc.Count = 1; + textureDesc.Usage = D3D11_USAGE_DEFAULT; + textureDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + HRESULT hr = device->CreateTexture2D(&textureDesc, nullptr, &blurTexture1); + if (FAILED(hr)) { + logger::error("Failed to create blur texture 1"); + return; + } + + hr = device->CreateTexture2D(&textureDesc, nullptr, &blurTexture2); + if (FAILED(hr)) { + logger::error("Failed to create blur texture 2"); + return; + } + + // Create render target views + hr = device->CreateRenderTargetView(blurTexture1, nullptr, &blurRTV1); + if (FAILED(hr)) { + logger::error("Failed to create blur RTV 1"); + return; + } + + hr = device->CreateRenderTargetView(blurTexture2, nullptr, &blurRTV2); + if (FAILED(hr)) { + logger::error("Failed to create blur RTV 2"); + return; + } + + // Create shader resource views + hr = device->CreateShaderResourceView(blurTexture1, nullptr, &blurSRV1); + if (FAILED(hr)) { + logger::error("Failed to create blur SRV 1"); + return; + } + + hr = device->CreateShaderResourceView(blurTexture2, nullptr, &blurSRV2); + if (FAILED(hr)) { + logger::error("Failed to create blur SRV 2"); + return; + } + + blurTextureWidth = width; + blurTextureHeight = height; +} + +void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax) +{ + auto context = globals::d3d::context; + if (!context || !sourceTexture || !targetRTV) return; + + // Get source texture description + D3D11_TEXTURE2D_DESC sourceDesc; + sourceTexture->GetDesc(&sourceDesc); + + // Create shader resource view for source + ID3D11ShaderResourceView* sourceSRV = nullptr; + auto device = globals::d3d::device; + HRESULT hr = device->CreateShaderResourceView(sourceTexture, nullptr, &sourceSRV); + if (FAILED(hr)) { + logger::error("Failed to create source SRV for blur"); + return; + } + + // Save current state + ID3D11RenderTargetView* originalRTV = nullptr; + ID3D11DepthStencilView* originalDSV = nullptr; + context->OMGetRenderTargets(1, &originalRTV, &originalDSV); + + D3D11_VIEWPORT originalViewport; + UINT numViewports = 1; + context->RSGetViewports(&numViewports, &originalViewport); + + // Calculate menu area in texture coordinates (for scissor testing) + FLOAT menuLeft = std::max(0.0f, menuMin.x); + FLOAT menuTop = std::max(0.0f, menuMin.y); + FLOAT menuRight = std::min(static_cast(sourceDesc.Width), menuMax.x); + FLOAT menuBottom = std::min(static_cast(sourceDesc.Height), menuMax.y); + + // Set scissor rectangle to limit blur to menu area + D3D11_RECT scissorRect; + scissorRect.left = static_cast(menuLeft); + scissorRect.top = static_cast(menuTop); + scissorRect.right = static_cast(menuRight); + scissorRect.bottom = static_cast(menuBottom); + context->RSSetScissorRects(1, &scissorRect); + + // Set up blur parameters matching our Unrimp-based HLSL shader structure + struct BlurConstants { + float texelSize[4]; // x = 1/width, y = 1/height, z = blur strength, w = unused + int blurParams[4]; // x = samples, y = unused, z = unused, w = unused + } constants; + + // Calculate blur parameters based on intensity slider + float blurRadius = currentBlurIntensity * 5.0f; // Scale blur radius by intensity + int sampleCount = std::max(3, std::min(15, static_cast(7 + currentBlurIntensity * 8))); // 3-15 samples based on intensity + + constants.texelSize[0] = blurRadius / static_cast(blurTextureWidth); + constants.texelSize[1] = blurRadius / static_cast(blurTextureHeight); + constants.texelSize[2] = currentBlurIntensity; // Blur strength multiplier + constants.texelSize[3] = 0.0f; // Unused + + constants.blurParams[0] = sampleCount; // Dynamic sample count based on intensity + constants.blurParams[1] = 0; // Unused + constants.blurParams[2] = 0; // Unused + constants.blurParams[3] = 0; // Unused + + context->UpdateSubresource(blurConstantBuffer, 0, nullptr, &constants, 0, 0); + + // Set up viewport for blur textures + D3D11_VIEWPORT blurViewport = {}; + blurViewport.Width = static_cast(blurTextureWidth); + blurViewport.Height = static_cast(blurTextureHeight); + blurViewport.MinDepth = 0.0f; + blurViewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &blurViewport); + + // Set shaders and states + context->VSSetShader(blurVertexShader, nullptr, 0); + context->PSSetConstantBuffers(0, 1, &blurConstantBuffer); + context->PSSetSamplers(0, 1, &blurSamplerState); + + // First pass: Horizontal blur (source -> blur texture 1) + context->OMSetRenderTargets(1, &blurRTV1, nullptr); + context->PSSetShader(blurHorizontalPixelShader, nullptr, 0); + context->PSSetShaderResources(0, 1, &sourceSRV); + context->Draw(3, 0); // Draw fullscreen triangle + + // Second pass: Vertical blur (blur texture 1 -> blur texture 2) + context->OMSetRenderTargets(1, &blurRTV2, nullptr); + context->PSSetShader(blurVerticalPixelShader, nullptr, 0); + ID3D11ShaderResourceView* nullSRV = nullptr; + context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV + context->PSSetShaderResources(0, 1, &blurSRV1); + context->Draw(3, 0); + + // Final composition: Blend blurred result back to main render target (only in menu area) + context->RSSetViewports(1, &originalViewport); + context->OMSetRenderTargets(1, &targetRTV, nullptr); + + // Enable scissor test to limit blur to menu area + ID3D11RasterizerState* originalRS = nullptr; + context->RSGetState(&originalRS); + + // Create rasterizer state with scissor test enabled for final composition + ID3D11RasterizerState* scissorRS = nullptr; + D3D11_RASTERIZER_DESC rsDesc = {}; + if (originalRS) { + originalRS->GetDesc(&rsDesc); + } else { + rsDesc.FillMode = D3D11_FILL_SOLID; + rsDesc.CullMode = D3D11_CULL_BACK; + rsDesc.FrontCounterClockwise = FALSE; + rsDesc.DepthBias = 0; + rsDesc.DepthBiasClamp = 0.0f; + rsDesc.SlopeScaledDepthBias = 0.0f; + rsDesc.DepthClipEnable = TRUE; + rsDesc.MultisampleEnable = FALSE; + rsDesc.AntialiasedLineEnable = FALSE; + } + rsDesc.ScissorEnable = TRUE; + + device->CreateRasterizerState(&rsDesc, &scissorRS); + if (scissorRS) { + context->RSSetState(scissorRS); + } + + // Set blend state for proper compositing + float blendFactor[4] = { 1.0f, 1.0f, 1.0f, currentBlurIntensity * 0.8f }; + context->OMSetBlendState(blurBlendState, blendFactor, 0xFFFFFFFF); + + context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV + context->PSSetShaderResources(0, 1, &blurSRV2); + context->Draw(3, 0); + + // Restore original state + context->OMSetRenderTargets(1, &originalRTV, originalDSV); + context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); + context->PSSetShaderResources(0, 1, &nullSRV); + context->RSSetState(originalRS); + context->RSSetScissorRects(0, nullptr); // Disable scissor test + + // Clean up + if (sourceSRV) sourceSRV->Release(); + if (originalRTV) originalRTV->Release(); + if (originalDSV) originalDSV->Release(); + if (originalRS) originalRS->Release(); + if (scissorRS) scissorRS->Release(); +} + +void ThemeManager::CleanupBlurResources() +{ + if (blurVertexShader) blurVertexShader->Release(); blurVertexShader = nullptr; + if (blurHorizontalPixelShader) blurHorizontalPixelShader->Release(); blurHorizontalPixelShader = nullptr; + if (blurVerticalPixelShader) blurVerticalPixelShader->Release(); blurVerticalPixelShader = nullptr; + if (blurConstantBuffer) blurConstantBuffer->Release(); blurConstantBuffer = nullptr; + if (blurSamplerState) blurSamplerState->Release(); blurSamplerState = nullptr; + if (blurBlendState) blurBlendState->Release(); blurBlendState = nullptr; + + if (blurTexture1) blurTexture1->Release(); blurTexture1 = nullptr; + if (blurTexture2) blurTexture2->Release(); blurTexture2 = nullptr; + if (blurRTV1) blurRTV1->Release(); blurRTV1 = nullptr; + if (blurRTV2) blurRTV2->Release(); blurRTV2 = nullptr; + if (blurSRV1) blurSRV1->Release(); blurSRV1 = nullptr; + if (blurSRV2) blurSRV2->Release(); blurSRV2 = nullptr; + + blurTextureWidth = 0; + blurTextureHeight = 0; + isBlurEnabled = false; + currentBlurIntensity = 0.0f; } \ No newline at end of file diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 6d549db859..52161d6133 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -43,6 +44,15 @@ class ThemeManager // Static UI helper methods static void SetupImGuiStyle(const class Menu& menu); static void ReloadFont(const class Menu& menu, float& cachedFontSize); + static void ApplyBackgroundBlur(float blurIntensity, ImVec4* colors); + static void RenderBackgroundBlur(); // Real-time shader-based blur rendering + static void ForceApplyDefaultTheme(); // Force Default.json colors to ImGui (bypass hardcoded defaults) + + // Blur system methods + static bool InitializeBlurShaders(); + static void CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format); + static void PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax); + static void CleanupBlurResources(); struct Constants { @@ -78,7 +88,7 @@ class ThemeManager static constexpr float BUTTON_SPACING = 8.0f; static constexpr float OVERLAY_WINDOW_POSITION = 10.0f; static constexpr float FONT_CACHE_EPSILON = 0.01f; - static constexpr float CURSOR_POSITION_PADDING = 5.0f; + static constexpr float CURSOR_POSITION_PADDING = 14.0f; static constexpr float SEPARATOR_THICKNESS = 3.0f; static constexpr float UNDOCKED_ICON_ITEM_SPACING = 6.0f; }; @@ -156,6 +166,29 @@ class ThemeManager void CreateDefaultThemeFiles(); private: + // Blur system state + static inline float currentBlurIntensity = 0.0f; + static inline bool isBlurEnabled = false; + + // DirectX blur resources + static inline ID3D11VertexShader* blurVertexShader = nullptr; + static inline ID3D11PixelShader* blurHorizontalPixelShader = nullptr; + static inline ID3D11PixelShader* blurVerticalPixelShader = nullptr; + static inline ID3D11Buffer* blurConstantBuffer = nullptr; + static inline ID3D11SamplerState* blurSamplerState = nullptr; + static inline ID3D11BlendState* blurBlendState = nullptr; + + // Intermediate blur textures + static inline ID3D11Texture2D* blurTexture1 = nullptr; + static inline ID3D11Texture2D* blurTexture2 = nullptr; + static inline ID3D11RenderTargetView* blurRTV1 = nullptr; + static inline ID3D11RenderTargetView* blurRTV2 = nullptr; + static inline ID3D11ShaderResourceView* blurSRV1 = nullptr; + static inline ID3D11ShaderResourceView* blurSRV2 = nullptr; + + static inline UINT blurTextureWidth = 0; + static inline UINT blurTextureHeight = 0; + ThemeManager() = default; ~ThemeManager() = default; ThemeManager(const ThemeManager&) = delete; diff --git a/src/State.cpp b/src/State.cpp index 31a769f068..d628e23c43 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -830,6 +830,12 @@ float State::GetTotalSmoothedDrawCalls() const void State::LoadTheme() { + // Don't override if a theme preset is already selected (e.g., first-time Default Dark setup) + if (!globals::menu->GetSettings().SelectedThemePreset.empty()) { + logger::info("Theme preset '{}' already selected, skipping SettingsTheme.json load", globals::menu->GetSettings().SelectedThemePreset); + return; + } + auto themeConfigPath = Util::PathHelpers::GetSettingsThemePath(); if (!std::filesystem::exists(themeConfigPath)) { diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 8ec6dd4c10..6540ed4658 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -176,11 +176,12 @@ namespace Util logger::info("InitializeMenuIcons: Loading icons from base path: {}", basePath); // Initialize all texture pointers to nullptr for safe cleanup - std::array texturePointers = { + std::array texturePointers = { &menu->uiIcons.saveSettings.texture, &menu->uiIcons.loadSettings.texture, &menu->uiIcons.clearCache.texture, &menu->uiIcons.logo.texture, + &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.discord.texture, &menu->uiIcons.characters.texture, &menu->uiIcons.display.texture, @@ -228,6 +229,7 @@ namespace Util loadIconWithLogging(basePath + "Action Icons\\load-settings.png", &menu->uiIcons.loadSettings.texture, menu->uiIcons.loadSettings.size, "load-settings"); loadIconWithLogging(basePath + "Action Icons\\clear-cache.png", &menu->uiIcons.clearCache.texture, menu->uiIcons.clearCache.size, "clear-cache"); loadIconWithLogging(basePath + "Community Shaders Logo\\cs-logo.png", &menu->uiIcons.logo.texture, menu->uiIcons.logo.size, "logo"); + loadIconWithLogging(basePath + "Action Icons\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, menu->uiIcons.featureSettingRevert.size, "restore-settings"); loadIconWithLogging(basePath + "Action Icons\\discord.png", &menu->uiIcons.discord.texture, menu->uiIcons.discord.size, "discord"); // Load category icons in a more compact way @@ -256,7 +258,7 @@ namespace Util loadIcon(path, icon.texture, icon.size); } - logger::info("InitializeMenuIcons: Loaded {}/15 icons successfully", iconsLoaded); + logger::info("InitializeMenuIcons: Loaded {}/16 icons successfully", iconsLoaded); return anyIconLoaded; } @@ -311,6 +313,9 @@ namespace Util // Render logo ImGui::Image(logoTexture, logoSize); ImGui::SameLine(); + + // Add consistent spacing between logo and text + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f); // Reset cursor for text with proper vertical alignment ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX(), startPos.y)); @@ -1384,6 +1389,79 @@ namespace Util return clicked; } + bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size) + { + if (!enabled) return false; + + // Calculate appropriate size if not specified - make it smaller + ImVec2 toggleSize = size; + if (toggleSize.x <= 0) { + toggleSize.x = ImGui::GetFrameHeight() * 1.6f; // Smaller 1.6:1 aspect ratio + } + if (toggleSize.y <= 0) { + toggleSize.y = ImGui::GetFrameHeight() * 0.8f; // Smaller height + } + + // Get theme colors for better integration + auto& style = ImGui::GetStyle(); + auto& colors = style.Colors; + + // Use theme header colors instead of bright green/red + ImVec4 toggleBg = *enabled ? + colors[ImGuiCol_Header] : // Use header color when enabled + colors[ImGuiCol_FrameBg]; // Use frame background when disabled + + ImVec4 toggleBgHovered = *enabled ? + colors[ImGuiCol_HeaderHovered] : // Use header hovered when enabled + colors[ImGuiCol_FrameBgHovered]; // Use frame hovered when disabled + + ImVec4 toggleBgActive = *enabled ? + colors[ImGuiCol_HeaderActive] : // Use header active when enabled + colors[ImGuiCol_FrameBgActive]; // Use frame active when disabled + + // Apply toggle styling with border + ImGui::PushStyleColor(ImGuiCol_Button, toggleBg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, toggleBgHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, toggleBgActive); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toggleSize.y * 0.5f); // Round ends + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); // Larger border + + // Create unique ID for the toggle + ImGui::PushID(label); + + // Draw the toggle button + bool clicked = ImGui::Button("", toggleSize); + + // Draw the toggle knob + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 buttonMin = ImGui::GetItemRectMin(); + ImVec2 buttonMax = ImGui::GetItemRectMax(); + + // Calculate knob position and size + float knobRadius = (toggleSize.y - 4.0f) * 0.5f; + float knobPadding = 2.0f; + float knobTravel = toggleSize.x - (knobRadius * 2.0f) - (knobPadding * 2.0f); + float knobX = *enabled ? + buttonMin.x + knobPadding + knobRadius + knobTravel : + buttonMin.x + knobPadding + knobRadius; + float knobY = buttonMin.y + toggleSize.y * 0.5f; + + // Draw knob + ImU32 knobColor = ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + drawList->AddCircleFilled(ImVec2(knobX, knobY), knobRadius, knobColor); + + ImGui::PopID(); + ImGui::PopStyleVar(2); // Pop both FrameRounding and FrameBorderSize + ImGui::PopStyleColor(3); + + // Handle toggle action + if (clicked) { + *enabled = !*enabled; + } + + return clicked; + } + std::vector DiscoverFonts() { std::vector fonts; diff --git a/src/Utils/UI.h b/src/Utils/UI.h index d6f1a1e964..711a049bff 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -118,6 +118,15 @@ namespace Util */ bool ButtonWithFlash(const char* label, const ImVec2& size = ImVec2(0, 0), int flashDurationMs = 200); + /** + * Clean, minimalist toggle switch for feature enable/disable state + * @param label Label text to display next to the toggle + * @param enabled Reference to the boolean state to toggle + * @param size Toggle size (optional, defaults to automatic sizing) + * @return True if the toggle state was changed + */ + bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size = ImVec2(0, 0)); + /** * Discovers available font files in the Fonts directory * @return Vector of font file names (including .ttf extension) From e5a267287070a6940393c4ef26b6295985d9374f Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 22:22:14 +1000 Subject: [PATCH 18/29] Update Default.json --- package/SKSE/Plugins/CommunityShaders/Themes/Default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 4c48d07e59..24aaf908c2 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -9,7 +9,7 @@ "TooltipHoverDelay": 0.5, "BackgroundBlur": 0.5, "Palette": { - "Background": [0.1, 0.1, 0.1, 1.0], + "Background": [0.1, 0.1, 0.1, 0.39216], "Text": [1.0, 1.0, 1.0, 1.0], "Border": [0.5, 0.5, 0.5, 0.8] }, From e6dba05ab5a2548d488a10b481bca8aea48da1f9 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 22:25:19 +1000 Subject: [PATCH 19/29] background blur added to themes --- package/SKSE/Plugins/CommunityShaders/Themes/Amber.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/Default.json | 2 +- package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/Forest.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/Light.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json | 1 + package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json | 1 + src/Menu.h | 2 +- 11 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json index 75bd0a4039..1c410991a3 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.2, 0.15, 0.05, 0.9], "Text": [1.0, 0.9, 0.7, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 24aaf908c2..29394e4c22 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -9,7 +9,7 @@ "TooltipHoverDelay": 0.5, "BackgroundBlur": 0.5, "Palette": { - "Background": [0.1, 0.1, 0.1, 0.39216], + "Background": [0.03, 0.03, 0.03, 0.39216], "Text": [1.0, 1.0, 1.0, 1.0], "Border": [0.5, 0.5, 0.5, 0.8] }, diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 53886801d5..f738e51535 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index 2657e7ed42..9b4d57ecb0 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.15, 0.12, 0.08, 0.9], "Text": [0.9, 0.75, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json index 0f5cf1ce8e..94295d94ec 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.1, 0.3, 0.15, 0.9], "Text": [0.9, 1.0, 0.9, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index 30780fdfc9..2b498f2c31 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.0, 0.0, 0.0, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 4f2a73a8ab..404a867913 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.9, 0.9, 0.9, 0.95], "Text": [0.1, 0.1, 0.1, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json index 1841cc1c07..4465010254 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.2, 0.1, 0.3, 0.9], "Text": [0.95, 0.9, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index ef794a4e39..16dc51d247 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json index acb79f726b..2eec2f33c9 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -7,6 +7,7 @@ "UseSimplePalette": false, "ShowActionIcons": true, "TooltipHoverDelay": 0.5, + "BackgroundBlur": 0.5, "Palette": { "Background": [0.1, 0.2, 0.4, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], diff --git a/src/Menu.h b/src/Menu.h index eb3772062a..1804d24ad3 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -123,7 +123,7 @@ class Menu bool UseSimplePalette = false; // DEPRECATED: No longer affects behavior. UI now shows both Simple and Advanced controls. bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds - float BackgroundBlur = 0.0f; // background blur effect intensity + float BackgroundBlur = 0.5f; // background blur effect intensity // Scrollbar opacity settings struct ScrollbarOpacitySettings From 3c40396d4329ebbcf6bd855b6a7a843deb6297ff Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 22:39:54 +1000 Subject: [PATCH 20/29] Fixes, Credits --- .../Menu/Shaders/GaussianBlur_Horizontal.hlsl | 38 +++++++++++-------- .../Menu/Shaders/GaussianBlur_Vertical.hlsl | 38 +++++++++++-------- src/Menu/ThemeManager.h | 3 +- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl index 5c66f5616a..02f797f37c 100644 --- a/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl +++ b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl @@ -1,5 +1,7 @@ // Horizontal Gaussian Blur Shader // Based on Unrimp rendering engine's separable blur implementation +// Credits: Christian Ofenberg and the Unrimp project (https://github.com/cofenberg/unrimp) +// License: MIT License // Used for ImGui background blur effects // Uniforms @@ -33,15 +35,17 @@ VS_OUTPUT VS_Main(VS_INPUT input) return output; } -// Gaussian weight calculation based on Unrimp's implementation +// Improved Gaussian weight calculation based on Unrimp's implementation +// Uses proper 2D Gaussian distribution with better normalization float GaussianWeight(float offset) { - const float SIGMA = 0.5f; // Unrimp's SIGMA value + const float SIGMA = 0.5f; // Unrimp's SIGMA value for smooth blur const float v = 2.0f * SIGMA * SIGMA; - return exp(-(offset * offset) / v) / (3.14159265f * v); + return exp(-(offset * offset) / v) / (sqrt(2.0f * 3.14159265f) * SIGMA); } // Pixel Shader - Horizontal Gaussian Blur +// Improved implementation to eliminate scanline artifacts float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); @@ -51,25 +55,27 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET const int samples = min(BlurParams.x, 15); // Cap at 15 for performance const int halfSamples = samples / 2; - // Sample horizontally + // Improved horizontal sampling with sub-pixel offset for anti-aliasing for (int i = -halfSamples; i <= halfSamples; ++i) { - float2 sampleCoord = input.TexCoord + float2(i * TexelSize.x * TexelSize.z, 0.0f); - float weight = GaussianWeight(float(i)); + // Add slight sub-pixel jitter to reduce aliasing artifacts + float offset = float(i) + 0.5f * (float(i % 2) - 0.5f) * 0.1f; + float2 sampleCoord = input.TexCoord + float2(offset * TexelSize.x * TexelSize.z, 0.0f); + float weight = GaussianWeight(offset); - // Sample the texture with proper bounds checking - if (sampleCoord.x >= 0.0f && sampleCoord.x <= 1.0f) - { - result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; - totalWeight += weight; - } + // Use clamp addressing to avoid artifacts at borders + sampleCoord = clamp(sampleCoord, 0.0f, 1.0f); + + float4 sample = InputTexture.Sample(LinearSampler, sampleCoord); + result += sample * weight; + totalWeight += weight; } // Normalize by total weight to maintain brightness - if (totalWeight > 0.0f) - { - result /= totalWeight; - } + result /= totalWeight; + + // Apply slight gamma correction to reduce scanline perception + result.rgb = pow(abs(result.rgb), 0.95f) * sign(result.rgb); return result; } \ No newline at end of file diff --git a/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl index 29d05f7445..8c25deec20 100644 --- a/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl +++ b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl @@ -1,5 +1,7 @@ // Vertical Gaussian Blur Shader // Based on Unrimp rendering engine's separable blur implementation +// Credits: Christian Ofenberg and the Unrimp project (https://github.com/cofenberg/unrimp) +// License: MIT License // Used for ImGui background blur effects - Second pass (vertical) // Uniforms @@ -33,15 +35,17 @@ VS_OUTPUT VS_Main(VS_INPUT input) return output; } -// Gaussian weight calculation based on Unrimp's implementation +// Improved Gaussian weight calculation based on Unrimp's implementation +// Uses proper 2D Gaussian distribution with better normalization float GaussianWeight(float offset) { - const float SIGMA = 0.5f; // Unrimp's SIGMA value + const float SIGMA = 0.5f; // Unrimp's SIGMA value for smooth blur const float v = 2.0f * SIGMA * SIGMA; - return exp(-(offset * offset) / v) / (3.14159265f * v); + return exp(-(offset * offset) / v) / (sqrt(2.0f * 3.14159265f) * SIGMA); } // Pixel Shader - Vertical Gaussian Blur +// Improved implementation to eliminate scanline artifacts float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); @@ -51,25 +55,27 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET const int samples = min(BlurParams.x, 15); // Cap at 15 for performance const int halfSamples = samples / 2; - // Sample vertically + // Improved vertical sampling with sub-pixel offset for anti-aliasing for (int i = -halfSamples; i <= halfSamples; ++i) { - float2 sampleCoord = input.TexCoord + float2(0.0f, i * TexelSize.y * TexelSize.z); - float weight = GaussianWeight(float(i)); + // Add slight sub-pixel jitter to reduce aliasing artifacts + float offset = float(i) + 0.5f * (float(i % 2) - 0.5f) * 0.1f; + float2 sampleCoord = input.TexCoord + float2(0.0f, offset * TexelSize.y * TexelSize.z); + float weight = GaussianWeight(offset); - // Sample the texture with proper bounds checking - if (sampleCoord.y >= 0.0f && sampleCoord.y <= 1.0f) - { - result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; - totalWeight += weight; - } + // Use clamp addressing to avoid artifacts at borders + sampleCoord = clamp(sampleCoord, 0.0f, 1.0f); + + float4 sample = InputTexture.Sample(LinearSampler, sampleCoord); + result += sample * weight; + totalWeight += weight; } // Normalize by total weight to maintain brightness - if (totalWeight > 0.0f) - { - result /= totalWeight; - } + result /= totalWeight; + + // Apply slight gamma correction to reduce scanline perception + result.rgb = pow(abs(result.rgb), 0.95f) * sign(result.rgb); return result; } \ No newline at end of file diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 52161d6133..46c2a6c635 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -48,7 +48,8 @@ class ThemeManager static void RenderBackgroundBlur(); // Real-time shader-based blur rendering static void ForceApplyDefaultTheme(); // Force Default.json colors to ImGui (bypass hardcoded defaults) - // Blur system methods + // Blur system methods - inspired by Unrimp rendering engine + // Credits: Christian Ofenberg and the Unrimp project (https://github.com/cofenberg/unrimp) static bool InitializeBlurShaders(); static void CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT format); static void PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax); From ded010978972285042a4c9a00a79f66ad8018e24 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 4 Oct 2025 23:03:27 +1000 Subject: [PATCH 21/29] border settings --- .../Plugins/CommunityShaders/Themes/Amber.json | 5 ++++- .../Plugins/CommunityShaders/Themes/Default.json | 5 ++++- .../CommunityShaders/Themes/DragonBlood.json | 5 ++++- .../CommunityShaders/Themes/DwemerBronze.json | 5 ++++- .../Plugins/CommunityShaders/Themes/Forest.json | 5 ++++- .../CommunityShaders/Themes/HighContrast.json | 5 ++++- .../Plugins/CommunityShaders/Themes/Light.json | 5 ++++- .../Plugins/CommunityShaders/Themes/Mystic.json | 5 ++++- .../CommunityShaders/Themes/NordicFrost.json | 5 ++++- .../Plugins/CommunityShaders/Themes/Ocean.json | 5 ++++- src/FeatureIssues.cpp | 4 ++-- src/Menu.cpp | 5 ++++- src/Menu.h | 6 +++++- src/Menu/SettingsTabRenderer.cpp | 10 +++++++++- src/Menu/ThemeManager.cpp | 14 ++++++++++---- 15 files changed, 70 insertions(+), 19 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json index 1c410991a3..c428a5d659 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Amber.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.2, 0.15, 0.05, 0.9], "Text": [1.0, 0.9, 0.7, 1.0], - "Border": [0.8, 0.6, 0.3, 0.8] + "WindowBorder": [0.8, 0.5, 0.2, 0.9], + "FrameBorder": [0.7, 0.4, 0.15, 0.8], + "Separator": [0.8, 0.5, 0.2, 0.7], + "ResizeGrip": [0.9, 0.6, 0.25, 0.9] }, "StatusPalette": { "Disable": [0.5, 0.4, 0.3, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json index 29394e4c22..84f84b6771 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Default.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Default.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.03, 0.03, 0.03, 0.39216], "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [0.5, 0.5, 0.5, 0.8] + "WindowBorder": [0.5, 0.5, 0.5, 0.8], + "FrameBorder": [0.4, 0.4, 0.4, 0.7], + "Separator": [0.5, 0.5, 0.5, 0.6], + "ResizeGrip": [0.6, 0.6, 0.6, 0.8] }, "StatusPalette": { "Disable": [0.5, 0.5, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index f738e51535..0e4066035e 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], - "Border": [0.8, 0.3, 0.3, 0.8] + "WindowBorder": [0.8, 0.2, 0.2, 0.9], + "FrameBorder": [0.7, 0.15, 0.15, 0.8], + "Separator": [0.8, 0.2, 0.2, 0.7], + "ResizeGrip": [0.9, 0.25, 0.25, 0.9] }, "StatusPalette": { "Disable": [0.4, 0.2, 0.2, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json index 9b4d57ecb0..530d9a9b0d 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DwemerBronze.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.15, 0.12, 0.08, 0.9], "Text": [0.9, 0.75, 0.5, 1.0], - "Border": [0.7, 0.5, 0.3, 0.8] + "WindowBorder": [0.7, 0.5, 0.3, 0.9], + "FrameBorder": [0.6, 0.4, 0.25, 0.8], + "Separator": [0.7, 0.5, 0.3, 0.7], + "ResizeGrip": [0.8, 0.6, 0.35, 0.9] }, "StatusPalette": { "Disable": [0.4, 0.35, 0.25, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json index 94295d94ec..88c67ed156 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Forest.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.1, 0.3, 0.15, 0.9], "Text": [0.9, 1.0, 0.9, 1.0], - "Border": [0.4, 0.7, 0.4, 0.8] + "WindowBorder": [0.3, 0.6, 0.3, 0.85], + "FrameBorder": [0.25, 0.5, 0.25, 0.75], + "Separator": [0.3, 0.6, 0.3, 0.65], + "ResizeGrip": [0.35, 0.7, 0.35, 0.85] }, "StatusPalette": { "Disable": [0.3, 0.4, 0.3, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json index 2b498f2c31..fd88fffd67 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/HighContrast.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.0, 0.0, 0.0, 0.95], "Text": [1.0, 1.0, 1.0, 1.0], - "Border": [1.0, 1.0, 1.0, 0.9] + "WindowBorder": [1.0, 1.0, 1.0, 1.0], + "FrameBorder": [0.9, 0.9, 0.9, 0.9], + "Separator": [1.0, 1.0, 1.0, 0.8], + "ResizeGrip": [1.0, 1.0, 1.0, 1.0] }, "StatusPalette": { "Disable": [0.5, 0.5, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 404a867913..9edde10bd6 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.9, 0.9, 0.9, 0.95], "Text": [0.1, 0.1, 0.1, 1.0], - "Border": [0.3, 0.3, 0.3, 0.8] + "WindowBorder": [0.4, 0.4, 0.4, 0.8], + "FrameBorder": [0.3, 0.3, 0.3, 0.7], + "Separator": [0.4, 0.4, 0.4, 0.6], + "ResizeGrip": [0.5, 0.5, 0.5, 0.8] }, "StatusPalette": { "Disable": [0.6, 0.6, 0.6, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json index 4465010254..c970983bfc 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.2, 0.1, 0.3, 0.9], "Text": [0.95, 0.9, 1.0, 1.0], - "Border": [0.6, 0.4, 0.8, 0.8] + "WindowBorder": [0.6, 0.4, 0.8, 0.9], + "FrameBorder": [0.5, 0.3, 0.7, 0.8], + "Separator": [0.6, 0.4, 0.8, 0.7], + "ResizeGrip": [0.7, 0.5, 0.9, 0.9] }, "StatusPalette": { "Disable": [0.4, 0.3, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index 16dc51d247..615d16a440 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], - "Border": [0.6, 0.8, 1.0, 0.8] + "WindowBorder": [0.7, 0.85, 0.9, 0.8], + "FrameBorder": [0.6, 0.75, 0.8, 0.7], + "Separator": [0.7, 0.85, 0.9, 0.6], + "ResizeGrip": [0.8, 0.9, 0.95, 0.8] }, "StatusPalette": { "Disable": [0.4, 0.45, 0.5, 1.0], diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json index 2eec2f33c9..0fe01da234 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Ocean.json @@ -11,7 +11,10 @@ "Palette": { "Background": [0.1, 0.2, 0.4, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], - "Border": [0.3, 0.5, 0.8, 0.8] + "WindowBorder": [0.2, 0.6, 0.8, 0.85], + "FrameBorder": [0.15, 0.5, 0.7, 0.75], + "Separator": [0.2, 0.6, 0.8, 0.65], + "ResizeGrip": [0.25, 0.7, 0.9, 0.85] }, "StatusPalette": { "Disable": [0.4, 0.45, 0.5, 1.0], diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index 45c7de67e4..295dea82f5 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1459,7 +1459,7 @@ namespace FeatureIssues { auto disableGuard = Util::DisableGuard(hasActiveTests); auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.Border, + themeSettings.Palette.FrameBorder, themeSettings.StatusPalette.RestartNeeded, themeSettings.StatusPalette.CurrentHotkey); @@ -1482,7 +1482,7 @@ namespace FeatureIssues { auto disableGuard = Util::DisableGuard(!hasActiveTests); auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.Border, + themeSettings.Palette.FrameBorder, themeSettings.StatusPalette.Error, themeSettings.StatusPalette.CurrentHotkey); diff --git a/src/Menu.cpp b/src/Menu.cpp index 969d9ac058..05d23a8cdd 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -45,7 +45,10 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, Background, Text, - Border) + WindowBorder, + FrameBorder, + Separator, + ResizeGrip) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::StatusPaletteColors, diff --git a/src/Menu.h b/src/Menu.h index 1804d24ad3..9d2a691223 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -137,7 +137,11 @@ class Menu { ImVec4 Background{ 0.10f, 0.10f, 0.10f, 0.80f }; ImVec4 Text{ 1.0f, 1.0f, 1.0f, 1.0f }; - ImVec4 Border{ 0.5f, 0.5f, 0.5f, 0.8f }; + // Separated border controls for better theming granularity + ImVec4 WindowBorder{ 0.5f, 0.5f, 0.5f, 0.8f }; // Outer window borders + ImVec4 FrameBorder{ 0.4f, 0.4f, 0.4f, 0.7f }; // Button, slider, input field borders + ImVec4 Separator{ 0.5f, 0.5f, 0.5f, 0.6f }; // Internal separators and dividers + ImVec4 ResizeGrip{ 0.6f, 0.6f, 0.6f, 0.8f }; // Window resize grips } Palette; struct StatusPaletteColors { diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 49653dd338..afadc4805b 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -614,11 +614,19 @@ void SettingsTabRenderer::RenderColorsTab() if (ImGui::CollapsingHeader("Simple", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::ColorEdit4("Background", (float*)&themeSettings.Palette.Background); ImGui::ColorEdit4("Text", (float*)&themeSettings.Palette.Text); - ImGui::ColorEdit4("Border", (float*)&themeSettings.Palette.Border); } // Advanced Colors Section - collapsed by default to avoid overwhelming users if (ImGui::CollapsingHeader("Advanced")) { + // Separated border controls for granular theming + if (ImGui::TreeNode("Border Controls")) { + ImGui::ColorEdit4("Window Border", (float*)&themeSettings.Palette.WindowBorder); + ImGui::ColorEdit4("Frame Border", (float*)&themeSettings.Palette.FrameBorder); + ImGui::ColorEdit4("Separator", (float*)&themeSettings.Palette.Separator); + ImGui::ColorEdit4("Resize Grip", (float*)&themeSettings.Palette.ResizeGrip); + ImGui::TreePop(); + } + ImGui::Separator(); ImGui::TextWrapped("Advanced color controls for detailed customization of all UI elements."); static ImGuiTextFilter filter; diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 3259c08254..7866b2bc63 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -90,16 +90,22 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // This allows the simple palette controls to work by updating the FullPalette colors[ImGuiCol_WindowBg] = themeSettings.Palette.Background; colors[ImGuiCol_Text] = themeSettings.Palette.Text; - colors[ImGuiCol_Border] = themeSettings.Palette.Border; - colors[ImGuiCol_Separator] = themeSettings.Palette.Border; - colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.Border; + colors[ImGuiCol_Border] = themeSettings.Palette.WindowBorder; + colors[ImGuiCol_Separator] = themeSettings.Palette.Separator; + colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.ResizeGrip; + + // Apply frame border to UI elements with frames/borders + colors[ImGuiCol_FrameBg] = themeSettings.Palette.FrameBorder; + colors[ImGuiCol_CheckMark] = themeSettings.Palette.Text; + colors[ImGuiCol_SliderGrab] = themeSettings.Palette.FrameBorder; + colors[ImGuiCol_SliderGrabActive] = themeSettings.Palette.FrameBorder; // Apply derived colors based on simple palette ImVec4 textDisabled = themeSettings.Palette.Text; textDisabled.w = 0.3f; colors[ImGuiCol_TextDisabled] = textDisabled; - ImVec4 resizeGripHovered = themeSettings.Palette.Border; + ImVec4 resizeGripHovered = themeSettings.Palette.ResizeGrip; resizeGripHovered.w = 0.1f; colors[ImGuiCol_ResizeGripHovered] = resizeGripHovered; colors[ImGuiCol_ResizeGripActive] = resizeGripHovered; From 3117a63126e25b12c058e50d60dc0610352cf53d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:04:11 +0000 Subject: [PATCH 22/29] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../CommunityShaders/Themes/DragonBlood.json | 2 +- .../CommunityShaders/Themes/Light.json | 2 +- .../CommunityShaders/Themes/NordicFrost.json | 2 +- .../Menu/Shaders/GaussianBlur_Horizontal.hlsl | 14 +- .../Menu/Shaders/GaussianBlur_Vertical.hlsl | 14 +- src/Menu.cpp | 8 +- src/Menu.h | 24 +- src/Menu/FeatureListRenderer.cpp | 33 +-- src/Menu/OverlayRenderer.cpp | 4 +- src/Menu/ThemeManager.cpp | 267 ++++++++++-------- src/Menu/ThemeManager.h | 10 +- src/Utils/UI.cpp | 51 ++-- 12 files changed, 239 insertions(+), 192 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json index 0e4066035e..d8ea6a01dd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/DragonBlood.json @@ -11,7 +11,7 @@ "Palette": { "Background": [0.25, 0.05, 0.05, 0.9], "Text": [1.0, 0.85, 0.85, 1.0], - "WindowBorder": [0.8, 0.2, 0.2, 0.9], + "WindowBorder": [0.8, 0.2, 0.2, 0.9], "FrameBorder": [0.7, 0.15, 0.15, 0.8], "Separator": [0.8, 0.2, 0.2, 0.7], "ResizeGrip": [0.9, 0.25, 0.25, 0.9] diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json index 9edde10bd6..7970abef4a 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/Light.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/Light.json @@ -11,7 +11,7 @@ "Palette": { "Background": [0.9, 0.9, 0.9, 0.95], "Text": [0.1, 0.1, 0.1, 1.0], - "WindowBorder": [0.4, 0.4, 0.4, 0.8], + "WindowBorder": [0.4, 0.4, 0.4, 0.8], "FrameBorder": [0.3, 0.3, 0.3, 0.7], "Separator": [0.4, 0.4, 0.4, 0.6], "ResizeGrip": [0.5, 0.5, 0.5, 0.8] diff --git a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json index 615d16a440..4a7e026771 100644 --- a/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json +++ b/package/SKSE/Plugins/CommunityShaders/Themes/NordicFrost.json @@ -11,7 +11,7 @@ "Palette": { "Background": [0.05, 0.15, 0.25, 0.9], "Text": [0.9, 0.95, 1.0, 1.0], - "WindowBorder": [0.7, 0.85, 0.9, 0.8], + "WindowBorder": [0.7, 0.85, 0.9, 0.8], "FrameBorder": [0.6, 0.75, 0.8, 0.7], "Separator": [0.7, 0.85, 0.9, 0.6], "ResizeGrip": [0.8, 0.9, 0.95, 0.8] diff --git a/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl index 02f797f37c..f9da968386 100644 --- a/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl +++ b/src/Features/Menu/Shaders/GaussianBlur_Horizontal.hlsl @@ -50,11 +50,11 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); float totalWeight = 0.0f; - + // Use configurable blur samples (default 7 like Unrimp's SHADOW_MAP_FILTER_SIZE) const int samples = min(BlurParams.x, 15); // Cap at 15 for performance const int halfSamples = samples / 2; - + // Improved horizontal sampling with sub-pixel offset for anti-aliasing for (int i = -halfSamples; i <= halfSamples; ++i) { @@ -62,20 +62,20 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET float offset = float(i) + 0.5f * (float(i % 2) - 0.5f) * 0.1f; float2 sampleCoord = input.TexCoord + float2(offset * TexelSize.x * TexelSize.z, 0.0f); float weight = GaussianWeight(offset); - + // Use clamp addressing to avoid artifacts at borders sampleCoord = clamp(sampleCoord, 0.0f, 1.0f); - + float4 sample = InputTexture.Sample(LinearSampler, sampleCoord); result += sample * weight; totalWeight += weight; } - + // Normalize by total weight to maintain brightness result /= totalWeight; - + // Apply slight gamma correction to reduce scanline perception result.rgb = pow(abs(result.rgb), 0.95f) * sign(result.rgb); - + return result; } \ No newline at end of file diff --git a/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl index 8c25deec20..091a3addda 100644 --- a/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl +++ b/src/Features/Menu/Shaders/GaussianBlur_Vertical.hlsl @@ -50,11 +50,11 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); float totalWeight = 0.0f; - + // Use configurable blur samples (default 7 like Unrimp's SHADOW_MAP_FILTER_SIZE) const int samples = min(BlurParams.x, 15); // Cap at 15 for performance const int halfSamples = samples / 2; - + // Improved vertical sampling with sub-pixel offset for anti-aliasing for (int i = -halfSamples; i <= halfSamples; ++i) { @@ -62,20 +62,20 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET float offset = float(i) + 0.5f * (float(i % 2) - 0.5f) * 0.1f; float2 sampleCoord = input.TexCoord + float2(0.0f, offset * TexelSize.y * TexelSize.z); float weight = GaussianWeight(offset); - + // Use clamp addressing to avoid artifacts at borders sampleCoord = clamp(sampleCoord, 0.0f, 1.0f); - + float4 sample = InputTexture.Sample(LinearSampler, sampleCoord); result += sample * weight; totalWeight += weight; } - + // Normalize by total weight to maintain brightness result /= totalWeight; - + // Apply slight gamma correction to reduce scanline perception result.rgb = pow(abs(result.rgb), 0.95f) * sign(result.rgb); - + return result; } \ No newline at end of file diff --git a/src/Menu.cpp b/src/Menu.cpp index 05d23a8cdd..041b5e3a43 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -172,12 +172,12 @@ Menu::~Menu() void Menu::Load(json& o_json) { settings = o_json; - + // Apply Default Dark theme on first launch if no theme is selected if (!settings.FirstTimeSetupCompleted && settings.SelectedThemePreset.empty()) { // Ensure default themes are created/available CreateDefaultThemes(); - + // Load the Default Dark theme and mark it as selected to prevent override if (LoadThemePreset("Default")) { settings.SelectedThemePreset = "Default"; // Mark as selected to prevent State::LoadTheme override @@ -269,7 +269,7 @@ void Menu::Init() // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); - + // IMPORTANT: Immediately override ImGui's default styles with our Default.json theme // This prevents hardcoded ImGui defaults from ever showing through auto* themeManager = ThemeManager::GetSingleton(); @@ -285,7 +285,7 @@ void Menu::Init() // Last resort: Apply Default.json colors directly to ImGui ThemeManager::ForceApplyDefaultTheme(); } - + auto& imgui_io = ImGui::GetIO(); imgui_io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable; imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_HasGamepad; diff --git a/src/Menu.h b/src/Menu.h index 9d2a691223..21c8380f2d 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -94,9 +94,9 @@ class Menu UIIcon saveSettings; UIIcon loadSettings; UIIcon clearCache; - UIIcon logo; // New logo icon - UIIcon search; // Search icon for search bars - UIIcon featureSettingRevert; // Feature revert settings icon + UIIcon logo; // New logo icon + UIIcon search; // Search icon for search bars + UIIcon featureSettingRevert; // Feature revert settings icon // Social media/external link icons UIIcon discord; @@ -128,20 +128,20 @@ class Menu // Scrollbar opacity settings struct ScrollbarOpacitySettings { - float Background = 0.0f; // Background of the scrollbar area - float Thumb = 0.5f; // The draggable thumb/grip - float ThumbHovered = 0.75f; // Thumb when hovered - float ThumbActive = 0.9f; // Thumb when being dragged + float Background = 0.0f; // Background of the scrollbar area + float Thumb = 0.5f; // The draggable thumb/grip + float ThumbHovered = 0.75f; // Thumb when hovered + float ThumbActive = 0.9f; // Thumb when being dragged } ScrollbarOpacity; struct PaletteColors { ImVec4 Background{ 0.10f, 0.10f, 0.10f, 0.80f }; ImVec4 Text{ 1.0f, 1.0f, 1.0f, 1.0f }; // Separated border controls for better theming granularity - ImVec4 WindowBorder{ 0.5f, 0.5f, 0.5f, 0.8f }; // Outer window borders - ImVec4 FrameBorder{ 0.4f, 0.4f, 0.4f, 0.7f }; // Button, slider, input field borders - ImVec4 Separator{ 0.5f, 0.5f, 0.5f, 0.6f }; // Internal separators and dividers - ImVec4 ResizeGrip{ 0.6f, 0.6f, 0.6f, 0.8f }; // Window resize grips + ImVec4 WindowBorder{ 0.5f, 0.5f, 0.5f, 0.8f }; // Outer window borders + ImVec4 FrameBorder{ 0.4f, 0.4f, 0.4f, 0.7f }; // Button, slider, input field borders + ImVec4 Separator{ 0.5f, 0.5f, 0.5f, 0.6f }; // Internal separators and dividers + ImVec4 ResizeGrip{ 0.6f, 0.6f, 0.6f, 0.8f }; // Window resize grips } Palette; struct StatusPaletteColors { @@ -157,7 +157,7 @@ class Menu { ImVec4 ColorDefault{ 0.8f, 0.8f, 0.8f, 1.0f }; ImVec4 ColorHovered{ 0.6f, 0.6f, 0.6f, 1.0f }; - float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized + float MinimizedFactor = 0.7f; // 70% of original alpha for when the header is minimized } FeatureHeading; ImGuiStyle Style = []() { diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 3c5f16fc13..b71684a9ce 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -437,19 +437,20 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettingsTab(Feature* fea // Restore Defaults icon at bottom right (when feature is not disabled and is loaded) if (!isDisabled && isLoaded) { - // Position at bottom right of the child window - ImVec2 childSize = ImGui::GetWindowSize(); - // Scale icon with font size like other UI elements - float iconDimension = ImGui::GetFrameHeight() * 1.2f; // Larger for better visibility - ImVec2 iconSize = ImVec2(iconDimension, iconDimension); - ImGui::SetCursorPos(ImVec2(childSize.x - iconSize.x - 10.0f, childSize.y - iconSize.y - 10.0f)); auto& theme = globals::menu->GetTheme().Palette; + // Position at bottom right of the child window + ImVec2 childSize = ImGui::GetWindowSize(); + // Scale icon with font size like other UI elements + float iconDimension = ImGui::GetFrameHeight() * 1.2f; // Larger for better visibility + ImVec2 iconSize = ImVec2(iconDimension, iconDimension); + ImGui::SetCursorPos(ImVec2(childSize.x - iconSize.x - 10.0f, childSize.y - iconSize.y - 10.0f)); + auto& theme = globals::menu->GetTheme().Palette; ImVec4 iconColor = theme.Text; iconColor.w *= 0.7f; // Reduce alpha for subtler appearance - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // Transparent background - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.3f)); // Subtle hover - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.5f)); // Subtle active - + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); // Transparent background + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.3f)); // Subtle hover + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(iconColor.x, iconColor.y, iconColor.z, 0.5f)); // Subtle active + // Check if icon is available, fallback to text if not auto& menu = *globals::menu; if (menu.uiIcons.featureSettingRevert.texture) { @@ -462,9 +463,9 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettingsTab(Feature* fea feat->RestoreDefaultSettings(); } } - + ImGui::PopStyleColor(3); - + if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Restore default settings for this feature"); } @@ -581,17 +582,17 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureActionButtons(Feature* f // Enable/Disable at boot toggle bool bootEnabled = !isDisabled; - + // Apply disabled styling if feature has failed to load if (!feat->failedLoadedMessage.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); } - + if (Util::FeatureToggle("##BootToggle", &bootEnabled)) { bool newState = feat->ToggleAtBootSetting(); logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); } - + if (!feat->failedLoadedMessage.empty()) { ImGui::PopStyleColor(); } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 8090a3d5d4..d34f72b2cf 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -196,11 +196,11 @@ void OverlayRenderer::HandleABTesting() void OverlayRenderer::FinalizeImGuiFrame() { ImGui::Render(); - + // Apply background blur behind ImGui windows before rendering them // This ensures blur is only applied to areas behind visible windows ThemeManager::RenderBackgroundBlur(); - + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); if (globals::features::vr.IsOpenVRCompatible()) { diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 7866b2bc63..c02fd9c1a9 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -49,12 +49,12 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) // Theme based on https://github.com/powerof3/DialogueHistory auto& themeSettings = menu.GetTheme(); - + // Safety check: If theme appears corrupted/empty, force reload Default.json // This prevents fallback to ImGui's hardcoded defaults - bool isThemeCorrupted = (themeSettings.FullPalette.size() < ImGuiCol_COUNT / 2) || - (themeSettings.Palette.Background.w == 0.0f && themeSettings.Palette.Text.w == 0.0f); - + bool isThemeCorrupted = (themeSettings.FullPalette.size() < ImGuiCol_COUNT / 2) || + (themeSettings.Palette.Background.w == 0.0f && themeSettings.Palette.Text.w == 0.0f); + if (isThemeCorrupted) { logger::warn("Theme appears corrupted, attempting to reload Default.json to prevent ImGui defaults"); auto* nonConstMenu = const_cast(&menu); @@ -93,7 +93,7 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) colors[ImGuiCol_Border] = themeSettings.Palette.WindowBorder; colors[ImGuiCol_Separator] = themeSettings.Palette.Separator; colors[ImGuiCol_ResizeGrip] = themeSettings.Palette.ResizeGrip; - + // Apply frame border to UI elements with frames/borders colors[ImGuiCol_FrameBg] = themeSettings.Palette.FrameBorder; colors[ImGuiCol_CheckMark] = themeSettings.Palette.Text; @@ -185,7 +185,7 @@ void ThemeManager::ApplyBackgroundBlur(float blurIntensity, ImVec4* colors) // NOTE: Window transparency is now controlled by the background alpha setting // The blur intensity only affects the backdrop effect strength, not window alpha - + // Optional: Enhance text contrast very slightly for better readability over backdrops ImVec4& text = colors[ImGuiCol_Text]; float contrastBoost = 1.0f + (blurIntensity * 0.05f); // Reduced from 0.15f @@ -198,7 +198,7 @@ void ThemeManager::RenderBackgroundBlur() { // This function should be called after ImGui::Render() but before presenting // It renders blur behind visible ImGui windows only - + if (!isBlurEnabled || currentBlurIntensity <= 0.0f) { return; } @@ -231,7 +231,7 @@ void ThemeManager::RenderBackgroundBlur() // Get current render target ID3D11RenderTargetView* currentRTV = nullptr; context->OMGetRenderTargets(1, ¤tRTV, nullptr); - + if (!currentRTV) { return; } @@ -239,22 +239,24 @@ void ThemeManager::RenderBackgroundBlur() // Get render target texture and its dimensions ID3D11Resource* currentRT = nullptr; currentRTV->GetResource(¤tRT); - + ID3D11Texture2D* currentTexture = nullptr; HRESULT hr = currentRT->QueryInterface(__uuidof(ID3D11Texture2D), (void**)¤tTexture); - + if (FAILED(hr) || !currentTexture) { - if (currentRT) currentRT->Release(); - if (currentRTV) currentRTV->Release(); + if (currentRT) + currentRT->Release(); + if (currentRTV) + currentRTV->Release(); return; } D3D11_TEXTURE2D_DESC texDesc; currentTexture->GetDesc(&texDesc); - + // Create blur textures if needed CreateBlurTextures(texDesc.Width, texDesc.Height, texDesc.Format); - + // Find ImGui windows that need blur ImGuiContext* ctx = ImGui::GetCurrentContext(); if (!ctx || ctx->Windows.Size == 0) { @@ -283,7 +285,7 @@ void ThemeManager::RenderBackgroundBlur() // Perform blur for this window area PerformGaussianBlur(currentTexture, currentRTV, windowMin, windowMax); } - + // Cleanup currentTexture->Release(); currentRT->Release(); @@ -294,48 +296,47 @@ void ThemeManager::ForceApplyDefaultTheme() { // This function applies Default.json colors directly to ImGui, bypassing any hardcoded defaults // It's used when the theme system fails or ImGui resets to defaults unexpectedly - + auto* themeManager = GetSingleton(); json defaultThemeSettings; - + if (!themeManager->LoadTheme("Default", defaultThemeSettings)) { logger::warn("ForceApplyDefaultTheme: Could not load Default.json theme"); return; } - + auto& style = ImGui::GetStyle(); auto& colors = style.Colors; - + // Apply the Default.json theme's FullPalette directly to ImGui colors if (defaultThemeSettings.contains("FullPalette") && defaultThemeSettings["FullPalette"].is_array()) { auto& palette = defaultThemeSettings["FullPalette"]; - + for (size_t i = 0; i < std::min(palette.size(), static_cast(ImGuiCol_COUNT)); ++i) { if (palette[i].is_array() && palette[i].size() >= 4) { colors[i] = ImVec4( palette[i][0].get(), - palette[i][1].get(), + palette[i][1].get(), palette[i][2].get(), - palette[i][3].get() - ); + palette[i][3].get()); } } logger::info("ForceApplyDefaultTheme: Applied Default.json colors directly to ImGui"); } else { logger::warn("ForceApplyDefaultTheme: Default.json missing FullPalette - applying basic dark theme"); - + // Fallback: Apply a basic dark theme that matches Default.json style - colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.05f, 0.05f, 1.0f); // Dark background - colors[ImGuiCol_Text] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text - colors[ImGuiCol_Border] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Gray border - colors[ImGuiCol_ChildBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.0f); // Slightly darker child background - colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 1.0f); // Popup background - colors[ImGuiCol_Header] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Header background - colors[ImGuiCol_HeaderHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Header hover - colors[ImGuiCol_HeaderActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Header active - colors[ImGuiCol_Button] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Button background - colors[ImGuiCol_ButtonHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Button hover - colors[ImGuiCol_ButtonActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Button active + colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.05f, 0.05f, 1.0f); // Dark background + colors[ImGuiCol_Text] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text + colors[ImGuiCol_Border] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Gray border + colors[ImGuiCol_ChildBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.0f); // Slightly darker child background + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 1.0f); // Popup background + colors[ImGuiCol_Header] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Header background + colors[ImGuiCol_HeaderHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Header hover + colors[ImGuiCol_HeaderActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Header active + colors[ImGuiCol_Button] = ImVec4(0.2f, 0.2f, 0.2f, 1.0f); // Button background + colors[ImGuiCol_ButtonHovered] = ImVec4(0.3f, 0.3f, 0.3f, 1.0f); // Button hover + colors[ImGuiCol_ButtonActive] = ImVec4(0.4f, 0.4f, 0.4f, 1.0f); // Button active } } @@ -783,7 +784,7 @@ float ThemeManager::ResolveFontSize(const Menu& menu) return std::clamp(dynamicSize, Constants::MIN_FONT_SIZE, Constants::MAX_FONT_SIZE); } -// Blur shader implementation +// Blur shader implementation // https://github.com/cofenberg/unrimp/ bool ThemeManager::InitializeBlurShaders() { @@ -803,7 +804,7 @@ bool ThemeManager::InitializeBlurShaders() try { // Baked-in HLSL shaders for reliable Gaussian blur implementation // Based on Unrimp rendering engine's separable blur architecture - + const char* horizontalBlurShader = R"( // Horizontal Gaussian Blur Shader - Baked into ThemeManager.cpp cbuffer BlurBuffer : register(b0) @@ -845,25 +846,25 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); float totalWeight = 0.0f; - + const int samples = min(BlurParams.x, 15); const int halfSamples = samples / 2; - + for (int i = -halfSamples; i <= halfSamples; ++i) { float2 sampleCoord = input.TexCoord + float2(i * TexelSize.x, 0.0f); float weight = GaussianWeight(float(i)); - + if (sampleCoord.x >= 0.0f && sampleCoord.x <= 1.0f) { result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; totalWeight += weight; } } - + if (totalWeight > 0.0f) result /= totalWeight; - + return result; } )"; @@ -909,25 +910,25 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET { float4 result = float4(0.0f, 0.0f, 0.0f, 0.0f); float totalWeight = 0.0f; - + const int samples = min(BlurParams.x, 15); const int halfSamples = samples / 2; - + for (int i = -halfSamples; i <= halfSamples; ++i) { float2 sampleCoord = input.TexCoord + float2(0.0f, i * TexelSize.y); float weight = GaussianWeight(float(i)); - + if (sampleCoord.y >= 0.0f && sampleCoord.y <= 1.0f) { result += InputTexture.Sample(LinearSampler, sampleCoord) * weight; totalWeight += weight; } } - + if (totalWeight > 0.0f) result /= totalWeight; - + return result; } )"; @@ -935,11 +936,11 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET // Compile vertex shader using D3DCompile with baked-in HLSL ID3DBlob* vsBlob = nullptr; ID3DBlob* errorBlob = nullptr; - - HRESULT hr = D3DCompile(horizontalBlurShader, strlen(horizontalBlurShader), + + HRESULT hr = D3DCompile(horizontalBlurShader, strlen(horizontalBlurShader), "InlineHorizontalBlurShader", nullptr, nullptr, "VS_Main", "vs_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob); - + if (FAILED(hr)) { if (errorBlob) { logger::error("Failed to compile baked Gaussian blur vertex shader: {}", (char*)errorBlob->GetBufferPointer()); @@ -951,7 +952,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET hr = device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &blurVertexShader); vsBlob->Release(); - + if (FAILED(hr)) { logger::error("Failed to create Gaussian blur vertex shader"); initializationFailed = true; @@ -963,7 +964,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET hr = D3DCompile(horizontalBlurShader, strlen(horizontalBlurShader), "InlineHorizontalBlurShader", nullptr, nullptr, "PS_Main", "ps_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &hpsBlob, &errorBlob); - + if (FAILED(hr)) { if (errorBlob) { logger::error("Failed to compile baked horizontal Gaussian blur pixel shader: {}", (char*)errorBlob->GetBufferPointer()); @@ -975,7 +976,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET hr = device->CreatePixelShader(hpsBlob->GetBufferPointer(), hpsBlob->GetBufferSize(), nullptr, &blurHorizontalPixelShader); hpsBlob->Release(); - + if (FAILED(hr)) { logger::error("Failed to create horizontal Gaussian blur pixel shader"); initializationFailed = true; @@ -987,7 +988,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET hr = D3DCompile(verticalBlurShader, strlen(verticalBlurShader), "InlineVerticalBlurShader", nullptr, nullptr, "PS_Main", "ps_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vpsBlob, &errorBlob); - + if (FAILED(hr)) { if (errorBlob) { logger::error("Failed to compile baked vertical Gaussian blur pixel shader: {}", (char*)errorBlob->GetBufferPointer()); @@ -999,7 +1000,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET hr = device->CreatePixelShader(vpsBlob->GetBufferPointer(), vpsBlob->GetBufferSize(), nullptr, &blurVerticalPixelShader); vpsBlob->Release(); - + if (FAILED(hr)) { logger::error("Failed to create vertical Gaussian blur pixel shader"); initializationFailed = true; @@ -1009,9 +1010,9 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET // Create constant buffer for blur parameters based on Unrimp architecture D3D11_BUFFER_DESC bufferDesc = {}; bufferDesc.Usage = D3D11_USAGE_DEFAULT; - bufferDesc.ByteWidth = 32; // Match our BlurConstants struct: float4 texelSize + int4 blurParams + bufferDesc.ByteWidth = 32; // Match our BlurConstants struct: float4 texelSize + int4 blurParams bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - + hr = device->CreateBuffer(&bufferDesc, nullptr, &blurConstantBuffer); if (FAILED(hr)) { logger::error("Failed to create blur constant buffer"); @@ -1028,7 +1029,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET samplerDesc.MaxAnisotropy = 1; samplerDesc.MinLOD = 0; samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; - + hr = device->CreateSamplerState(&samplerDesc, &blurSamplerState); if (FAILED(hr)) { logger::error("Failed to create blur sampler state"); @@ -1048,7 +1049,7 @@ float4 PS_Main(VS_OUTPUT input) : SV_TARGET blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; - + hr = device->CreateBlendState(&blendDesc, &blurBlendState); if (FAILED(hr)) { logger::error("Failed to create blur blend state"); @@ -1079,15 +1080,28 @@ void ThemeManager::CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT forma } // Clean up existing textures - if (blurTexture1) blurTexture1->Release(); blurTexture1 = nullptr; - if (blurTexture2) blurTexture2->Release(); blurTexture2 = nullptr; - if (blurRTV1) blurRTV1->Release(); blurRTV1 = nullptr; - if (blurRTV2) blurRTV2->Release(); blurRTV2 = nullptr; - if (blurSRV1) blurSRV1->Release(); blurSRV1 = nullptr; - if (blurSRV2) blurSRV2->Release(); blurSRV2 = nullptr; + if (blurTexture1) + blurTexture1->Release(); + blurTexture1 = nullptr; + if (blurTexture2) + blurTexture2->Release(); + blurTexture2 = nullptr; + if (blurRTV1) + blurRTV1->Release(); + blurRTV1 = nullptr; + if (blurRTV2) + blurRTV2->Release(); + blurRTV2 = nullptr; + if (blurSRV1) + blurSRV1->Release(); + blurSRV1 = nullptr; + if (blurSRV2) + blurSRV2->Release(); + blurSRV2 = nullptr; auto device = globals::d3d::device; - if (!device) return; + if (!device) + return; // Use full resolution textures for better quality UINT blurWidth = width; @@ -1149,7 +1163,8 @@ void ThemeManager::CreateBlurTextures(UINT width, UINT height, DXGI_FORMAT forma void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11RenderTargetView* targetRTV, ImVec2 menuMin, ImVec2 menuMax) { auto context = globals::d3d::context; - if (!context || !sourceTexture || !targetRTV) return; + if (!context || !sourceTexture || !targetRTV) + return; // Get source texture description D3D11_TEXTURE2D_DESC sourceDesc; @@ -1178,7 +1193,7 @@ void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11Ren FLOAT menuTop = std::max(0.0f, menuMin.y); FLOAT menuRight = std::min(static_cast(sourceDesc.Width), menuMax.x); FLOAT menuBottom = std::min(static_cast(sourceDesc.Height), menuMax.y); - + // Set scissor rectangle to limit blur to menu area D3D11_RECT scissorRect; scissorRect.left = static_cast(menuLeft); @@ -1188,24 +1203,25 @@ void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11Ren context->RSSetScissorRects(1, &scissorRect); // Set up blur parameters matching our Unrimp-based HLSL shader structure - struct BlurConstants { - float texelSize[4]; // x = 1/width, y = 1/height, z = blur strength, w = unused - int blurParams[4]; // x = samples, y = unused, z = unused, w = unused + struct BlurConstants + { + float texelSize[4]; // x = 1/width, y = 1/height, z = blur strength, w = unused + int blurParams[4]; // x = samples, y = unused, z = unused, w = unused } constants; - + // Calculate blur parameters based on intensity slider - float blurRadius = currentBlurIntensity * 5.0f; // Scale blur radius by intensity - int sampleCount = std::max(3, std::min(15, static_cast(7 + currentBlurIntensity * 8))); // 3-15 samples based on intensity - + float blurRadius = currentBlurIntensity * 5.0f; // Scale blur radius by intensity + int sampleCount = std::max(3, std::min(15, static_cast(7 + currentBlurIntensity * 8))); // 3-15 samples based on intensity + constants.texelSize[0] = blurRadius / static_cast(blurTextureWidth); constants.texelSize[1] = blurRadius / static_cast(blurTextureHeight); - constants.texelSize[2] = currentBlurIntensity; // Blur strength multiplier - constants.texelSize[3] = 0.0f; // Unused - - constants.blurParams[0] = sampleCount; // Dynamic sample count based on intensity - constants.blurParams[1] = 0; // Unused - constants.blurParams[2] = 0; // Unused - constants.blurParams[3] = 0; // Unused + constants.texelSize[2] = currentBlurIntensity; // Blur strength multiplier + constants.texelSize[3] = 0.0f; // Unused + + constants.blurParams[0] = sampleCount; // Dynamic sample count based on intensity + constants.blurParams[1] = 0; // Unused + constants.blurParams[2] = 0; // Unused + constants.blurParams[3] = 0; // Unused context->UpdateSubresource(blurConstantBuffer, 0, nullptr, &constants, 0, 0); @@ -1226,24 +1242,24 @@ void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11Ren context->OMSetRenderTargets(1, &blurRTV1, nullptr); context->PSSetShader(blurHorizontalPixelShader, nullptr, 0); context->PSSetShaderResources(0, 1, &sourceSRV); - context->Draw(3, 0); // Draw fullscreen triangle + context->Draw(3, 0); // Draw fullscreen triangle // Second pass: Vertical blur (blur texture 1 -> blur texture 2) context->OMSetRenderTargets(1, &blurRTV2, nullptr); context->PSSetShader(blurVerticalPixelShader, nullptr, 0); ID3D11ShaderResourceView* nullSRV = nullptr; - context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV + context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV context->PSSetShaderResources(0, 1, &blurSRV1); context->Draw(3, 0); // Final composition: Blend blurred result back to main render target (only in menu area) context->RSSetViewports(1, &originalViewport); context->OMSetRenderTargets(1, &targetRTV, nullptr); - + // Enable scissor test to limit blur to menu area ID3D11RasterizerState* originalRS = nullptr; context->RSGetState(&originalRS); - + // Create rasterizer state with scissor test enabled for final composition ID3D11RasterizerState* scissorRS = nullptr; D3D11_RASTERIZER_DESC rsDesc = {}; @@ -1261,17 +1277,17 @@ void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11Ren rsDesc.AntialiasedLineEnable = FALSE; } rsDesc.ScissorEnable = TRUE; - + device->CreateRasterizerState(&rsDesc, &scissorRS); if (scissorRS) { context->RSSetState(scissorRS); } - + // Set blend state for proper compositing float blendFactor[4] = { 1.0f, 1.0f, 1.0f, currentBlurIntensity * 0.8f }; context->OMSetBlendState(blurBlendState, blendFactor, 0xFFFFFFFF); - - context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV + + context->PSSetShaderResources(0, 1, &nullSRV); // Clear previous SRV context->PSSetShaderResources(0, 1, &blurSRV2); context->Draw(3, 0); @@ -1280,32 +1296,61 @@ void ThemeManager::PerformGaussianBlur(ID3D11Texture2D* sourceTexture, ID3D11Ren context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); context->PSSetShaderResources(0, 1, &nullSRV); context->RSSetState(originalRS); - context->RSSetScissorRects(0, nullptr); // Disable scissor test - + context->RSSetScissorRects(0, nullptr); // Disable scissor test + // Clean up - if (sourceSRV) sourceSRV->Release(); - if (originalRTV) originalRTV->Release(); - if (originalDSV) originalDSV->Release(); - if (originalRS) originalRS->Release(); - if (scissorRS) scissorRS->Release(); + if (sourceSRV) + sourceSRV->Release(); + if (originalRTV) + originalRTV->Release(); + if (originalDSV) + originalDSV->Release(); + if (originalRS) + originalRS->Release(); + if (scissorRS) + scissorRS->Release(); } void ThemeManager::CleanupBlurResources() { - if (blurVertexShader) blurVertexShader->Release(); blurVertexShader = nullptr; - if (blurHorizontalPixelShader) blurHorizontalPixelShader->Release(); blurHorizontalPixelShader = nullptr; - if (blurVerticalPixelShader) blurVerticalPixelShader->Release(); blurVerticalPixelShader = nullptr; - if (blurConstantBuffer) blurConstantBuffer->Release(); blurConstantBuffer = nullptr; - if (blurSamplerState) blurSamplerState->Release(); blurSamplerState = nullptr; - if (blurBlendState) blurBlendState->Release(); blurBlendState = nullptr; - - if (blurTexture1) blurTexture1->Release(); blurTexture1 = nullptr; - if (blurTexture2) blurTexture2->Release(); blurTexture2 = nullptr; - if (blurRTV1) blurRTV1->Release(); blurRTV1 = nullptr; - if (blurRTV2) blurRTV2->Release(); blurRTV2 = nullptr; - if (blurSRV1) blurSRV1->Release(); blurSRV1 = nullptr; - if (blurSRV2) blurSRV2->Release(); blurSRV2 = nullptr; - + if (blurVertexShader) + blurVertexShader->Release(); + blurVertexShader = nullptr; + if (blurHorizontalPixelShader) + blurHorizontalPixelShader->Release(); + blurHorizontalPixelShader = nullptr; + if (blurVerticalPixelShader) + blurVerticalPixelShader->Release(); + blurVerticalPixelShader = nullptr; + if (blurConstantBuffer) + blurConstantBuffer->Release(); + blurConstantBuffer = nullptr; + if (blurSamplerState) + blurSamplerState->Release(); + blurSamplerState = nullptr; + if (blurBlendState) + blurBlendState->Release(); + blurBlendState = nullptr; + + if (blurTexture1) + blurTexture1->Release(); + blurTexture1 = nullptr; + if (blurTexture2) + blurTexture2->Release(); + blurTexture2 = nullptr; + if (blurRTV1) + blurRTV1->Release(); + blurRTV1 = nullptr; + if (blurRTV2) + blurRTV2->Release(); + blurRTV2 = nullptr; + if (blurSRV1) + blurSRV1->Release(); + blurSRV1 = nullptr; + if (blurSRV2) + blurSRV2->Release(); + blurSRV2 = nullptr; + blurTextureWidth = 0; blurTextureHeight = 0; isBlurEnabled = false; diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 46c2a6c635..956c524531 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -45,8 +45,8 @@ class ThemeManager static void SetupImGuiStyle(const class Menu& menu); static void ReloadFont(const class Menu& menu, float& cachedFontSize); static void ApplyBackgroundBlur(float blurIntensity, ImVec4* colors); - static void RenderBackgroundBlur(); // Real-time shader-based blur rendering - static void ForceApplyDefaultTheme(); // Force Default.json colors to ImGui (bypass hardcoded defaults) + static void RenderBackgroundBlur(); // Real-time shader-based blur rendering + static void ForceApplyDefaultTheme(); // Force Default.json colors to ImGui (bypass hardcoded defaults) // Blur system methods - inspired by Unrimp rendering engine // Credits: Christian Ofenberg and the Unrimp project (https://github.com/cofenberg/unrimp) @@ -170,7 +170,7 @@ class ThemeManager // Blur system state static inline float currentBlurIntensity = 0.0f; static inline bool isBlurEnabled = false; - + // DirectX blur resources static inline ID3D11VertexShader* blurVertexShader = nullptr; static inline ID3D11PixelShader* blurHorizontalPixelShader = nullptr; @@ -178,7 +178,7 @@ class ThemeManager static inline ID3D11Buffer* blurConstantBuffer = nullptr; static inline ID3D11SamplerState* blurSamplerState = nullptr; static inline ID3D11BlendState* blurBlendState = nullptr; - + // Intermediate blur textures static inline ID3D11Texture2D* blurTexture1 = nullptr; static inline ID3D11Texture2D* blurTexture2 = nullptr; @@ -186,7 +186,7 @@ class ThemeManager static inline ID3D11RenderTargetView* blurRTV2 = nullptr; static inline ID3D11ShaderResourceView* blurSRV1 = nullptr; static inline ID3D11ShaderResourceView* blurSRV2 = nullptr; - + static inline UINT blurTextureWidth = 0; static inline UINT blurTextureHeight = 0; diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 6540ed4658..35a8f634a7 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -313,7 +313,7 @@ namespace Util // Render logo ImGui::Image(logoTexture, logoSize); ImGui::SameLine(); - + // Add consistent spacing between logo and text ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f); @@ -1391,67 +1391,68 @@ namespace Util bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size) { - if (!enabled) return false; + if (!enabled) + return false; // Calculate appropriate size if not specified - make it smaller ImVec2 toggleSize = size; if (toggleSize.x <= 0) { - toggleSize.x = ImGui::GetFrameHeight() * 1.6f; // Smaller 1.6:1 aspect ratio + toggleSize.x = ImGui::GetFrameHeight() * 1.6f; // Smaller 1.6:1 aspect ratio } if (toggleSize.y <= 0) { - toggleSize.y = ImGui::GetFrameHeight() * 0.8f; // Smaller height + toggleSize.y = ImGui::GetFrameHeight() * 0.8f; // Smaller height } // Get theme colors for better integration auto& style = ImGui::GetStyle(); auto& colors = style.Colors; - + // Use theme header colors instead of bright green/red - ImVec4 toggleBg = *enabled ? - colors[ImGuiCol_Header] : // Use header color when enabled - colors[ImGuiCol_FrameBg]; // Use frame background when disabled - + ImVec4 toggleBg = *enabled ? + colors[ImGuiCol_Header] : // Use header color when enabled + colors[ImGuiCol_FrameBg]; // Use frame background when disabled + ImVec4 toggleBgHovered = *enabled ? - colors[ImGuiCol_HeaderHovered] : // Use header hovered when enabled - colors[ImGuiCol_FrameBgHovered]; // Use frame hovered when disabled - + colors[ImGuiCol_HeaderHovered] : // Use header hovered when enabled + colors[ImGuiCol_FrameBgHovered]; // Use frame hovered when disabled + ImVec4 toggleBgActive = *enabled ? - colors[ImGuiCol_HeaderActive] : // Use header active when enabled - colors[ImGuiCol_FrameBgActive]; // Use frame active when disabled + colors[ImGuiCol_HeaderActive] : // Use header active when enabled + colors[ImGuiCol_FrameBgActive]; // Use frame active when disabled // Apply toggle styling with border ImGui::PushStyleColor(ImGuiCol_Button, toggleBg); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, toggleBgHovered); ImGui::PushStyleColor(ImGuiCol_ButtonActive, toggleBgActive); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toggleSize.y * 0.5f); // Round ends - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); // Larger border + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toggleSize.y * 0.5f); // Round ends + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); // Larger border // Create unique ID for the toggle ImGui::PushID(label); - + // Draw the toggle button bool clicked = ImGui::Button("", toggleSize); - + // Draw the toggle knob ImDrawList* drawList = ImGui::GetWindowDrawList(); ImVec2 buttonMin = ImGui::GetItemRectMin(); ImVec2 buttonMax = ImGui::GetItemRectMax(); - + // Calculate knob position and size float knobRadius = (toggleSize.y - 4.0f) * 0.5f; float knobPadding = 2.0f; float knobTravel = toggleSize.x - (knobRadius * 2.0f) - (knobPadding * 2.0f); - float knobX = *enabled ? - buttonMin.x + knobPadding + knobRadius + knobTravel : - buttonMin.x + knobPadding + knobRadius; + float knobX = *enabled ? + buttonMin.x + knobPadding + knobRadius + knobTravel : + buttonMin.x + knobPadding + knobRadius; float knobY = buttonMin.y + toggleSize.y * 0.5f; - + // Draw knob ImU32 knobColor = ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); drawList->AddCircleFilled(ImVec2(knobX, knobY), knobRadius, knobColor); - + ImGui::PopID(); - ImGui::PopStyleVar(2); // Pop both FrameRounding and FrameBorderSize + ImGui::PopStyleVar(2); // Pop both FrameRounding and FrameBorderSize ImGui::PopStyleColor(3); // Handle toggle action From f2efcf39a458dd7028036d7c2e416a0c8c3ee516 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 6 Oct 2025 22:02:35 +1000 Subject: [PATCH 23/29] Font Family Support --- .../Fonts/IBMPlexMono/IBMPlexMono-Light.ttf | Bin 0 -> 133468 bytes .../Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf | Bin 0 -> 133796 bytes .../IBMPlexMono/IBMPlexMono-SemiBold.ttf | Bin 0 -> 138448 bytes .../Fonts/IBMPlexMono/OFL.txt | 93 ++++ .../Fonts/Jost/Jost-Light.ttf | Bin 0 -> 61624 bytes .../Fonts/{ => Jost}/Jost-Regular.ttf | Bin .../Fonts/Jost/Jost-SemiBold.ttf | Bin 0 -> 61648 bytes .../CommunityShaders/Fonts/{ => Jost}/OFL.txt | 0 .../LibreBaskerville-Bold.ttf | Bin 0 -> 160652 bytes .../LibreBaskerville-Italic.ttf | Bin 0 -> 195048 bytes .../LibreBaskerville-Regular.ttf | Bin 0 -> 159676 bytes .../Fonts/LibreBaskerville/OFL.txt | 94 ++++ .../CommunityShaders/Fonts/Roboto/OFL.txt | 93 ++++ .../Fonts/Roboto/Roboto-Bold.ttf | Bin 0 -> 146768 bytes .../Fonts/Roboto/Roboto-Regular.ttf | Bin 0 -> 146004 bytes .../Fonts/Roboto/Roboto-SemiBold.ttf | Bin 0 -> 146760 bytes .../Fonts/Roboto/Roboto-Thin.ttf | Bin 0 -> 145936 bytes .../Fonts/Roboto/Roboto_Condensed-Light.ttf | Bin 0 -> 145952 bytes .../Fonts/Roboto/Roboto_Condensed-Regular.ttf | Bin 0 -> 145908 bytes .../Roboto/Roboto_Condensed-SemiBold.ttf | Bin 0 -> 146516 bytes .../Fonts/Sanguis/Sanguis.ttf | Bin 0 -> 60536 bytes .../Fonts/Sovngarde/SovngardeBold.ttf | Bin 0 -> 68540 bytes .../Fonts/Sovngarde/SovngardeLight.ttf | Bin 0 -> 70168 bytes .../CommunityShaders/Themes/Amber.json | 43 +- .../CommunityShaders/Themes/Default.json | 41 ++ .../CommunityShaders/Themes/DragonBlood.json | 43 +- .../CommunityShaders/Themes/DwemerBronze.json | 43 +- .../CommunityShaders/Themes/Forest.json | 43 +- .../CommunityShaders/Themes/HighContrast.json | 43 +- .../CommunityShaders/Themes/Light.json | 43 +- .../CommunityShaders/Themes/Mystic.json | 102 ----- .../CommunityShaders/Themes/NordicFrost.json | 43 +- .../CommunityShaders/Themes/Ocean.json | 43 +- .../Plugins/CommunityShaders/Themes/README.md | 107 ----- src/Features/VR.cpp | 46 +- src/Menu.cpp | 124 +++--- src/Menu.h | 107 ++++- src/Menu/FeatureListRenderer.cpp | 311 +++++++------ src/Menu/Fonts.cpp | 415 ++++++++++++++++++ src/Menu/Fonts.h | 72 +++ src/Menu/MenuHeaderRenderer.cpp | 49 ++- src/Menu/OverlayRenderer.cpp | 8 +- src/Menu/SettingsTabRenderer.cpp | 382 +++++++++++----- src/Menu/SettingsTabRenderer.h | 1 + src/Menu/ThemeManager.cpp | 127 +++++- src/Utils/UI.cpp | 64 --- src/Utils/UI.h | 15 +- 47 files changed, 1949 insertions(+), 646 deletions(-) create mode 100644 package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Regular.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt create mode 100644 package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf rename package/Interface/CommunityShaders/Fonts/{ => Jost}/Jost-Regular.ttf (100%) create mode 100644 package/Interface/CommunityShaders/Fonts/Jost/Jost-SemiBold.ttf rename package/Interface/CommunityShaders/Fonts/{ => Jost}/OFL.txt (100%) create mode 100644 package/Interface/CommunityShaders/Fonts/LibreBaskerville/LibreBaskerville-Bold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/LibreBaskerville/LibreBaskerville-Italic.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/LibreBaskerville/LibreBaskerville-Regular.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/LibreBaskerville/OFL.txt create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/OFL.txt create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Bold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Regular.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto-SemiBold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto-Thin.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Light.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-Regular.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Roboto/Roboto_Condensed-SemiBold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Sanguis/Sanguis.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeBold.ttf create mode 100644 package/Interface/CommunityShaders/Fonts/Sovngarde/SovngardeLight.ttf delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/Mystic.json delete mode 100644 package/SKSE/Plugins/CommunityShaders/Themes/README.md create mode 100644 src/Menu/Fonts.cpp create mode 100644 src/Menu/Fonts.h diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0dcb2fba5bfe024b9fb44c686f58cff2560da882 GIT binary patch literal 133468 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[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�E6E!8AFV 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⁣rQSDW#esk4*UBqI1R6Qk9 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 O8O^Eu)*9E2^Zx)#YBXm6 literal 0 HcmV?d00001 diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/IBMPlexMono-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e0b41df1aa5307433af91fed67d9e5bac2a1658 GIT binary patch literal 138448 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 XvE+krhs?SJ?vS(rPuX}G;|}>haX~K8 literal 0 HcmV?d00001 diff --git a/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt new file mode 100644 index 0000000000..e423b74780 --- /dev/null +++ b/package/Interface/CommunityShaders/Fonts/IBMPlexMono/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf b/package/Interface/CommunityShaders/Fonts/Jost/Jost-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8cf8779c93ef1b943ca2ae4c2f544dd93791fa51 GIT binary patch literal 61624 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 z