From 124ba1b93162a50e70713ca6e6440de2144cfa3d Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:29:34 +0100 Subject: [PATCH] Add section headers to Gameplay settings Add section headers to Gameplay settings Add lightweight, non-selectable section headers and spacers to the Gameplay settings menu so related options are easier to scan. Keep the change UI-only by injecting disabled list rows while building the settings list, without changing the options model or option behavior. Also make the initial focus fall back to the first selectable option if the previously selected option is no longer visible. --- Source/DiabloUI/settingsmenu.cpp | 51 +++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Source/DiabloUI/settingsmenu.cpp b/Source/DiabloUI/settingsmenu.cpp index 94cdae470..aa2a74516 100644 --- a/Source/DiabloUI/settingsmenu.cpp +++ b/Source/DiabloUI/settingsmenu.cpp @@ -107,6 +107,41 @@ bool NeedsTwoLinesToDisplayOption(std::vector &formatArgs) return GetLineWidth("{}: {}", formatArgs.data(), formatArgs.size(), 0, GameFontTables::GameFont24, 1) >= (rectList.size.width - 90); } +void AddSettingsSpacer() +{ + vecDialogItems.push_back(std::make_unique( + std::string_view {}, static_cast(SpecialMenuEntry::None), UiFlags::ElementDisabled)); +} + +void AddSettingsSectionHeader(std::string_view text, bool addSpacer) +{ + if (addSpacer) + AddSettingsSpacer(); + vecDialogItems.push_back(std::make_unique( + text, static_cast(SpecialMenuEntry::None), UiFlags::ColorWhitegold | UiFlags::ElementDisabled)); +} + +void MaybeAddGameplaySectionHeader(OptionEntryBase *pEntry) +{ + if (selectedCategory != &GetOptions().Gameplay) + return; + + const GameplayOptions &gameplay = GetOptions().Gameplay; + + // Sections are intentionally lightweight: just labels/separators, no submenus and no changes to options model. + if (pEntry == &gameplay.friendlyFire) { + AddSettingsSectionHeader(_("Game Rules"), false); + } else if (pEntry == &gameplay.runInTown) { + AddSettingsSectionHeader(_("Controls"), true); + } else if (pEntry == &gameplay.experienceBar) { + AddSettingsSectionHeader(_("Interface"), true); + } else if (pEntry == &gameplay.autoRefillBelt) { + AddSettingsSectionHeader(_("Items & Auto Pickup"), true); + } else if (pEntry == &gameplay.disableCripplingShrines) { + AddSettingsSectionHeader(_("Safety & Focus"), true); + } +} + void CleanUpSettingsUI() { UiInitList_clear(); @@ -419,11 +454,21 @@ void UiSettingsMenu() } } break; case ShownMenuType::Settings: { + bool foundSelectedOption = false; + std::optional firstSelectableItemIndex; for (OptionEntryBase *pEntry : selectedCategory->GetEntries()) { if (!IsValidEntry(pEntry)) continue; - if (selectedOption == pEntry) + + MaybeAddGameplaySectionHeader(pEntry); + + if (!firstSelectableItemIndex.has_value()) + firstSelectableItemIndex = vecDialogItems.size(); + if (selectedOption == pEntry) { itemToSelect = vecDialogItems.size(); + foundSelectedOption = true; + } + auto formatArgs = CreateDrawStringFormatArgForEntry(pEntry); const int optionId = static_cast(vecOptions.size()); if (NeedsTwoLinesToDisplayOption(formatArgs)) { @@ -434,6 +479,10 @@ void UiSettingsMenu() } vecOptions.push_back(pEntry); } + // If previously selected option doesn't exist (e.g., became invisible), + // set focus on first real option, not on header. + if (!foundSelectedOption && firstSelectableItemIndex.has_value()) + itemToSelect = *firstSelectableItemIndex; } break; case ShownMenuType::ListOption: { auto *pOptionList = static_cast(selectedOption);