diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e9c2dfd8..e7c5c7cb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -296,6 +296,8 @@ ECM uses LibSettingsBuilder as a single declarative registration tree: `ExtraIconsOptions` owns the main viewer-management page, while `ItemStacksOptions` appends the Item Stacks subpage under the same Extra Icons section. Viewer entries reference item stacks by stable profile IDs so renames do not mutate viewer rows. +Resource bar color rows prepend a fixed two-icon prefix so shared resources like Combo Points can show multiple class icons while keeping the icon column right-aligned. + `UI/SpellColorsPage.lua` owns the shared Spell Colors subcategory. `BuffBarsOptions` registers the page once, and both `BuffBars` and `ExternalBars` register scoped sections into it, so the two modules share one editor without sharing saved color pools. ECM only consumes the documented public surface (`LSB.New`, `lsb:GetSection`, `lsb:GetRootPage`, `lsb:GetPage`, `lsb:HasCategory`, `page:GetId`, `page:Refresh`) and registers pages through raw declarative row tables — no builder-level helper constructors and no deprecated transition namespaces. diff --git a/Tests/UI/ResourceBarOptions_spec.lua b/Tests/UI/ResourceBarOptions_spec.lua index 1cffc94a..e28c2895 100644 --- a/Tests/UI/ResourceBarOptions_spec.lua +++ b/Tests/UI/ResourceBarOptions_spec.lua @@ -8,6 +8,15 @@ local TestHelpers = describe("ResourceBarOptions getters/setters/defaults", function() local originalGlobals local profile, defaults, SB, ns, settings, capturedPage + local emptyIcon = "|TInterface\\Common\\spacer:14:14|t" + + local function classIcon(className) + return "|A:classicon-" .. className .. ":14:14|a" + end + + local function resourceLabel(prefix, colorClass, labelKey) + return prefix .. " |cff" .. ns.Constants.CLASS_COLORS[colorClass] .. ns.L[labelKey] .. "|r" + end local function getPageRow(path) for _, row in ipairs(capturedPage.rows) do @@ -157,19 +166,47 @@ describe("ResourceBarOptions getters/setters/defaults", function() end) it("prefixes each resource label with its class icon and color", function() local defsByKey = {} + local demonHunterIcon = emptyIcon .. " " .. classIcon("demonhunter") for _, def in ipairs(assert(getPageRow("colors")).defs) do defsByKey[def.key] = def.name end - assert.are.equal("|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) - assert.are.equal("|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) - assert.are.equal("|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) - assert.are.equal("|A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) - assert.are.equal("|A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) - assert.are.equal("|A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) - assert.are.equal("|A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) - assert.are.equal("|A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) - assert.are.equal("|A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) + assert.are.equal( + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DH"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS] + ) + assert.are.equal( + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DEVOURER"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("mage"), "MAGE", "RESOURCE_ICICLES"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("monk"), "MONK", "RESOURCE_CHI"), + defsByKey[Enum.PowerType.Chi] + ) + assert.are.equal( + resourceLabel(classIcon("druid") .. " " .. classIcon("rogue"), "ROGUE", "RESOURCE_COMBO_POINTS"), + defsByKey[Enum.PowerType.ComboPoints] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("evoker"), "EVOKER", "RESOURCE_ESSENCE"), + defsByKey[Enum.PowerType.Essence] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("paladin"), "PALADIN", "RESOURCE_HOLY_POWER"), + defsByKey[Enum.PowerType.HolyPower] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("shaman"), "SHAMAN", "RESOURCE_MAELSTROM_WEAPON"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("warlock"), "WARLOCK", "RESOURCE_SOUL_SHARDS"), + defsByKey[Enum.PowerType.SoulShards] + ) assert.is_nil(defsByKey[Enum.PowerType.ArcaneCharges]) end) end) @@ -209,20 +246,21 @@ describe("ResourceBarOptions getters/setters/defaults", function() end) it("reuses the icon-prefixed names for capped resource rows", function() local defsByKey = {} + local demonHunterIcon = emptyIcon .. " " .. classIcon("demonhunter") for _, def in ipairs(assert(getPageRow("maxColors")).defs) do defsByKey[def.key] = def.name end assert.are.equal( - "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DEVOURER"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] ) assert.are.equal( - "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_VOID_FRAGMENTS_DEVOURER"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_META] ) assert.are.equal( - "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", + resourceLabel(emptyIcon .. " " .. classIcon("mage"), "MAGE", "RESOURCE_ICICLES"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] ) end) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index f1d1650d..9ca546c3 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -6,53 +6,75 @@ local _, ns = ... local C = ns.Constants local L = ns.L local COLOR_WHITE_HEX = C.COLOR_WHITE_HEX or "FFFFFF" +local RESOURCE_ICON_SIZE = 14 +local RESOURCE_ICON_SLOTS = 2 +-- Transparent texture slot for right-aligning rows with fewer class icons. +local EMPTY_RESOURCE_ICON = + "|TInterface\\Common\\spacer:" + .. RESOURCE_ICON_SIZE + .. ":" + .. RESOURCE_ICON_SIZE + .. "|t" -local function createResourceColorName(className, label) - local color = (C.CLASS_COLORS and C.CLASS_COLORS[className]) or COLOR_WHITE_HEX - local icon = className and ("|A:classicon-" .. string.lower(className) .. ":14:14|a ") or "" - return icon .. "|cff" .. color .. label .. "|r" +local function createResourceColorName(colorClassName, label, iconClasses) + local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX + local icons = {} + local iconCount = math.min(#iconClasses, RESOURCE_ICON_SLOTS) + local padding = math.max(0, RESOURCE_ICON_SLOTS - iconCount) + local startIndex = math.max(1, #iconClasses - RESOURCE_ICON_SLOTS + 1) + + for _ = 1, padding do + icons[#icons + 1] = EMPTY_RESOURCE_ICON + end + + for i = startIndex, #iconClasses do + local iconClass = string.lower(iconClasses[i]) + icons[#icons + 1] = "|A:classicon-" .. iconClass .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" + end + + return table.concat(icons, " ") .. " |cff" .. color .. label .. "|r" end local RESOURCE_COLOR_DEFS = { { key = C.RESOURCEBAR_TYPE_VENGEANCE_SOULS, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DH"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DH"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_DEVOURER_NORMAL, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_DEVOURER_META, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_VOID_FRAGMENTS_DEVOURER"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_VOID_FRAGMENTS_DEVOURER"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_ICICLES, - name = createResourceColorName("MAGE", L["RESOURCE_ICICLES"]), + name = createResourceColorName("MAGE", L["RESOURCE_ICICLES"], { "MAGE" }), }, { key = Enum.PowerType.Chi, - name = createResourceColorName("MONK", L["RESOURCE_CHI"]), + name = createResourceColorName("MONK", L["RESOURCE_CHI"], { "MONK" }), }, { key = Enum.PowerType.ComboPoints, - name = createResourceColorName("ROGUE", L["RESOURCE_COMBO_POINTS"]), + name = createResourceColorName("ROGUE", L["RESOURCE_COMBO_POINTS"], { "DRUID", "ROGUE" }), }, { key = Enum.PowerType.Essence, - name = createResourceColorName("EVOKER", L["RESOURCE_ESSENCE"]), + name = createResourceColorName("EVOKER", L["RESOURCE_ESSENCE"], { "EVOKER" }), }, { key = Enum.PowerType.HolyPower, - name = createResourceColorName("PALADIN", L["RESOURCE_HOLY_POWER"]), + name = createResourceColorName("PALADIN", L["RESOURCE_HOLY_POWER"], { "PALADIN" }), }, { key = C.RESOURCEBAR_TYPE_MAELSTROM_WEAPON, - name = createResourceColorName("SHAMAN", L["RESOURCE_MAELSTROM_WEAPON"]), + name = createResourceColorName("SHAMAN", L["RESOURCE_MAELSTROM_WEAPON"], { "SHAMAN" }), }, { key = Enum.PowerType.SoulShards, - name = createResourceColorName("WARLOCK", L["RESOURCE_SOUL_SHARDS"]), + name = createResourceColorName("WARLOCK", L["RESOURCE_SOUL_SHARDS"], { "WARLOCK" }), }, }