From b8517f3f827d35eb2b31f51998bd6383d5ac2c28 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 14 Jul 2025 21:58:34 +1000 Subject: [PATCH 1/6] search system --- src/Feature.h | 10 ++++ src/Menu.cpp | 142 +++++++++++++++++++++++++++++++++++++++++++++----- src/Menu.h | 18 ++++++- 3 files changed, 157 insertions(+), 13 deletions(-) diff --git a/src/Feature.h b/src/Feature.h index 16c8a201f3..931fa7ec15 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -4,6 +4,16 @@ struct Feature { + // For global settings search + struct SettingSearchEntry { + std::string label; + std::string description; + std::function focusCallback; // Called to focus/highlight this setting in the UI + std::string featureName; // For display context + }; + // Override in features to expose settings for search + virtual std::vector GetSettingsSearchEntries() { return {}; } + // Nexus Mods base URL for Skyrim Special Edition static constexpr std::string_view NEXUS_BASE_URL = "https://www.nexusmods.com/skyrimspecialedition/mods/"; bool loaded = false; diff --git a/src/Menu.cpp b/src/Menu.cpp index abce9b6ad1..7c488999cd 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -296,6 +296,74 @@ void Menu::Init() initialized = true; } +void Menu::DrawFeatureSearchBar() { + ImGui::PushID("FeatureSearchBar"); + + float iconSize = 20.0f; + float iconSpace = iconSize + 14.0f; + + // Get the current cursor position and available width + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + float availableWidth = ImGui::GetContentRegionAvail().x; + float frameHeight = ImGui::GetFrameHeight(); + + // 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); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); + + // Draw the input field + ImGui::SetNextItemWidth(availableWidth); + ImGui::InputTextWithHint("##feature_search", "Search Features...", &featureSearch); + // Draw a simple search icon (magnifying glass shape) + ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + 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); + + // Draw circle + drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + + // Draw handle + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(5); + ImGui::PopID(); +} + + +bool Menu::FeatureMatchesSearch(Feature* feat) const { + if (featureSearch.empty()) return true; + std::string s = feat->GetShortName(); + std::string q = featureSearch; + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + std::transform(q.begin(), q.end(), q.begin(), ::tolower); + return s.find(q) != std::string::npos; +} + +bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const { + if (settingsSearch.empty()) return true; + std::string q = settingsSearch; + std::transform(q.begin(), q.end(), q.begin(), ::tolower); + std::string l = label, d = description; + std::transform(l.begin(), l.end(), l.begin(), ::tolower); + std::transform(d.begin(), d.end(), d.begin(), ::tolower); + return l.find(q) != std::string::npos || d.find(q) != std::string::npos; +} + void Menu::DrawSettings() { if (focusChanged) { @@ -663,10 +731,8 @@ void Menu::DrawSettings() float footer_height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y * 3 + 3.0f; // text + separator ImGui::BeginChild("Menus Table", ImVec2(0, -footer_height)); - if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { - ImGui::TableSetupColumn("##ListOfMenus", 0, 2); - ImGui::TableSetupColumn("##MenuConfig", 0, 8); + // Move all declarations before the table setup static size_t selectedMenu = 0; // some type erasure bs for virtual-free polymorphism struct BuiltInMenu { @@ -1002,12 +1068,21 @@ void Menu::DrawSettings() } }; + // Build the menu list auto& featureList = Feature::GetFeatureList(); auto sortedFeatureList{ featureList }; // need a copy so the load order is not lost std::ranges::sort(sortedFeatureList, [](Feature* a, Feature* b) { return a->GetName() < b->GetName(); }); + // Filter features by search string + if (!featureSearch.empty()) { + auto pred = [this](Feature* feat) { return FeatureMatchesSearch(feat); }; + auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), + [pred](Feature* feat) { return !pred(feat); }); + sortedFeatureList.erase(it, sortedFeatureList.end()); + } + auto menuList = std::vector{ BuiltInMenu{ "General", [&]() { DrawGeneralSettings(); } }, BuiltInMenu{ "Advanced", [&]() { DrawAdvancedSettings(); } }, @@ -1099,19 +1174,62 @@ void Menu::DrawSettings() pendingFeatureSelection.clear(); // Clear after processing } - ImGui::TableNextColumn(); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); - if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { - ImGui::PopStyleVar(); - ImGui::PopStyleColor(); - for (size_t i = 0; i < menuList.size(); i++) { - std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); - } + // Now create the table with the declared variables available + if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("##ListOfMenus", 0, 2); + ImGui::TableSetupColumn("##MenuConfig", 0, 8); + + ImGui::TableNextColumn(); + // Draw the feature list + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); + if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { + + // Find where built-in menus end (General, Advanced, Display) + size_t builtInMenuCount = 0; + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + builtInMenuCount++; + } + } + } + + // First render the built-in menus (General, Advanced, Display) + size_t renderedBuiltIns = 0; + for (size_t i = 0; i < menuList.size() && renderedBuiltIns < 3; i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + renderedBuiltIns++; + } + } + } + + // Add Features header and search bar after built-in settings + Util::DrawSectionHeader("Features", true); + DrawFeatureSearchBar(); + + // Then render the rest (features and categories, but skip already rendered built-ins) + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + continue; // Skip, already rendered + } + } + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + } + ImGui::EndListBox(); } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(0, 8)); // spacing if (selectedMenu < menuList.size()) { std::visit(DrawMenuVisitor{}, menuList[selectedMenu]); diff --git a/src/Menu.h b/src/Menu.h index 772fa3492b..7ed9fc7596 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -1,9 +1,12 @@ -#pragma once +#pragma once #include "Feature.h" #include "Utils/Serialize.h" #include #include +#include + +using json = nlohmann::json; class Menu { @@ -23,6 +26,10 @@ class Menu void Init(); void DrawSettings(); + + // Search bar state + std::string featureSearch; // For left pane feature search + std::string settingsSearch; // For top-right settings search void DrawOverlay(); void DrawWeatherDetailsWindow(); @@ -54,6 +61,7 @@ class Menu UIIcon clearCache; UIIcon clearDiskCache; UIIcon logo; // New logo icon + UIIcon search; // Search icon for search bars } uiIcons; struct ThemeSettings @@ -205,6 +213,14 @@ class Menu void DrawFooter(); void BuildCategoryCounts(); + // Helper for feature search bar (left pane) + void DrawFeatureSearchBar(); + // Helper for settings search bar (top right) + void DrawSettingsSearchBar(); + // Helper to filter features by search + bool FeatureMatchesSearch(Feature* feat) const; + bool SettingMatchesSearch(const std::string& label, const std::string& description) const; + class CharEvent : public RE::InputEvent { public: From c8700e7c32f08a39d74f12eb4b1e2eb582cc41c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:02:32 +0000 Subject: [PATCH 2/6] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commit.?= =?UTF-8?q?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/Feature.h | 7 +- src/Menu.cpp | 875 +++++++++++++++++++++++++------------------------- src/Menu.h | 10 +- 3 files changed, 448 insertions(+), 444 deletions(-) diff --git a/src/Feature.h b/src/Feature.h index 931fa7ec15..84aad69f16 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -5,11 +5,12 @@ struct Feature { // For global settings search - struct SettingSearchEntry { + struct SettingSearchEntry + { std::string label; std::string description; - std::function focusCallback; // Called to focus/highlight this setting in the UI - std::string featureName; // For display context + std::function focusCallback; // Called to focus/highlight this setting in the UI + std::string featureName; // For display context }; // Override in features to expose settings for search virtual std::vector GetSettingsSearchEntries() { return {}; } diff --git a/src/Menu.cpp b/src/Menu.cpp index 7c488999cd..421df489ef 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -296,57 +296,59 @@ void Menu::Init() initialized = true; } -void Menu::DrawFeatureSearchBar() { +void Menu::DrawFeatureSearchBar() +{ ImGui::PushID("FeatureSearchBar"); - + float iconSize = 20.0f; float iconSpace = iconSize + 14.0f; - + // Get the current cursor position and available width ImVec2 cursorPos = ImGui::GetCursorScreenPos(); float availableWidth = ImGui::GetContentRegionAvail().x; float frameHeight = ImGui::GetFrameHeight(); - + // 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); - + ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); - + // Draw the input field ImGui::SetNextItemWidth(availableWidth); ImGui::InputTextWithHint("##feature_search", "Search Features...", &featureSearch); - // Draw a simple search icon (magnifying glass shape) - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - 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); - - // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - - // Draw handle - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); - + // Draw a simple search icon (magnifying glass shape) + ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + 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); + + // Draw circle + drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + + // Draw handle + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + ImGui::PopStyleVar(2); ImGui::PopStyleColor(5); ImGui::PopID(); } - -bool Menu::FeatureMatchesSearch(Feature* feat) const { - if (featureSearch.empty()) return true; +bool Menu::FeatureMatchesSearch(Feature* feat) const +{ + if (featureSearch.empty()) + return true; std::string s = feat->GetShortName(); std::string q = featureSearch; std::transform(s.begin(), s.end(), s.begin(), ::tolower); @@ -354,8 +356,10 @@ bool Menu::FeatureMatchesSearch(Feature* feat) const { return s.find(q) != std::string::npos; } -bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const { - if (settingsSearch.empty()) return true; +bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const +{ + if (settingsSearch.empty()) + return true; std::string q = settingsSearch; std::transform(q.begin(), q.end(), q.begin(), ::tolower); std::string l = label, d = description; @@ -732,504 +736,503 @@ void Menu::DrawSettings() ImGui::BeginChild("Menus Table", ImVec2(0, -footer_height)); - // Move all declarations before the table setup - static size_t selectedMenu = 0; // some type erasure bs for virtual-free polymorphism - struct BuiltInMenu - { - std::string name; - std::function func; - }; - struct CategoryHeader - { - std::string name; - }; - using MenuFuncInfo = std::variant; - - // Static storage for category expansion states - static std::map categoryExpansionStates; + // Move all declarations before the table setup + static size_t selectedMenu = 0; // some type erasure bs for virtual-free polymorphism + struct BuiltInMenu + { + std::string name; + std::function func; + }; + struct CategoryHeader + { + std::string name; + }; + using MenuFuncInfo = std::variant; - struct ListMenuVisitor - { - size_t listId; - size_t& selectedMenuRef; + // Static storage for category expansion states + static std::map categoryExpansionStates; - void operator()(const BuiltInMenu& menu) - { - // Use error color for Feature Issues menu item - bool isFeatureIssues = (menu.name == "Feature Issues"); - if (isFeatureIssues) { - auto& themeSettings = globals::menu->settings.Theme; - ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); - } - - if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) - selectedMenuRef = listId; + struct ListMenuVisitor + { + size_t listId; + size_t& selectedMenuRef; - if (isFeatureIssues) { - ImGui::PopStyleColor(); - } - } - void operator()(const std::string& label) - { - // Style "Unloaded Features" to match category headers - if (label == "Unloaded Features") { - Util::DrawSectionHeader(label.c_str(), true); - } else { - // Use default separator text for other labels - ImGui::SeparatorText(label.c_str()); - } + void operator()(const BuiltInMenu& menu) + { + // Use error color for Feature Issues menu item + bool isFeatureIssues = (menu.name == "Feature Issues"); + if (isFeatureIssues) { + auto& themeSettings = globals::menu->settings.Theme; + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); } - void operator()(const CategoryHeader& header) - { - // Get expansion state from static map - bool isExpanded = categoryExpansionStates[header.name]; - // Draw category header with custom styling using util:UI function - int count = categoryCounts[std::string(header.name)]; - Util::DrawCategoryHeader(header.name.c_str(), isExpanded, count); + if (ImGui::Selectable(fmt::format(" {} ", menu.name).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) + selectedMenuRef = listId; - // Update expansion state - categoryExpansionStates[header.name] = isExpanded; + if (isFeatureIssues) { + ImGui::PopStyleColor(); } - void operator()(Feature* feat) - { - const auto featureName = feat->GetShortName(); - bool isDisabled = globals::state->IsFeatureDisabled(featureName); - bool isLoaded = feat->loaded; - bool hasFailedMessage = !feat->failedLoadedMessage.empty(); - auto& themeSettings = globals::menu->settings.Theme; + } + void operator()(const std::string& label) + { + // Style "Unloaded Features" to match category headers + if (label == "Unloaded Features") { + Util::DrawSectionHeader(label.c_str(), true); + } else { + // Use default separator text for other labels + ImGui::SeparatorText(label.c_str()); + } + } + void operator()(const CategoryHeader& header) + { + // Get expansion state from static map + bool isExpanded = categoryExpansionStates[header.name]; - ImVec4 textColor; + // Draw category header with custom styling using util:UI function + int count = categoryCounts[std::string(header.name)]; + Util::DrawCategoryHeader(header.name.c_str(), isExpanded, count); - // Determine the text color based on the state - if (isDisabled) { + // Update expansion state + categoryExpansionStates[header.name] = isExpanded; + } + void operator()(Feature* feat) + { + const auto featureName = feat->GetShortName(); + bool isDisabled = globals::state->IsFeatureDisabled(featureName); + bool isLoaded = feat->loaded; + bool hasFailedMessage = !feat->failedLoadedMessage.empty(); + auto& themeSettings = globals::menu->settings.Theme; + + ImVec4 textColor; + + // Determine the text color based on the state + if (isDisabled) { + textColor = themeSettings.StatusPalette.Disable; + } else if (isLoaded) { + textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + } else if (hasFailedMessage) { + textColor = feat->version.empty() ? themeSettings.StatusPalette.Disable : themeSettings.StatusPalette.Error; + } else { + // No failed message but not loaded - check if INI file exists + if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { + // INI file missing - treat as missing feature (grey) textColor = themeSettings.StatusPalette.Disable; - } else if (isLoaded) { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); - } else if (hasFailedMessage) { - textColor = feat->version.empty() ? themeSettings.StatusPalette.Disable : themeSettings.StatusPalette.Error; } else { - // No failed message but not loaded - check if INI file exists - if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { - // INI file missing - treat as missing feature (grey) - textColor = themeSettings.StatusPalette.Disable; - } else { - // INI file exists but feature not loaded - truly pending restart (green) - textColor = themeSettings.StatusPalette.RestartNeeded; - } + // INI file exists but feature not loaded - truly pending restart (green) + textColor = themeSettings.StatusPalette.RestartNeeded; } + } - // Set text color - ImGui::PushStyleColor(ImGuiCol_Text, textColor); + // Set text color + ImGui::PushStyleColor(ImGuiCol_Text, textColor); - // Create selectable item - if (ImGui::Selectable(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { - selectedMenuRef = listId; - } + // Create selectable item + if (ImGui::Selectable(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { + selectedMenuRef = listId; + } - // Restore original text color - ImGui::PopStyleColor(); + // Restore original text color + ImGui::PopStyleColor(); - // Display version if loaded - if (isLoaded) { - ImGui::SameLine(); - std::string formattedVersion = feat->version; - std::replace(formattedVersion.begin(), formattedVersion.end(), '-', '.'); - ImGui::TextDisabled(fmt::format("({})", formattedVersion).c_str()); - } + // Display version if loaded + if (isLoaded) { + ImGui::SameLine(); + std::string formattedVersion = feat->version; + std::replace(formattedVersion.begin(), formattedVersion.end(), '-', '.'); + ImGui::TextDisabled(fmt::format("({})", formattedVersion).c_str()); } - }; - struct DrawMenuVisitor + } + }; + struct DrawMenuVisitor + { + void operator()(const BuiltInMenu& menu) { - void operator()(const BuiltInMenu& menu) - { - if (ImGui::BeginChild("##FeatureConfigFrame", { 0, 0 }, true)) { - menu.func(); - } - ImGui::EndChild(); + if (ImGui::BeginChild("##FeatureConfigFrame", { 0, 0 }, true)) { + menu.func(); } - void operator()(const std::string&) - { - // std::unreachable() from c++23 - // you are not supposed to have selected a label! - } - void operator()(const CategoryHeader&) - { - // Category headers are not selectable in the right panel - ImGui::TextDisabled("Please select a feature from the left."); + ImGui::EndChild(); + } + void operator()(const std::string&) + { + // std::unreachable() from c++23 + // you are not supposed to have selected a label! + } + void operator()(const CategoryHeader&) + { + // Category headers are not selectable in the right panel + ImGui::TextDisabled("Please select a feature from the left."); + } + void operator()(Feature* feat) + { + const auto featureName = feat->GetShortName(); + bool isDisabled = globals::state->IsFeatureDisabled(featureName); + bool isLoaded = feat->loaded; + bool hasFailedMessage = !feat->failedLoadedMessage.empty(); + auto& themeSettings = globals::menu->settings.Theme; + // Calculate button widths based on text content + const char* bootButtonText = isDisabled ? "Enable at Boot" : "Disable at Boot"; + const char* defaultsButtonText = "Restore Defaults"; + + float buttonPadding = 16.0f; + float buttonSpacing = 8.0f; + float bootButtonWidth = ImGui::CalcTextSize(bootButtonText).x + buttonPadding; + float defaultsButtonWidth = ImGui::CalcTextSize(defaultsButtonText).x + buttonPadding; + + float totalButtonWidth = bootButtonWidth; + if (!isDisabled && isLoaded) { + totalButtonWidth += defaultsButtonWidth + buttonSpacing; } - void operator()(Feature* feat) - { - const auto featureName = feat->GetShortName(); - bool isDisabled = globals::state->IsFeatureDisabled(featureName); - bool isLoaded = feat->loaded; - bool hasFailedMessage = !feat->failedLoadedMessage.empty(); - auto& themeSettings = globals::menu->settings.Theme; - // Calculate button widths based on text content - const char* bootButtonText = isDisabled ? "Enable at Boot" : "Disable at Boot"; - const char* defaultsButtonText = "Restore Defaults"; - - float buttonPadding = 16.0f; - float buttonSpacing = 8.0f; - float bootButtonWidth = ImGui::CalcTextSize(bootButtonText).x + buttonPadding; - float defaultsButtonWidth = ImGui::CalcTextSize(defaultsButtonText).x + buttonPadding; - - float totalButtonWidth = bootButtonWidth; - if (!isDisabled && isLoaded) { - totalButtonWidth += defaultsButtonWidth + buttonSpacing; - } - if (ImGui::BeginTabBar("##FeatureTabs", ImGuiTabBarFlags_Reorderable)) { - // Draw standard tabs - if (ImGui::BeginTabItem("Settings")) { - if (ImGui::BeginChild("##FeatureSettingsFrame", { 0, 0 }, true)) { - // Feature-specific settings section - ImGui::SeparatorText("Feature Settings"); - if (isDisabled) { - // Show disabled message - ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); - ImGui::Spacing(); - ImGui::Text("Enable the feature above to access its configuration options."); - } else { - if (isLoaded) { - // Check if the feature has any settings by monitoring cursor position (if the feature draws settings, the imgui cursor position will change) - ImVec2 cursorPosBefore = ImGui::GetCursorPos(); + if (ImGui::BeginTabBar("##FeatureTabs", ImGuiTabBarFlags_Reorderable)) { + // Draw standard tabs + if (ImGui::BeginTabItem("Settings")) { + if (ImGui::BeginChild("##FeatureSettingsFrame", { 0, 0 }, true)) { + // Feature-specific settings section + ImGui::SeparatorText("Feature Settings"); + if (isDisabled) { + // Show disabled message + ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); + ImGui::Spacing(); + ImGui::Text("Enable the feature above to access its configuration options."); + } else { + if (isLoaded) { + // Check if the feature has any settings by monitoring cursor position (if the feature draws settings, the imgui cursor position will change) + ImVec2 cursorPosBefore = ImGui::GetCursorPos(); - feat->DrawSettings(); + feat->DrawSettings(); - ImVec2 cursorPosAfter = ImGui::GetCursorPos(); + ImVec2 cursorPosAfter = ImGui::GetCursorPos(); - // If cursor position hasn't changed significantly, no visible settings were drawn - const float epsilon = 0.1f; // Simple check to ensure we don't trigger on minor cursor movements / weird imgui math - bool cursorMoved = (std::abs(cursorPosAfter.x - cursorPosBefore.x) > epsilon || - std::abs(cursorPosAfter.y - cursorPosBefore.y) > epsilon); - if (!cursorMoved) { - ImGui::TextColored(themeSettings.StatusPalette.Disable, "There are no settings available for this feature."); - } + // If cursor position hasn't changed significantly, no visible settings were drawn + const float epsilon = 0.1f; // Simple check to ensure we don't trigger on minor cursor movements / weird imgui math + bool cursorMoved = (std::abs(cursorPosAfter.x - cursorPosBefore.x) > epsilon || + std::abs(cursorPosAfter.y - cursorPosBefore.y) > epsilon); + if (!cursorMoved) { + ImGui::TextColored(themeSettings.StatusPalette.Disable, "There are no settings available for this feature."); + } + } else { + // Check if feature is obsolete first - always show error for obsolete features + if (FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { + // Obsolete feature - show detailed unloaded UI with error info + feat->DrawUnloadedUI(); + } else if (std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { + // INI file exists - show simple pending restart message + ImGui::Text("This feature will be available after restart."); } else { - // Check if feature is obsolete first - always show error for obsolete features - if (FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { - // Obsolete feature - show detailed unloaded UI with error info - feat->DrawUnloadedUI(); - } else if (std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { - // INI file exists - show simple pending restart message - ImGui::Text("This feature will be available after restart."); - } else { - // INI file missing - show detailed unloaded UI with installation info - feat->DrawUnloadedUI(); - // Add download link if available - if (!feat->GetFeatureModLink().empty()) { - ImGui::Spacing(); - const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); - if (ImGui::Selectable(downloadText.c_str())) { - ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Download the feature from the mod page."); - } + // INI file missing - show detailed unloaded UI with installation info + feat->DrawUnloadedUI(); + // Add download link if available + if (!feat->GetFeatureModLink().empty()) { + ImGui::Spacing(); + const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); + if (ImGui::Selectable(downloadText.c_str())) { + ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Download the feature from the mod page."); } } } } + } - // Error Messages (Not for obsolete features as this is already covered by DrawUnloadedUI) - if (hasFailedMessage && feat->DrawFailLoadMessage() && !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { - ImGui::Spacing(); - ImGui::SeparatorText("Error"); - ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); - } + // Error Messages (Not for obsolete features as this is already covered by DrawUnloadedUI) + if (hasFailedMessage && feat->DrawFailLoadMessage() && !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { + ImGui::Spacing(); + ImGui::SeparatorText("Error"); + ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); } - ImGui::EndChild(); - ImGui::EndTabItem(); } + ImGui::EndChild(); + ImGui::EndTabItem(); + } - // About Tab - Information about the feature and how it works - if (ImGui::BeginTabItem("About")) { - if (ImGui::BeginChild("##FeatureAboutFrame", { 0, 0 }, true)) { - // Status Section - ImGui::SeparatorText("Status"); - - ImVec4 statusColor; - const char* statusText; - if (isDisabled) { - statusColor = themeSettings.StatusPalette.Disable; - statusText = "Disabled at boot."; - } else if (hasFailedMessage) { + // About Tab - Information about the feature and how it works + if (ImGui::BeginTabItem("About")) { + if (ImGui::BeginChild("##FeatureAboutFrame", { 0, 0 }, true)) { + // Status Section + ImGui::SeparatorText("Status"); + + ImVec4 statusColor; + const char* statusText; + if (isDisabled) { + statusColor = themeSettings.StatusPalette.Disable; + statusText = "Disabled at boot."; + } else if (hasFailedMessage) { + statusColor = themeSettings.StatusPalette.Error; + statusText = "Failed to load."; + } else if (!isLoaded) { + // Check if INI file exists to determine actual status + if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { + // INI file missing - feature not installed statusColor = themeSettings.StatusPalette.Error; - statusText = "Failed to load."; - } else if (!isLoaded) { - // Check if INI file exists to determine actual status - if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { - // INI file missing - feature not installed - statusColor = themeSettings.StatusPalette.Error; - statusText = "Not installed."; - } else { - // INI file exists but feature not loaded - truly pending restart - statusColor = themeSettings.StatusPalette.RestartNeeded; - statusText = "Pending restart."; - } + statusText = "Not installed."; } else { - statusColor = themeSettings.StatusPalette.SuccessColor; - statusText = "Active."; + // INI file exists but feature not loaded - truly pending restart + statusColor = themeSettings.StatusPalette.RestartNeeded; + statusText = "Pending restart."; } + } else { + statusColor = themeSettings.StatusPalette.SuccessColor; + statusText = "Active."; + } - ImGui::TextColored(statusColor, "Current State: %s", statusText); + ImGui::TextColored(statusColor, "Current State: %s", statusText); - // Feature Info - Description and key features - if (isLoaded) { - auto [description, keyFeatures] = feat->GetFeatureSummary(); - if (!description.empty()) { - ImGui::Spacing(); - ImGui::SeparatorText("Description"); - ImGui::TextWrapped("%s", description.c_str()); + // Feature Info - Description and key features + if (isLoaded) { + auto [description, keyFeatures] = feat->GetFeatureSummary(); + if (!description.empty()) { + ImGui::Spacing(); + ImGui::SeparatorText("Description"); + ImGui::TextWrapped("%s", description.c_str()); - if (!keyFeatures.empty()) { - ImGui::Spacing(); - ImGui::SeparatorText("Key Features"); - for (const auto& feature : keyFeatures) { - ImGui::BulletText("%s", feature.c_str()); - } + if (!keyFeatures.empty()) { + ImGui::Spacing(); + ImGui::SeparatorText("Key Features"); + for (const auto& feature : keyFeatures) { + ImGui::BulletText("%s", feature.c_str()); } } + } + } else { + // For unloaded features, show basic info if available + ImGui::Spacing(); + ImGui::SeparatorText("Information"); + if (hasFailedMessage) { + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", feat->failedLoadedMessage.c_str()); } else { - // For unloaded features, show basic info if available - ImGui::Spacing(); - ImGui::SeparatorText("Information"); - if (hasFailedMessage) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", feat->failedLoadedMessage.c_str()); + // For features that are pending restart or not installed, + // the detailed information is shown in the Settings tab. + // Here we just show a simple message directing users there. + if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { + ImGui::Text("Feature installation details are available in the Settings tab."); } else { - // For features that are pending restart or not installed, - // the detailed information is shown in the Settings tab. - // Here we just show a simple message directing users there. - if (!std::filesystem::exists(Util::PathHelpers::GetFeatureIniPath(feat->GetShortName()))) { - ImGui::Text("Feature installation details are available in the Settings tab."); - } else { - // INI file exists but feature not loaded - truly pending restart - ImGui::Text("This feature is pending restart."); - } + // INI file exists but feature not loaded - truly pending restart + ImGui::Text("This feature is pending restart."); } } } - ImGui::EndChild(); - ImGui::EndTabItem(); } + ImGui::EndChild(); + ImGui::EndTabItem(); + } - // Position buttons on the right side of the tab bar - ImGui::SameLine(); - float availableSpace = ImGui::GetContentRegionAvail().x; - float rightOffset = availableSpace - totalButtonWidth; - if (rightOffset > 0) { - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightOffset); - } + // Position buttons on the right side of the tab bar + ImGui::SameLine(); + float availableSpace = ImGui::GetContentRegionAvail().x; + float rightOffset = availableSpace - totalButtonWidth; + if (rightOffset > 0) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rightOffset); + } - // Disable/Enable at boot button - ImVec4 textColor; - if (isDisabled) { - textColor = themeSettings.StatusPalette.Disable; - } else if (hasFailedMessage) { - textColor = themeSettings.StatusPalette.Error; - } else { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); - } + // Disable/Enable at boot button + ImVec4 textColor; + if (isDisabled) { + textColor = themeSettings.StatusPalette.Disable; + } else if (hasFailedMessage) { + textColor = themeSettings.StatusPalette.Error; + } else { + textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + } + + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + if (ImGui::Button(bootButtonText, { bootButtonWidth, 0 })) { + bool newState = feat->ToggleAtBootSetting(); + logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); + } + 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"); + } - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - if (ImGui::Button(bootButtonText, { bootButtonWidth, 0 })) { - bool newState = feat->ToggleAtBootSetting(); - logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); + // Restore Defaults button (when feature is not disabled and is loaded) + if (!isDisabled && isLoaded) { + ImGui::SameLine(); + if (ImGui::Button(defaultsButtonText, { defaultsButtonWidth, 0 })) { + feat->RestoreDefaultSettings(); } - 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"); - } - - // Restore Defaults button (when feature is not disabled and is loaded) - if (!isDisabled && isLoaded) { - ImGui::SameLine(); - if (ImGui::Button(defaultsButtonText, { defaultsButtonWidth, 0 })) { - feat->RestoreDefaultSettings(); - } - - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Restores the feature's settings back to their default values. " - "You will still need to Save Settings to make these changes permanent."); - } + "Restores the feature's settings back to their default values. " + "You will still need to Save Settings to make these changes permanent."); } } - ImGui::EndTabBar(); } - }; + ImGui::EndTabBar(); + } + }; - // Build the menu list - auto& featureList = Feature::GetFeatureList(); - auto sortedFeatureList{ featureList }; // need a copy so the load order is not lost - std::ranges::sort(sortedFeatureList, [](Feature* a, Feature* b) { - return a->GetName() < b->GetName(); - }); + // Build the menu list + auto& featureList = Feature::GetFeatureList(); + auto sortedFeatureList{ featureList }; // need a copy so the load order is not lost + std::ranges::sort(sortedFeatureList, [](Feature* a, Feature* b) { + return a->GetName() < b->GetName(); + }); + + // Filter features by search string + if (!featureSearch.empty()) { + auto pred = [this](Feature* feat) { return FeatureMatchesSearch(feat); }; + auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), + [pred](Feature* feat) { return !pred(feat); }); + sortedFeatureList.erase(it, sortedFeatureList.end()); + } - // Filter features by search string - if (!featureSearch.empty()) { - auto pred = [this](Feature* feat) { return FeatureMatchesSearch(feat); }; - auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), - [pred](Feature* feat) { return !pred(feat); }); - sortedFeatureList.erase(it, sortedFeatureList.end()); + auto menuList = std::vector{ + BuiltInMenu{ "General", [&]() { DrawGeneralSettings(); } }, + BuiltInMenu{ "Advanced", [&]() { DrawAdvancedSettings(); } }, + BuiltInMenu{ "Display", [&]() { DrawDisplaySettings(); } } + }; // NOTE: The menu list is rebuilt every frame, so category expansion states + // persist correctly. This is acceptable since the list is small and built + // infrequently, but could be optimized if performance becomes an issue. + + // Group features by category + std::map> categorizedFeatures; + for (Feature* feat : sortedFeatureList) { + if (feat->IsInMenu() && feat->loaded) { + std::string category(feat->GetCategory()); + categorizedFeatures[category].push_back(feat); } + } + + // Sort features within each category + for (auto& [category, features] : categorizedFeatures) { + std::ranges::sort(features, [](Feature* a, Feature* b) { + return a->GetName() < b->GetName(); + }); + } - auto menuList = std::vector{ - BuiltInMenu{ "General", [&]() { DrawGeneralSettings(); } }, - BuiltInMenu{ "Advanced", [&]() { DrawAdvancedSettings(); } }, - BuiltInMenu{ "Display", [&]() { DrawDisplaySettings(); } } - }; // NOTE: The menu list is rebuilt every frame, so category expansion states - // persist correctly. This is acceptable since the list is small and built - // infrequently, but could be optimized if performance becomes an issue. - - // Group features by category - std::map> categorizedFeatures; - for (Feature* feat : sortedFeatureList) { - if (feat->IsInMenu() && feat->loaded) { - std::string category(feat->GetCategory()); - categorizedFeatures[category].push_back(feat); + // Define category order + std::vector categoryOrder = { "Characters", "Grass", "Lighting", "Sky", "Landscape & Textures", "Water", "Other" }; + // Add categorized features to menu with collapsible headers + for (const std::string& category : categoryOrder) { + if (categorizedFeatures.find(category) != categorizedFeatures.end() && !categorizedFeatures[category].empty()) { + // Initialize expansion state if not exists + if (categoryExpansionStates.find(category) == categoryExpansionStates.end()) { + categoryExpansionStates[category] = true; // Default to expanded } - } - // Sort features within each category - for (auto& [category, features] : categorizedFeatures) { - std::ranges::sort(features, [](Feature* a, Feature* b) { - return a->GetName() < b->GetName(); - }); + // Add category header + menuList.push_back(CategoryHeader{ category }); + + // Add features only if category is expanded + if (categoryExpansionStates[category]) { + std::ranges::copy(categorizedFeatures[category], std::back_inserter(menuList)); + } } + } - // Define category order - std::vector categoryOrder = { "Characters", "Grass", "Lighting", "Sky", "Landscape & Textures", "Water", "Other" }; - // Add categorized features to menu with collapsible headers - for (const std::string& category : categoryOrder) { - if (categorizedFeatures.find(category) != categorizedFeatures.end() && !categorizedFeatures[category].empty()) { - // Initialize expansion state if not exists - if (categoryExpansionStates.find(category) == categoryExpansionStates.end()) { - categoryExpansionStates[category] = true; // Default to expanded - } + // Add any categories not in the predefined order + for (const auto& [category, features] : categorizedFeatures) { + if (std::find(categoryOrder.begin(), categoryOrder.end(), category) == categoryOrder.end() && !features.empty()) { + // Initialize expansion state if not exists + if (categoryExpansionStates.find(category) == categoryExpansionStates.end()) { + categoryExpansionStates[category] = true; // Default to expanded + } - // Add category header - menuList.push_back(CategoryHeader{ category }); + // Add category header + menuList.push_back(CategoryHeader{ category }); - // Add features only if category is expanded - if (categoryExpansionStates[category]) { - std::ranges::copy(categorizedFeatures[category], std::back_inserter(menuList)); - } + // Add features only if category is expanded + if (categoryExpansionStates[category]) { + std::ranges::copy(features, std::back_inserter(menuList)); } } + } - // Add any categories not in the predefined order - for (const auto& [category, features] : categorizedFeatures) { - if (std::find(categoryOrder.begin(), categoryOrder.end(), category) == categoryOrder.end() && !features.empty()) { - // Initialize expansion state if not exists - if (categoryExpansionStates.find(category) == categoryExpansionStates.end()) { - categoryExpansionStates[category] = true; // Default to expanded - } - - // Add category header - menuList.push_back(CategoryHeader{ category }); + auto unloadedFeatures = sortedFeatureList | std::ranges::views::filter([](Feature* feat) { + return !feat->loaded && feat->IsInMenu() && (!FeatureIssues::IsObsoleteFeature(feat->GetShortName()) || globals::state->IsDeveloperMode()); + }); + if (std::ranges::distance(unloadedFeatures) != 0) { + menuList.push_back("Unloaded Features"s); + std::ranges::copy(unloadedFeatures, std::back_inserter(menuList)); + } + // Add top section for feature issues (rejected features, obsolete info, etc.) + if (FeatureIssues::HasFeatureIssues()) { + menuList.insert(menuList.begin(), BuiltInMenu{ "Feature Issues", []() { + FeatureIssues::DrawFeatureIssuesUI(); + } }); + } - // Add features only if category is expanded - if (categoryExpansionStates[category]) { - std::ranges::copy(features, std::back_inserter(menuList)); + // Handle pending feature selection + if (!pendingFeatureSelection.empty()) { + for (size_t i = 0; i < menuList.size(); ++i) { + if (std::holds_alternative(menuList[i])) { + Feature* feature = std::get(menuList[i]); + if (feature->GetShortName() == pendingFeatureSelection) { + selectedMenu = i; + logger::info("Navigated to {} feature menu", pendingFeatureSelection); + break; } } } + pendingFeatureSelection.clear(); // Clear after processing + } - auto unloadedFeatures = sortedFeatureList | std::ranges::views::filter([](Feature* feat) { - return !feat->loaded && feat->IsInMenu() && (!FeatureIssues::IsObsoleteFeature(feat->GetShortName()) || globals::state->IsDeveloperMode()); - }); - if (std::ranges::distance(unloadedFeatures) != 0) { - menuList.push_back("Unloaded Features"s); - std::ranges::copy(unloadedFeatures, std::back_inserter(menuList)); - } - // Add top section for feature issues (rejected features, obsolete info, etc.) - if (FeatureIssues::HasFeatureIssues()) { - menuList.insert(menuList.begin(), BuiltInMenu{ "Feature Issues", []() { - FeatureIssues::DrawFeatureIssuesUI(); - } }); - } + // Now create the table with the declared variables available + if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("##ListOfMenus", 0, 2); + ImGui::TableSetupColumn("##MenuConfig", 0, 8); - // Handle pending feature selection - if (!pendingFeatureSelection.empty()) { - for (size_t i = 0; i < menuList.size(); ++i) { - if (std::holds_alternative(menuList[i])) { - Feature* feature = std::get(menuList[i]); - if (feature->GetShortName() == pendingFeatureSelection) { - selectedMenu = i; - logger::info("Navigated to {} feature menu", pendingFeatureSelection); - break; + ImGui::TableNextColumn(); + // Draw the feature list + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); + if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { + // Find where built-in menus end (General, Advanced, Display) + size_t builtInMenuCount = 0; + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + builtInMenuCount++; } } } - pendingFeatureSelection.clear(); // Clear after processing - } - // Now create the table with the declared variables available - if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { - ImGui::TableSetupColumn("##ListOfMenus", 0, 2); - ImGui::TableSetupColumn("##MenuConfig", 0, 8); - - ImGui::TableNextColumn(); - // Draw the feature list - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); - if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { - - // Find where built-in menus end (General, Advanced, Display) - size_t builtInMenuCount = 0; - for (size_t i = 0; i < menuList.size(); i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - builtInMenuCount++; - } - } - } - - // First render the built-in menus (General, Advanced, Display) - size_t renderedBuiltIns = 0; - for (size_t i = 0; i < menuList.size() && renderedBuiltIns < 3; i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); - renderedBuiltIns++; - } + // First render the built-in menus (General, Advanced, Display) + size_t renderedBuiltIns = 0; + for (size_t i = 0; i < menuList.size() && renderedBuiltIns < 3; i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + renderedBuiltIns++; } } - - // Add Features header and search bar after built-in settings - Util::DrawSectionHeader("Features", true); - DrawFeatureSearchBar(); - - // Then render the rest (features and categories, but skip already rendered built-ins) - for (size_t i = 0; i < menuList.size(); i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - continue; // Skip, already rendered - } + } + + // Add Features header and search bar after built-in settings + Util::DrawSectionHeader("Features", true); + DrawFeatureSearchBar(); + + // Then render the rest (features and categories, but skip already rendered built-ins) + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + continue; // Skip, already rendered } - std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); } - + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + } + ImGui::EndListBox(); } ImGui::PopStyleVar(); ImGui::PopStyleColor(); ImGui::TableNextColumn(); - ImGui::Dummy(ImVec2(0, 8)); // spacing + ImGui::Dummy(ImVec2(0, 8)); // spacing if (selectedMenu < menuList.size()) { std::visit(DrawMenuVisitor{}, menuList[selectedMenu]); diff --git a/src/Menu.h b/src/Menu.h index 7ed9fc7596..fd1713c767 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -3,8 +3,8 @@ #include "Feature.h" #include "Utils/Serialize.h" #include -#include #include +#include using json = nlohmann::json; @@ -28,8 +28,8 @@ class Menu void DrawSettings(); // Search bar state - std::string featureSearch; // For left pane feature search - std::string settingsSearch; // For top-right settings search + std::string featureSearch; // For left pane feature search + std::string settingsSearch; // For top-right settings search void DrawOverlay(); void DrawWeatherDetailsWindow(); @@ -60,8 +60,8 @@ class Menu UIIcon loadSettings; UIIcon clearCache; UIIcon clearDiskCache; - UIIcon logo; // New logo icon - UIIcon search; // Search icon for search bars + UIIcon logo; // New logo icon + UIIcon search; // Search icon for search bars } uiIcons; struct ThemeSettings From 989dcd8198c8e86b16f0b339d5a74f1ea378e585 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 15 Jul 2025 13:05:39 +1000 Subject: [PATCH 3/6] refactor to UI.cpp/ search fixes for name --- src/Menu.cpp | 74 ++------------------------------------------- src/Menu.h | 9 ------ src/Utils/UI.cpp | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Utils/UI.h | 18 +++++++++++ 4 files changed, 99 insertions(+), 80 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 7c488999cd..83b639c2bb 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -18,6 +18,7 @@ #include "State.h" #include "Streamline.h" #include "TruePBR.h" +#include "Utils/UI.h" #include "Upscaling.h" #include "Util.h" #include "Utils/UI.h" @@ -296,74 +297,6 @@ void Menu::Init() initialized = true; } -void Menu::DrawFeatureSearchBar() { - ImGui::PushID("FeatureSearchBar"); - - float iconSize = 20.0f; - float iconSpace = iconSize + 14.0f; - - // Get the current cursor position and available width - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - float availableWidth = ImGui::GetContentRegionAvail().x; - float frameHeight = ImGui::GetFrameHeight(); - - // 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); - - ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); - - // Draw the input field - ImGui::SetNextItemWidth(availableWidth); - ImGui::InputTextWithHint("##feature_search", "Search Features...", &featureSearch); - // Draw a simple search icon (magnifying glass shape) - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - 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); - - // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - - // Draw handle - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); - - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(5); - ImGui::PopID(); -} - - -bool Menu::FeatureMatchesSearch(Feature* feat) const { - if (featureSearch.empty()) return true; - std::string s = feat->GetShortName(); - std::string q = featureSearch; - std::transform(s.begin(), s.end(), s.begin(), ::tolower); - std::transform(q.begin(), q.end(), q.begin(), ::tolower); - return s.find(q) != std::string::npos; -} - -bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const { - if (settingsSearch.empty()) return true; - std::string q = settingsSearch; - std::transform(q.begin(), q.end(), q.begin(), ::tolower); - std::string l = label, d = description; - std::transform(l.begin(), l.end(), l.begin(), ::tolower); - std::transform(d.begin(), d.end(), d.begin(), ::tolower); - return l.find(q) != std::string::npos || d.find(q) != std::string::npos; -} - void Menu::DrawSettings() { if (focusChanged) { @@ -1077,9 +1010,8 @@ void Menu::DrawSettings() // Filter features by search string if (!featureSearch.empty()) { - auto pred = [this](Feature* feat) { return FeatureMatchesSearch(feat); }; auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), - [pred](Feature* feat) { return !pred(feat); }); + [this](Feature* feat) { return !Util::FeatureMatchesSearch(feat, featureSearch); }); sortedFeatureList.erase(it, sortedFeatureList.end()); } @@ -1210,7 +1142,7 @@ void Menu::DrawSettings() // Add Features header and search bar after built-in settings Util::DrawSectionHeader("Features", true); - DrawFeatureSearchBar(); + Util::DrawFeatureSearchBar(featureSearch); // Then render the rest (features and categories, but skip already rendered built-ins) for (size_t i = 0; i < menuList.size(); i++) { diff --git a/src/Menu.h b/src/Menu.h index 7ed9fc7596..519e7e9f17 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -29,7 +29,6 @@ class Menu // Search bar state std::string featureSearch; // For left pane feature search - std::string settingsSearch; // For top-right settings search void DrawOverlay(); void DrawWeatherDetailsWindow(); @@ -213,14 +212,6 @@ class Menu void DrawFooter(); void BuildCategoryCounts(); - // Helper for feature search bar (left pane) - void DrawFeatureSearchBar(); - // Helper for settings search bar (top right) - void DrawSettingsSearchBar(); - // Helper to filter features by search - bool FeatureMatchesSearch(Feature* feat) const; - bool SettingMatchesSearch(const std::string& label, const std::string& description) const; - class CharEvent : public RE::InputEvent { public: diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index e0a871df9e..34fd298fcf 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -5,6 +5,7 @@ #include #include +#include "../Feature.h" #include "../Globals.h" #include "../Menu.h" @@ -614,4 +615,81 @@ namespace Util else return badColor; } + + bool FeatureMatchesSearch(Feature* feat, const std::string& searchQuery) + { + if (searchQuery.empty()) return true; + + // Get both short name and display name + std::string shortName = feat->GetShortName(); + std::string displayName = feat->GetName(); + std::string query = searchQuery; + + // Convert all to lowercase for case-insensitive search + std::transform(shortName.begin(), shortName.end(), shortName.begin(), ::tolower); + std::transform(displayName.begin(), displayName.end(), displayName.begin(), ::tolower); + std::transform(query.begin(), query.end(), query.begin(), ::tolower); + + // Search in both short name and display name + return shortName.find(query) != std::string::npos || + displayName.find(query) != std::string::npos; + } + + void DrawFeatureSearchBar(std::string& searchString, float availableWidth) + { + ImGui::PushID("FeatureSearchBar"); + + float iconSize = 20.0f; + float iconSpace = iconSize + 14.0f; + + // Get the current cursor position and available width + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + if (availableWidth <= 0.0f) { + availableWidth = ImGui::GetContentRegionAvail().x; + } + float frameHeight = ImGui::GetFrameHeight(); + + // 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); + + ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); + + // Draw the input field + ImGui::SetNextItemWidth(availableWidth); + char buffer[256]; + strncpy_s(buffer, searchString.c_str(), sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = '\0'; + + if (ImGui::InputTextWithHint("##feature_search", "Search Features...", buffer, sizeof(buffer))) { + searchString = buffer; + } + + // Draw a simple search icon (magnifying glass shape) + ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + 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); + + // Draw circle + drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + + // Draw handle + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(5); + ImGui::PopID(); + } } // namespace Util \ No newline at end of file diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 82f3570308..e997474254 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -10,6 +10,7 @@ struct ID3D11Device; struct ID3D11ShaderResourceView; struct ImVec2; class Menu; +class Feature; #define BUFFER_VIEWER_NODE(a_value, a_scale) \ if (ImGui::TreeNode(#a_value)) { \ @@ -449,4 +450,21 @@ namespace Util // Performance overlay formatting and color helpers ImVec4 GetThresholdColor(float value, float good, float warn, ImVec4 goodColor, ImVec4 warnColor, ImVec4 badColor); + + // Search functionality + /** + * @brief Checks if a feature matches the search query. + * Searches both the feature's short name and display name. + * @param feat The feature to check + * @param searchQuery The search query string + * @return True if the feature matches the search query + */ + bool FeatureMatchesSearch(Feature* feat, const std::string& searchQuery); + + /** + * @brief Draws the feature search bar with magnifying glass icon. + * @param searchString Reference to the search string to modify + * @param availableWidth The available width for the search bar + */ + void DrawFeatureSearchBar(std::string& searchString, float availableWidth = 0.0f); } // namespace Util From 55c675b892a419fac5e180287e1cbf28c13c6de3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:07:59 +0000 Subject: [PATCH 4/6] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commit.?= =?UTF-8?q?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.cpp | 158 ++++++++++++++++++++++++----------------------- src/Menu.h | 2 +- src/Utils/UI.cpp | 35 ++++++----- 3 files changed, 99 insertions(+), 96 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index bb5cbe2ee1..3f6e07ceeb 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -18,7 +18,6 @@ #include "State.h" #include "Streamline.h" #include "TruePBR.h" -#include "Utils/UI.h" #include "Upscaling.h" #include "Util.h" #include "Utils/UI.h" @@ -297,57 +296,59 @@ void Menu::Init() initialized = true; } -void Menu::DrawFeatureSearchBar() { +void Menu::DrawFeatureSearchBar() +{ ImGui::PushID("FeatureSearchBar"); - + float iconSize = 20.0f; float iconSpace = iconSize + 14.0f; - + // Get the current cursor position and available width ImVec2 cursorPos = ImGui::GetCursorScreenPos(); float availableWidth = ImGui::GetContentRegionAvail().x; float frameHeight = ImGui::GetFrameHeight(); - + // 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); - + ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); - + // Draw the input field ImGui::SetNextItemWidth(availableWidth); ImGui::InputTextWithHint("##feature_search", "Search Features...", &featureSearch); - // Draw a simple search icon (magnifying glass shape) - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - 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); - - // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - - // Draw handle - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); - + // Draw a simple search icon (magnifying glass shape) + ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + 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); + + // Draw circle + drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + + // Draw handle + ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); + ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + ImGui::PopStyleVar(2); ImGui::PopStyleColor(5); ImGui::PopID(); } - -bool Menu::FeatureMatchesSearch(Feature* feat) const { - if (featureSearch.empty()) return true; +bool Menu::FeatureMatchesSearch(Feature* feat) const +{ + if (featureSearch.empty()) + return true; std::string s = feat->GetShortName(); std::string q = featureSearch; std::transform(s.begin(), s.end(), s.begin(), ::tolower); @@ -355,8 +356,10 @@ bool Menu::FeatureMatchesSearch(Feature* feat) const { return s.find(q) != std::string::npos; } -bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const { - if (settingsSearch.empty()) return true; +bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const +{ + if (settingsSearch.empty()) + return true; std::string q = settingsSearch; std::transform(q.begin(), q.end(), q.begin(), ::tolower); std::string l = label, d = description; @@ -1076,12 +1079,12 @@ void Menu::DrawSettings() return a->GetName() < b->GetName(); }); - // Filter features by search string - if (!featureSearch.empty()) { - auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), - [this](Feature* feat) { return !Util::FeatureMatchesSearch(feat, featureSearch); }); - sortedFeatureList.erase(it, sortedFeatureList.end()); - } + // Filter features by search string + if (!featureSearch.empty()) { + auto it = std::remove_if(sortedFeatureList.begin(), sortedFeatureList.end(), + [this](Feature* feat) { return !Util::FeatureMatchesSearch(feat, featureSearch); }); + sortedFeatureList.erase(it, sortedFeatureList.end()); + } auto menuList = std::vector{ BuiltInMenu{ "General", [&]() { DrawGeneralSettings(); } }, @@ -1174,55 +1177,54 @@ void Menu::DrawSettings() pendingFeatureSelection.clear(); // Clear after processing } - // Now create the table with the declared variables available - if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { - ImGui::TableSetupColumn("##ListOfMenus", 0, 2); - ImGui::TableSetupColumn("##MenuConfig", 0, 8); - - ImGui::TableNextColumn(); - // Draw the feature list - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); - if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { - - // Find where built-in menus end (General, Advanced, Display) - size_t builtInMenuCount = 0; - for (size_t i = 0; i < menuList.size(); i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - builtInMenuCount++; - } + // Now create the table with the declared variables available + if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("##ListOfMenus", 0, 2); + ImGui::TableSetupColumn("##MenuConfig", 0, 8); + + ImGui::TableNextColumn(); + // Draw the feature list + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); + if (ImGui::BeginListBox("##MenusList", { -FLT_MIN, -FLT_MIN })) { + // Find where built-in menus end (General, Advanced, Display) + size_t builtInMenuCount = 0; + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + builtInMenuCount++; } } - - // First render the built-in menus (General, Advanced, Display) - size_t renderedBuiltIns = 0; - for (size_t i = 0; i < menuList.size() && renderedBuiltIns < 3; i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); - renderedBuiltIns++; - } + } + + // First render the built-in menus (General, Advanced, Display) + size_t renderedBuiltIns = 0; + for (size_t i = 0; i < menuList.size() && renderedBuiltIns < 3; i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + renderedBuiltIns++; } } - - // Add Features header and search bar after built-in settings - Util::DrawSectionHeader("Features", true); - Util::DrawFeatureSearchBar(featureSearch); - - // Then render the rest (features and categories, but skip already rendered built-ins) - for (size_t i = 0; i < menuList.size(); i++) { - if (std::holds_alternative(menuList[i])) { - BuiltInMenu& menu = std::get(menuList[i]); - if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { - continue; // Skip, already rendered - } + } + + // Add Features header and search bar after built-in settings + Util::DrawSectionHeader("Features", true); + Util::DrawFeatureSearchBar(featureSearch); + + // Then render the rest (features and categories, but skip already rendered built-ins) + for (size_t i = 0; i < menuList.size(); i++) { + if (std::holds_alternative(menuList[i])) { + BuiltInMenu& menu = std::get(menuList[i]); + if (menu.name == "General" || menu.name == "Advanced" || menu.name == "Display") { + continue; // Skip, already rendered } - std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); } - + std::visit(ListMenuVisitor{ i, selectedMenu }, menuList[i]); + } + ImGui::EndListBox(); } ImGui::PopStyleVar(); diff --git a/src/Menu.h b/src/Menu.h index 124b6dac8f..56a1d15bbb 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -28,7 +28,7 @@ class Menu void DrawSettings(); // Search bar state - std::string featureSearch; // For left pane feature search + std::string featureSearch; // For left pane feature search void DrawOverlay(); void DrawWeatherDetailsWindow(); diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 34fd298fcf..dd1e8b3aec 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -618,76 +618,77 @@ namespace Util bool FeatureMatchesSearch(Feature* feat, const std::string& searchQuery) { - if (searchQuery.empty()) return true; - + if (searchQuery.empty()) + return true; + // Get both short name and display name std::string shortName = feat->GetShortName(); std::string displayName = feat->GetName(); std::string query = searchQuery; - + // Convert all to lowercase for case-insensitive search std::transform(shortName.begin(), shortName.end(), shortName.begin(), ::tolower); std::transform(displayName.begin(), displayName.end(), displayName.begin(), ::tolower); std::transform(query.begin(), query.end(), query.begin(), ::tolower); - + // Search in both short name and display name - return shortName.find(query) != std::string::npos || + return shortName.find(query) != std::string::npos || displayName.find(query) != std::string::npos; } void DrawFeatureSearchBar(std::string& searchString, float availableWidth) { ImGui::PushID("FeatureSearchBar"); - + float iconSize = 20.0f; float iconSpace = iconSize + 14.0f; - + // Get the current cursor position and available width ImVec2 cursorPos = ImGui::GetCursorScreenPos(); if (availableWidth <= 0.0f) { availableWidth = ImGui::GetContentRegionAvail().x; } float frameHeight = ImGui::GetFrameHeight(); - + // 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); - + ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); - + // Draw the input field ImGui::SetNextItemWidth(availableWidth); char buffer[256]; strncpy_s(buffer, searchString.c_str(), sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; - + if (ImGui::InputTextWithHint("##feature_search", "Search Features...", buffer, sizeof(buffer))) { searchString = buffer; } - + // Draw a simple search icon (magnifying glass shape) ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); ImDrawList* drawList = ImGui::GetWindowDrawList(); - + 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); - + // Draw circle drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - + // Draw handle ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); - + ImGui::PopStyleVar(2); ImGui::PopStyleColor(5); ImGui::PopID(); From 1aafebdd10ef89f9abb9026994169655ddde5e87 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 15 Jul 2025 13:09:59 +1000 Subject: [PATCH 5/6] Update Menu.cpp --- src/Menu.cpp | 71 ---------------------------------------------------- 1 file changed, 71 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 3f6e07ceeb..76a657de60 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -296,77 +296,6 @@ void Menu::Init() initialized = true; } -void Menu::DrawFeatureSearchBar() -{ - ImGui::PushID("FeatureSearchBar"); - - float iconSize = 20.0f; - float iconSpace = iconSize + 14.0f; - - // Get the current cursor position and available width - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - float availableWidth = ImGui::GetContentRegionAvail().x; - float frameHeight = ImGui::GetFrameHeight(); - - // 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); - - ImGui::PushStyleColor(ImGuiCol_FrameBg, bgColor); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgColor); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgColorActive); - ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); - - // Draw the input field - ImGui::SetNextItemWidth(availableWidth); - ImGui::InputTextWithHint("##feature_search", "Search Features...", &featureSearch); - // Draw a simple search icon (magnifying glass shape) - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - 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); - - // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); - - // Draw handle - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); - - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(5); - ImGui::PopID(); -} - -bool Menu::FeatureMatchesSearch(Feature* feat) const -{ - if (featureSearch.empty()) - return true; - std::string s = feat->GetShortName(); - std::string q = featureSearch; - std::transform(s.begin(), s.end(), s.begin(), ::tolower); - std::transform(q.begin(), q.end(), q.begin(), ::tolower); - return s.find(q) != std::string::npos; -} - -bool Menu::SettingMatchesSearch(const std::string& label, const std::string& description) const -{ - if (settingsSearch.empty()) - return true; - std::string q = settingsSearch; - std::transform(q.begin(), q.end(), q.begin(), ::tolower); - std::string l = label, d = description; - std::transform(l.begin(), l.end(), l.begin(), ::tolower); - std::transform(d.begin(), d.end(), d.begin(), ::tolower); - return l.find(q) != std::string::npos || d.find(q) != std::string::npos; -} void Menu::DrawSettings() { From ec3f95dc1cafabd11f5071fc5ae3e4e6618c474f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:10:23 +0000 Subject: [PATCH 6/6] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commit.?= =?UTF-8?q?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.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 76a657de60..feb77ecafd 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -296,7 +296,6 @@ void Menu::Init() initialized = true; } - void Menu::DrawSettings() { if (focusChanged) {