diff --git a/README.md b/README.md index 362ac1cd7f..19d76b3f50 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,8 @@ See LICENSE within each directory; if none, it's [Default](#default) - [Features Shaders](features) - [Package Shaders](package/Shaders/) + +### Icons + +- [Microsoft Icons](package/Interface/CommunityShaders/Icons/Microsoft%20Icons/) are subject to the [MIT License](package/Interface/CommunityShaders/Icons/Microsoft%20Icons/LICENSE) https://github.com/microsoft/fluentui-system-icons +- [Community Shaders Logo](package/Interface/CommunityShaders/Icons/Community%20Shaders%20Logo/) is not covered by the GPL-3.0 license. It is provided solely for personal use (e.g., building from source) and may only be used in unmodified form. There is no license for any other purpose or to distribute the logo. No trademark license is granted for the logo. Any use not expressly permitted is prohibited without the express written consent of the Community Shaders team. diff --git a/package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png new file mode 100644 index 0000000000..ed4d243390 Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Community Shaders Logo/cs-logo.png differ diff --git a/package/Interface/CommunityShaders/Icons/Microsoft Icons/LICENSE b/package/Interface/CommunityShaders/Icons/Microsoft Icons/LICENSE new file mode 100644 index 0000000000..bc9c36b28f --- /dev/null +++ b/package/Interface/CommunityShaders/Icons/Microsoft Icons/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-cache.png b/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-cache.png new file mode 100644 index 0000000000..0dfba061da Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-cache.png differ diff --git a/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-disk.png b/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-disk.png new file mode 100644 index 0000000000..9c7242b547 Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Microsoft Icons/clear-disk.png differ diff --git a/package/Interface/CommunityShaders/Icons/Microsoft Icons/load-settings.png b/package/Interface/CommunityShaders/Icons/Microsoft Icons/load-settings.png new file mode 100644 index 0000000000..dfb2d32e18 Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Microsoft Icons/load-settings.png differ diff --git a/package/Interface/CommunityShaders/Icons/Microsoft Icons/save-settings.png b/package/Interface/CommunityShaders/Icons/Microsoft Icons/save-settings.png new file mode 100644 index 0000000000..202a9c04a6 Binary files /dev/null and b/package/Interface/CommunityShaders/Icons/Microsoft Icons/save-settings.png differ diff --git a/src/Menu.cpp b/src/Menu.cpp index effb8415f6..25e09978a9 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -19,6 +19,7 @@ #include "TruePBR.h" #include "Upscaling.h" #include "Util.h" +#include "Utils/UI.h" #include "Features/LightLimitFix/ParticleLights.h" #include "Utils/UI.h" @@ -109,6 +110,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings, GlobalScale, UseSimplePalette, + ShowActionIcons, Palette, StatusPalette, FeatureHeading, @@ -220,7 +222,13 @@ void Menu::SetupImGuiStyle() const bool IsEnabled = false; Menu::~Menu() -{ +{ // Release icon textures if loaded + uiIcons.saveSettings.Release(); + uiIcons.loadSettings.Release(); + uiIcons.clearCache.Release(); + uiIcons.clearDiskCache.Release(); + uiIcons.logo.Release(); + ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -245,14 +253,20 @@ void Menu::Init() IMGUI_CHECKVERSION(); ImGui::CreateContext(); auto& imgui_io = ImGui::GetIO(); - imgui_io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_DockingEnable; imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset; + // Enhanced font configuration for sharper text rendering ImFontConfig font_config; - font_config.GlyphExtraSpacing.x = -0.5f; + font_config.GlyphExtraSpacing.x = 0.0f; // Neutral spacing for cleaner look + font_config.OversampleH = 3; // Increased horizontal oversampling for sharper text + font_config.OversampleV = 2; // Increased vertical oversampling + font_config.PixelSnapH = true; // Align to pixel grid for sharper rendering + font_config.RasterizerMultiply = 1.1f; // Slightly darker font rendering + font_config.FontBuilderFlags = 0; // No additional flags needed - imgui_io.Fonts->AddFontFromFileTTF("Data\\Interface\\CommunityShaders\\Fonts\\Jost-Regular.ttf", 36.0f, &font_config); + // Add high-quality font with improved settings + imgui_io.Fonts->AddFontFromFileTTF("Data\\Interface\\CommunityShaders\\Fonts\\Jost-Regular.ttf", 36, &font_config); DXGI_SWAP_CHAIN_DESC desc; globals::d3d::swapChain->GetDesc(&desc); @@ -273,6 +287,10 @@ void Menu::Init() } } } + // Load UI icons + if (!Util::InitializeMenuIcons(this)) { + logger::warn("Failed to load UI icons. Will fallback to text buttons"); + } initialized = true; } @@ -285,77 +303,235 @@ void Menu::DrawSettings() ImGui::SetNextWindowSize(Util::GetNativeViewportSizeScaled(0.8f), ImGuiCond_FirstUseEver); auto title = std::format("Community Shaders {}", Util::GetFormattedVersion(Plugin::VERSION)); - ImGui::Begin(title.c_str(), &IsEnabled, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar); { - if (!ImGui::IsWindowDocked()) { - ImGui::SetWindowFontScale(1.5f); - ImGui::TextUnformatted(title.c_str()); - ImGui::SetWindowFontScale(1.0f); + auto shaderCache = globals::shaderCache; + const float iconSize = 48.0f; + const ImVec2 buttonSize(iconSize, iconSize); // No padding for header icons + // Check if we can show icons - require setting enabled and at least some icons loaded + bool canShowIcons = settings.Theme.ShowActionIcons && + (uiIcons.saveSettings.texture || + uiIcons.loadSettings.texture || + uiIcons.clearCache.texture || + uiIcons.clearDiskCache.texture); + + // Debug logging for icon availability + if (settings.Theme.ShowActionIcons) { + logger::debug("Icon status - Save: {}, Load: {}, Cache: {}, Disk: {}, Logo: {}", + uiIcons.saveSettings.texture ? "OK" : "NULL", + uiIcons.loadSettings.texture ? "OK" : "NULL", + uiIcons.clearCache.texture ? "OK" : "NULL", + uiIcons.clearDiskCache.texture ? "OK" : "NULL", + uiIcons.logo.texture ? "OK" : "NULL"); + } - ImGui::Spacing(); + // Always show logo if available, regardless of action icons setting + bool showLogo = uiIcons.logo.texture != nullptr; + + // Begin a layout - with or without action buttons depending on settings + if ((showLogo || canShowIcons) && ImGui::BeginTable("##HeaderLayout", 2, ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Title", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Buttons", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableNextColumn(); // Title on the left with logo + if (!ImGui::IsWindowDocked()) { + const float textScaleFactor = 1.7f; + const float logoHeightScale = 1.3f; + const float titleHeight = ImGui::GetFontSize() * logoHeightScale; + + // Always display logo if texture is available + if (showLogo) { + float logoAspectRatio = uiIcons.logo.size.x / uiIcons.logo.size.y; + ImVec2 logoSize(titleHeight * logoAspectRatio, titleHeight); + + // Add a bit of padding before the logo and text + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 5.0f); + + // Use our helper to render aligned logo and text with perfect vertical alignment + Util::DrawAlignedTextWithLogo( + uiIcons.logo.texture, + logoSize, + title.c_str(), + textScaleFactor); + } else { + // No logo, just render the text with proper alignment + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 5.0f); + Util::DrawSharpText(title.c_str(), true, textScaleFactor); + ImGui::PopStyleVar(); + } + } // Buttons on the right + ImGui::TableNextColumn(); + // Only show action buttons if canShowIcons is true + if (canShowIcons) { + // Create a horizontal layout for the buttons and remove button borders + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 0.0f)); // Tighter spacing for the icons + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); // Remove button borders + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); // Transparent button background + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.7f, 0.7f, 0.2f)); // Subtle hover effect + + // Save Settings Button + if (uiIcons.saveSettings.texture) { + if (ImGui::ImageButton("##SaveSettingsBtn", uiIcons.saveSettings.texture, buttonSize)) { + globals::state->Save(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Save Settings"); + } + ImGui::SameLine(); + } + + // Load Settings Button + if (uiIcons.loadSettings.texture) { + if (ImGui::ImageButton("##LoadSettingsBtn", uiIcons.loadSettings.texture, buttonSize)) { + globals::state->Load(); + globals::features::llf::particleLights->GetConfigs(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Load Settings"); + } + ImGui::SameLine(); + } + + // Clear Shader Cache Button + if (uiIcons.clearCache.texture) { + if (ImGui::ImageButton("##ClearShaderCacheBtn", uiIcons.clearCache.texture, buttonSize)) { + shaderCache->Clear(); + // any features should be added to shadercache's clear. + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Clear Shader Cache\n\n" + "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " + "Clearing the shader cache will mean that shaders are recompiled only when the game re-encounters them. " + "This is only needed for hot-loading shaders for development purposes. "); + } + ImGui::SameLine(); + } + + // Clear Disk Cache Button + if (uiIcons.clearDiskCache.texture) { + if (ImGui::ImageButton("##ClearDiskCacheBtn", uiIcons.clearDiskCache.texture, buttonSize)) { + shaderCache->DeleteDiskCache(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Clear Disk Cache\n\n" + "The Disk Cache is a collection of compiled shaders on disk, which are automatically created when shaders are added to the Shader Cache. " + "If you do not have a Disk Cache, or it is outdated or invalid, you will see \"Compiling Shaders\" in the upper-left corner. " + "After this has completed you will no longer see this message apart from when loading from the Disk Cache. " + "Only delete the Disk Cache manually if you are encountering issues. "); + } + } + // Restore default style only if we pushed styles + ImGui::PopStyleVar(2); // Pop both style variables: ItemSpacing and FrameBorderSize + ImGui::PopStyleColor(2); // Pop both style colors: Button and ButtonHovered + } // End of canShowIcons action buttons section + + ImGui::EndTable(); + } else if (!(showLogo || canShowIcons)) { + // No icons available - show just the title without the table layout + if (!ImGui::IsWindowDocked()) { + ImGui::SetWindowFontScale(1.5f); + ImGui::TextUnformatted(title.c_str()); + ImGui::SetWindowFontScale(1.0f); + } + } + // First separator - always shown + if (!ImGui::IsWindowDocked()) { ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3.0f); ImGui::Spacing(); } - auto shaderCache = globals::shaderCache; + // If icons are disabled or missing textures, show action buttons as text between separators + if (!canShowIcons) { + if (ImGui::BeginTable("##ActionButtons", 4, ImGuiTableFlags_SizingStretchSame)) { + // Save Settings Button + ImGui::TableNextColumn(); + if (ImGui::Button("Save Settings", { -1, 0 })) { + globals::state->Save(); + } - if (ImGui::BeginTable("##LeButtons", 4, ImGuiTableFlags_SizingStretchSame)) { - ImGui::TableNextColumn(); - if (ImGui::Button("Save Settings", { -1, 0 })) { - globals::state->Save(); - } + // Load Settings Button + ImGui::TableNextColumn(); + if (ImGui::Button("Load Settings", { -1, 0 })) { + globals::state->Load(); + globals::features::llf::particleLights->GetConfigs(); + } - ImGui::TableNextColumn(); - if (ImGui::Button("Load Settings", { -1, 0 })) { - globals::state->Load(); - globals::features::llf::particleLights->GetConfigs(); - } + // Clear Shader Cache Button + ImGui::TableNextColumn(); + if (ImGui::Button("Clear Shader Cache", { -1, 0 })) { + shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " + "Clearing the shader cache will mean that shaders are recompiled only when the game re-encounters them. " + "This is only needed for hot-loading shaders for development purposes. "); + } - ImGui::TableNextColumn(); - if (ImGui::Button("Clear Shader Cache", { -1, 0 })) { - shaderCache->Clear(); - // any features should be added to shadercache's clear. - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " - "Clearing the shader cache will mean that shaders are recompiled only when the game re-encounters them. " - "This is only needed for hot-loading shaders for development purposes. "); + // Clear Disk Cache Button + ImGui::TableNextColumn(); + if (ImGui::Button("Clear Disk Cache", { -1, 0 })) { + shaderCache->DeleteDiskCache(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "The Disk Cache is a collection of compiled shaders on disk, which are automatically created when shaders are added to the Shader Cache. " + "If you do not have a Disk Cache, or it is outdated or invalid, you will see \"Compiling Shaders\" in the upper-left corner. " + "After this has completed you will no longer see this message apart from when loading from the Disk Cache. " + "Only delete the Disk Cache manually if you are encountering issues. "); + } + + // Error message toggle if needed + if (shaderCache->GetFailedTasks()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Button("Toggle Error Message", { -1, 0 })) { + shaderCache->ToggleErrorMessages(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Hide or show the shader failure message. " + "Your installation is broken and will likely see errors in game. " + "Please double check you have updated all features and that your load order is correct. " + "See CommunityShaders.log for details and check the Nexus Mods page or Discord server. "); + } + } + + ImGui::EndTable(); } - ImGui::TableNextColumn(); - if (ImGui::Button("Clear Disk Cache", { -1, 0 })) { - shaderCache->DeleteDiskCache(); + // Second separator - only shown if icons are disabled/missing or if there are failed tasks + if (!ImGui::IsWindowDocked()) { + ImGui::Spacing(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3.0f); + ImGui::Spacing(); + } + } else if (shaderCache->GetFailedTasks()) { + // If icons are enabled but there are failed tasks, show error toggle button + // and add the second separator + if (ImGui::Button("Toggle Error Message", { -1, 0 })) { + shaderCache->ToggleErrorMessages(); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "The Disk Cache is a collection of compiled shaders on disk, which are automatically created when shaders are added to the Shader Cache. " - "If you do not have a Disk Cache, or it is outdated or invalid, you will see \"Compiling Shaders\" in the upper-left corner. " - "After this has completed you will no longer see this message apart from when loading from the Disk Cache. " - "Only delete the Disk Cache manually if you are encountering issues. "); + "Hide or show the shader failure message. " + "Your installation is broken and will likely see errors in game. " + "Please double check you have updated all features and that your load order is correct. " + "See CommunityShaders.log for details and check the Nexus Mods page or Discord server. "); } - if (shaderCache->GetFailedTasks()) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::Button("Toggle Error Message", { -1, 0 })) { - shaderCache->ToggleErrorMessages(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Hide or show the shader failure message. " - "Your installation is broken and will likely see errors in game. " - "Please double check you have updated all features and that your load order is correct. " - "See CommunityShaders.log for details and check the Nexus Mods page or Discord server. "); - } + // Add second separator when showing error button + if (!ImGui::IsWindowDocked()) { + ImGui::Spacing(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3.0f); + ImGui::Spacing(); } - ImGui::EndTable(); + } else { // No additional separator needed - already handled in the conditional block above } - ImGui::Spacing(); - ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3.0f); - ImGui::Spacing(); + // Main content starts here - no additional separator needed as it's already handled in the conditions above float footer_height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y * 3 + 3.0f; // text + separator @@ -818,14 +994,26 @@ void Menu::DrawGeneralSettings() auto& colors = themeSettings.FullPalette; if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { - if (ImGui::BeginTabItem("Sizes")) { - if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.0f, 1.0f, "%.2f")) { + if (ImGui::BeginTabItem("UI Options")) { + if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.f, 1.f, "%.2f")) { float trueScale = exp2(themeSettings.GlobalScale); auto& io = ImGui::GetIO(); io.FontGlobalScale = trueScale; } + ImGui::SeparatorText("UI Elements"); + ImGui::Checkbox("Use Icon Buttons in Header", &themeSettings.ShowActionIcons); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "When enabled: Shows action buttons (Save, Load, Clear Cache, Clear Disk Cache) as icons in the header\n" + "When disabled: Shows as text buttons below the header"); + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Sizes")) { ImGui::SeparatorText("Main"); ImGui::SliderFloat2("Window Padding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("Frame Padding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); diff --git a/src/Menu.h b/src/Menu.h index 41c330ed34..3325092cc9 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -46,12 +46,35 @@ class Menu void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); void OnFocusLost(); + // UI icon textures + struct UIIcon + { + ID3D11ShaderResourceView* texture = nullptr; + ImVec2 size = ImVec2(32.0f, 32.0f); + + void Release() + { + if (texture) { + texture->Release(); + texture = nullptr; + } + } + }; + struct UIIcons + { + UIIcon saveSettings; + UIIcon loadSettings; + UIIcon clearCache; + UIIcon clearDiskCache; + UIIcon logo; // New logo icon + } uiIcons; struct ThemeSettings { float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential bool UseSimplePalette = true; // simple palette or full customization + bool ShowActionIcons = true; // whether to show action buttons as icons struct PaletteColors { ImVec4 Background{ 0.f, 0.f, 0.f, 0.5882353186607361f }; diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index eb57aec6c6..8cdf0023fd 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,6 +1,17 @@ #include "UI.h" #include "Menu.h" +#include +#include +#include + +#include "../Globals.h" +#include "../Menu.h" + +#define STB_IMAGE_IMPLEMENTATION +#include +#include + namespace Util { PerformanceOverlay performanceOverlay; @@ -47,7 +58,241 @@ namespace Util const auto Size = ImGui::GetMainViewport()->Size; return { Size.x * scale, Size.y * scale }; } + // Icon loading functions (moved from UIIconLoader) + bool LoadTextureFromFile(ID3D11Device* device, + const char* filename, + ID3D11ShaderResourceView** out_srv, + ImVec2& out_size) + { + // Validate input parameters + if (!device || !out_srv) { + logger::warn("LoadTextureFromFile: Invalid parameters - device: {}, out_srv: {}", + device ? "valid" : "null", out_srv ? "valid" : "null"); + return false; + } + + // Initialize output to nullptr + *out_srv = nullptr; + + logger::debug("LoadTextureFromFile: Attempting to load {}", filename); + + // Load from disk into a raw RGBA buffer + int image_width = 0; + int image_height = 0; + int channels_in_file; + unsigned char* image_data = stbi_load(filename, &image_width, &image_height, &channels_in_file, 4); + if (image_data == NULL) { + logger::warn("LoadTextureFromFile: Failed to load image data from {}", filename); + return false; + } + // Creates Textures for Icons with Mipmapping to support high DPI displays. + logger::debug("LoadTextureFromFile: Loaded image {}x{} with {} channels from {}", + image_width, image_height, channels_in_file, filename); + D3D11_TEXTURE2D_DESC desc; + ZeroMemory(&desc, sizeof(desc)); + desc.Width = image_width; + desc.Height = image_height; + desc.MipLevels = 1; // Start with just one mip level + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; + desc.CPUAccessFlags = 0; + + ID3D11Texture2D* pTexture = nullptr; + D3D11_SUBRESOURCE_DATA subResource; + subResource.pSysMem = image_data; + subResource.SysMemPitch = desc.Width * 4; + subResource.SysMemSlicePitch = 0; + + HRESULT hr = device->CreateTexture2D(&desc, &subResource, &pTexture); + if (FAILED(hr) || !pTexture) { + logger::warn("LoadTextureFromFile: Failed to create D3D11 texture, HRESULT: 0x{:08X}", static_cast(hr)); + stbi_image_free(image_data); + return false; + } + // Create simple shader resource view + hr = device->CreateShaderResourceView(pTexture, nullptr, out_srv); + if (FAILED(hr) || !*out_srv) { + logger::warn("LoadTextureFromFile: Failed to create shader resource view, HRESULT: 0x{:08X}", static_cast(hr)); + pTexture->Release(); + stbi_image_free(image_data); + *out_srv = nullptr; + return false; + } + + // Generate mipmaps for better icon quality at different scales + ID3D11DeviceContext* context = nullptr; + device->GetImmediateContext(&context); + if (context) { + context->GenerateMips(*out_srv); + context->Release(); + } + // Success - clean up intermediate resources + pTexture->Release(); + stbi_image_free(image_data); + + out_size = ImVec2((float)image_width, (float)image_height); + logger::debug("LoadTextureFromFile: Successfully loaded {} ({}x{})", filename, image_width, image_height); + return true; + } + bool InitializeMenuIcons(Menu* menu) + { + if (!menu) { + logger::warn("InitializeMenuIcons: Menu pointer is null"); + return false; + } + + // Get the D3D device from globals + ID3D11Device* device = globals::d3d::device; + if (!device) { + logger::warn("InitializeMenuIcons: D3D device is null"); + return false; + } + // Define path to icons + std::string basePath = "Data\\Interface\\CommunityShaders\\Icons\\"; + logger::info("InitializeMenuIcons: Loading icons from base path: {}", basePath); + + // Initialize all texture pointers to nullptr for safe cleanup + std::array texturePointers = { + &menu->uiIcons.saveSettings.texture, + &menu->uiIcons.loadSettings.texture, + &menu->uiIcons.clearCache.texture, + &menu->uiIcons.clearDiskCache.texture, + &menu->uiIcons.logo.texture + }; + + // Safely release existing textures + for (auto* texturePtr : texturePointers) { + if (*texturePtr) { + (*texturePtr)->Release(); + *texturePtr = nullptr; + } + } + + // Instead of failing completely if one icon fails, try to load each one individually + bool anyIconLoaded = false; + int iconsLoaded = 0; + + // Load save settings icon + if (LoadTextureFromFile(device, (basePath + "Microsoft Icons\\save-settings.png").c_str(), &menu->uiIcons.saveSettings.texture, menu->uiIcons.saveSettings.size)) { + logger::info("InitializeMenuIcons: Successfully loaded save-settings icon"); + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load save-settings icon from: {}", basePath + "Microsoft Icons\\save-settings.png"); + } + + // Load load settings icon + if (LoadTextureFromFile(device, (basePath + "Microsoft Icons\\load-settings.png").c_str(), &menu->uiIcons.loadSettings.texture, menu->uiIcons.loadSettings.size)) { + logger::info("InitializeMenuIcons: Successfully loaded load-settings icon"); + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load load-settings icon from: {}", basePath + "Microsoft Icons\\load-settings.png"); + } + // Load clear cache icon + if (LoadTextureFromFile(device, (basePath + "Microsoft Icons\\clear-cache.png").c_str(), &menu->uiIcons.clearCache.texture, menu->uiIcons.clearCache.size)) { + logger::info("InitializeMenuIcons: Successfully loaded clear-cache icon"); + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load clear-cache icon from: {}", basePath + "Microsoft Icons\\clear-cache.png"); + } + + // Load clear disk cache icon + if (LoadTextureFromFile(device, (basePath + "Microsoft Icons\\clear-disk.png").c_str(), &menu->uiIcons.clearDiskCache.texture, menu->uiIcons.clearDiskCache.size)) { + logger::info("InitializeMenuIcons: Successfully loaded clear-disk icon"); + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load clear-disk icon from: {}", basePath + "Microsoft Icons\\clear-disk.png"); + } + + // Load logo icon + if (LoadTextureFromFile(device, (basePath + "Community Shaders Logo\\cs-logo.png").c_str(), &menu->uiIcons.logo.texture, menu->uiIcons.logo.size)) { + logger::info("InitializeMenuIcons: Successfully loaded logo icon"); + iconsLoaded++; + anyIconLoaded = true; + } else { + logger::warn("InitializeMenuIcons: Failed to load logo icon from: {}", basePath + "Community Shaders Logo\\cs-logo.png"); + } + + logger::info("InitializeMenuIcons: Loaded {}/5 icons successfully", iconsLoaded); + return anyIconLoaded; + } + + // Text rendering helpers (moved from UITextHelper) + ImVec2 DrawSharpText(const char* text, bool alignToPixelGrid, float scale) + { + ImVec2 startPos = ImGui::GetCursorPos(); + + if (alignToPixelGrid) { + // Get current position + ImVec2 pos = ImGui::GetCursorPos(); + + // Align to pixel grid for sharper rendering + pos.x = std::round(pos.x); + pos.y = std::round(pos.y); + + // Set aligned position + ImGui::SetCursorPos(pos); + } + // Apply scale if needed + if (scale != 1.0f) { + ImGui::SetWindowFontScale(scale); + } + + // Use Text instead of TextUnformatted for better rendering + ImGui::Text("%s", text); + // Restore original scale if needed + if (scale != 1.0f) + ImGui::SetWindowFontScale(1.0f); + + // Calculate and return the rendered size + ImVec2 endPos = ImGui::GetCursorPos(); + return ImVec2(endPos.x - startPos.x, endPos.y - startPos.y); + } + + ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale) + { + // Save current cursor position + ImVec2 startPos = ImGui::GetCursorPos(); + + // Calculate scaled text height + float fontHeight = ImGui::GetFontSize() * textScale; + float logoHeight = logoSize.y; + + // Calculate vertical offset to center align logo with text + float verticalOffset = (fontHeight - logoHeight) * 0.5f; + + // Position cursor for logo with vertical alignment + ImGui::SetCursorPos(ImVec2(startPos.x, startPos.y + verticalOffset)); + + // Render logo + ImGui::Image(logoTexture, logoSize); + ImGui::SameLine(); + + // Reset cursor for text with proper vertical alignment + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX(), startPos.y)); + // Use windowed font scale for sharper text + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::SetWindowFontScale(textScale); + + // Render text aligned to pixel grid for sharpness + ImGui::Text("%s", text); + // Restore style + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleVar(); + + // Calculate and return the total rendered size + ImVec2 endPos = ImGui::GetCursorPos(); + return ImVec2(endPos.x - startPos.x, endPos.y - startPos.y); + } // StyledButtonWrapper implementation StyledButtonWrapper::StyledButtonWrapper(const ImVec4& normalColor, const ImVec4& hoveredColor, const ImVec4& activeColor) : m_pushedStyles(0) @@ -81,9 +326,6 @@ namespace Util ImGui::TextWrapped("%s", description); ImGui::Spacing(); } - - // Note: For this simplified version, we don't use TreeNode - // The sections are always expanded in FeatureIssues UI } SectionWrapper::~SectionWrapper() diff --git a/src/Utils/UI.h b/src/Utils/UI.h index bb74ecd8f8..e75088e05a 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1,9 +1,16 @@ #pragma once - #include +// Forward declarations +struct ID3D11Device; +struct ID3D11ShaderResourceView; +struct ImVec2; +class Menu; + namespace Util { + // Text rendering constants + constexpr float DefaultHeaderTextScale = 1.5f; // Larger scale for header text to improve readability /** * Usage: @@ -110,6 +117,19 @@ namespace Util bool PercentageSlider(const char* label, float* data, float lb = 0.f, float ub = 100.f, const char* format = "%.1f %%"); ImVec2 GetNativeViewportSizeScaled(float scale); + // Icon loading functions + // `device` must remain alive for the SRV lifetime. Caller owns *out_srv and must `Release()` it. + bool LoadTextureFromFile(ID3D11Device* device, + const char* filename, + ID3D11ShaderResourceView** out_srv, + ImVec2& out_size); + bool InitializeMenuIcons(Menu* menu); + + // Text rendering helpers for clearer title text + // These functions modify ImGui rendering state and should be called within ImGui context + ImVec2 DrawSharpText(const char* text, bool alignToPixelGrid = true, float scale = 1.0f); + ImVec2 DrawAlignedTextWithLogo(ID3D11ShaderResourceView* logoTexture, const ImVec2& logoSize, const char* text, float textScale = DefaultHeaderTextScale); + /** * Draws a custom styled collapsible category header with lines extending from both sides * @param categoryName The name of the category to display diff --git a/vcpkg.json b/vcpkg.json index aaef2c7c4a..a4d6ddbbcd 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -23,6 +23,7 @@ "magic-enum", "nlohmann-json", "pystring", + "stb", "tracy", "unordered-dense", "xbyak"