diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index b52bc4ca..e6123b5e 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -3304,6 +3304,7 @@ initFrame:SetScript("OnEvent", function(self) local grow = bd.growDirection or "RIGHT" local numRows = bd.numRows or 1 if numRows < 1 then numRows = 1 end + local fillFirst = bd.fillFirstRow local tracked local isCustomBar = (bd.customSpells ~= nil) @@ -3324,15 +3325,18 @@ initFrame:SetScript("OnEvent", function(self) -- Spell columns: enough to fit all spells with full rows local stride = math.ceil(count / numRows) if stride < 1 then stride = 1 end + -- Recompute numRows to eliminate empty rows + if count > 0 then numRows = math.ceil(count / stride) end local gridSlots = (count > 0) and (stride * numRows) or 0 self._stride = stride self._numRows = numRows self._gridSlots = gridSlots - -- How many icons on the top row (remainder). Bottom rows are full. - local topRowCount = count - (numRows - 1) * stride - if topRowCount < 0 then topRowCount = 0 end - local topRowHasLess = (topRowCount > 0 and topRowCount < stride) + -- How many icons on the partial row + local partialCount = count - (numRows - 1) * stride + if partialCount < 0 then partialCount = 0 end + local partialHasLess = (partialCount > 0 and partialCount < stride) + local partialRow = fillFirst and (numRows - 1) or 0 -- Total dimensions: spell grid + 1 extra slot for the "+" button local isVert = (grow == "DOWN" or grow == "UP") @@ -3351,15 +3355,13 @@ initFrame:SetScript("OnEvent", function(self) local startY = -5 -- Position helper: places frame at grid position (col, row). - -- Row 0 = top row (partial, centered when fewer icons). - -- Rows 1..numRows-1 = bottom rows (always full). + -- Centering applies to the partial row (row 0 when !fillFirst, last row when fillFirst). local function PosAtGrid(frame, col, row) PP.Size(frame, iconSize, iconH); frame:ClearAllPoints() - -- Centering only applies to top row when it has fewer icons local rowOffset = 0 if isVert then - if row == 0 and topRowHasLess then - rowOffset = math.floor((stride - topRowCount) * (iconH + spacing) / 2) + if row == partialRow and partialHasLess then + rowOffset = math.floor((stride - partialCount) * (iconH + spacing) / 2) end local px = startX + row * (iconSize + spacing) local py @@ -3372,8 +3374,8 @@ initFrame:SetScript("OnEvent", function(self) frame._baseX = px frame._baseY = py else - if row == 0 and topRowHasLess then - rowOffset = math.floor((stride - topRowCount) * (iconSize + spacing) / 2) + if row == partialRow and partialHasLess then + rowOffset = math.floor((stride - partialCount) * (iconSize + spacing) / 2) end local px if grow == "LEFT" then @@ -3400,21 +3402,31 @@ initFrame:SetScript("OnEvent", function(self) local shape = bd.iconShape or "none" - -- Layout: fill bottom-up. Icons 1..topRowCount go to top row (row 0), - -- remaining icons fill rows 1..numRows-1 (full bottom rows). + -- Layout icons across grid rows for i = 1, math.min(gridSlots, MAX_PREVIEW_ICONS) do local slot = previewSlots[i] slot._slotIdx = i - -- Map sequential index to bottom-up grid position + -- Map sequential index to grid position local col, row - if i <= topRowCount then - col = i - 1 - row = 0 + if fillFirst then + local fullSlots = (numRows - 1) * stride + if i <= fullSlots then + col = (i - 1) % stride + row = math.floor((i - 1) / stride) + else + col = i - fullSlots - 1 + row = numRows - 1 + end else - local bottomIdx = i - topRowCount - 1 - col = bottomIdx % stride - row = 1 + math.floor(bottomIdx / stride) + if i <= partialCount then + col = i - 1 + row = 0 + else + local bottomIdx = i - partialCount - 1 + col = bottomIdx % stride + row = 1 + math.floor(bottomIdx / stride) + end end PosAtGrid(slot, col, row) @@ -4062,14 +4074,36 @@ initFrame:SetScript("OnEvent", function(self) MakeCogBtn(rightRgn, bgCogShow, ctrl) end - -- Row 2: Number of Rows | Vertical Orientation - _, h = W:DualRow(parent, y, + -- Row 2: Number of Rows/Columns | Vertical Orientation + local function GetSpellCount() + local bd = BD() + if bd.customSpells then return #bd.customSpells end + local n = 0 + if bd.trackedSpells then n = n + #bd.trackedSpells end + if bd.extraSpells then n = n + #bd.extraSpells end + return n + end + local function MaxRows() + local count = GetSpellCount() + if count <= 1 then return 1 end + return math.min(6, math.ceil(count / 2)) + end + local numRowsRow + numRowsRow, h = W:DualRow(parent, y, { type="slider", text="Number of Rows", min=1, max=6, step=1, - getValue=function() return BD().numRows or 1 end, + getValue=function() + local v = BD().numRows or 1 + local mx = MaxRows() + if v > mx then return mx end + return v + end, setValue=function(v) + local mx = MaxRows() + if v > mx then v = mx end BD().numRows = v ns.BuildAllCDMBars(); Refresh(); UpdateCDMPreviewAndResize() + EllesmereUI:RefreshPage() end }, { type="toggle", text="Vertical Orientation", getValue=function() return BD().verticalOrientation end, @@ -4077,9 +4111,51 @@ initFrame:SetScript("OnEvent", function(self) BD().verticalOrientation = v BD().growDirection = v and "DOWN" or "RIGHT" ns.BuildAllCDMBars(); Refresh(); UpdateCDMPreviewAndResize() + EllesmereUI:RefreshPage() + end }); y = y - h + -- Dynamic label: adapts to vertical orientation + EllesmereUI.RegisterWidgetRefresh(function() + local lbl = numRowsRow._leftRegion and numRowsRow._leftRegion._label + if lbl then + lbl:SetText(BD().verticalOrientation and "Number of Columns" or "Number of Rows") + end + end) + + -- Row 3: Fill Top Row/Left Column First | Show Tooltip on Hover + local fillRow + fillRow, h = W:DualRow(parent, y, + { type="toggle", text="Fill Top Row First", + disabled=function() + local rows = BD().numRows or 1 + local mx = MaxRows() + if rows > mx then rows = mx end + return rows <= 1 + end, + disabledTooltip="Requires more than 1 row", + tooltip="When enabled, fills the first row or column completely before distributing remaining icons to subsequent rows or columns", + getValue=function() return BD().fillFirstRow end, + setValue=function(v) + BD().fillFirstRow = v + ns.BuildAllCDMBars(); Refresh(); UpdateCDMPreviewAndResize() + end }, + { type="toggle", text="Show Tooltip on Hover", + getValue=function() return BD().showTooltip == true end, + setValue=function(v) + BD().showTooltip = v + ns.ApplyCDMTooltipState(BD().key) + Refresh() end }); y = y - h + -- Dynamic label + tooltip: adapts to vertical orientation + EllesmereUI.RegisterWidgetRefresh(function() + local rgn = fillRow._leftRegion + if not rgn then return end + local isVert = BD().verticalOrientation + if rgn._label then + rgn._label:SetText(isVert and "Fill Left Column First" or "Fill Top Row First") + end + end) - -- Row 3: Anchored To | Anchor Position (cog: Growth + X + Y) + -- Row 4: Anchored To | Anchor Position (cog: Growth + X + Y) local _erbLoaded = C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("EllesmereUIResourceBars") local ERB_ANCHOR_KEYS = { erb_castbar = true, erb_powerbar = true, erb_classresource = true } local function GetAnchorChoices() @@ -4251,18 +4327,6 @@ initFrame:SetScript("OnEvent", function(self) ); y = y - h end - -- Tooltip - _, h = W:DualRow(parent, y, - { type="toggle", text="Show Tooltip on Hover", - getValue=function() return BD().showTooltip == true end, - setValue=function(v) - BD().showTooltip = v - ns.ApplyCDMTooltipState(BD().key) - Refresh() - end }, - { type="label", text="" } - ); y = y - h - -- Inline color swatch + cog on Show Keybind (right region) do local rgn = kbRow._rightRegion diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index 16b78b14..6f61a2c8 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -927,6 +927,7 @@ local DEFAULTS = { barDefaults = { iconSize = 36, numRows = 1, + fillFirstRow = false, spacing = 2, borderSize = 1, borderR = 0, borderG = 0, borderB = 0, borderA = 1, @@ -2810,6 +2811,7 @@ LayoutCDMBar = function(barKey) local grow = frame._mouseGrow or barData.growDirection or "RIGHT" local numRows = barData.numRows or 1 if numRows < 1 then numRows = 1 end + local fillFirst = barData.fillFirstRow -- Collect visible icons (reuse buffer to avoid garbage) local visibleIcons = frame._visibleIconsBuf @@ -2829,6 +2831,8 @@ LayoutCDMBar = function(barKey) local isHoriz = (grow == "RIGHT" or grow == "LEFT") local stride = math.ceil(count / numRows) + -- Recompute numRows to eliminate empty rows + numRows = math.ceil(count / stride) -- Container size (already snapped values) local totalW, totalH @@ -2869,13 +2873,15 @@ LayoutCDMBar = function(barKey) local stepW = iconW + spacing local stepH = iconH + spacing - -- How many icons on the top row (remainder goes top, full rows on bottom) - local topRowCount = count - (numRows - 1) * stride - if topRowCount < 0 then topRowCount = 0 end - local topRowHasLess = (topRowCount > 0 and topRowCount < stride) + -- How many icons on the partial row + local partialCount = count - (numRows - 1) * stride + if partialCount < 0 then partialCount = 0 end + local partialHasLess = (partialCount > 0 and partialCount < stride) + + -- When fillFirst is true, full rows come first and the partial row is last. + -- When false (default), partial row is first (row 0) and full rows follow. + local partialRow = fillFirst and (numRows - 1) or 0 - -- Position each icon: fill bottom-up so bottom rows are full, - -- top row gets the remainder. Centering only on top row when partial. for i, icon in ipairs(visibleIcons) do icon:SetSize(iconW, iconH) if icon._glowOverlay then @@ -2883,48 +2889,58 @@ LayoutCDMBar = function(barKey) end icon:ClearAllPoints() - -- Map sequential index to bottom-up grid position. - -- Icon 1..topRowCount fill the top row (visual row 0). - -- Remaining icons fill rows 1..numRows-1 (bottom rows, full). local col, row - if i <= topRowCount then - col = i - 1 - row = 0 + if fillFirst then + -- Full rows first (rows 0..numRows-2), partial row last + local fullSlots = (numRows - 1) * stride + if i <= fullSlots then + col = (i - 1) % stride + row = math.floor((i - 1) / stride) + else + col = i - fullSlots - 1 + row = numRows - 1 + end else - local bottomIdx = i - topRowCount - 1 - col = bottomIdx % stride - row = 1 + math.floor(bottomIdx / stride) + -- Partial row first (row 0), full rows after (original behavior) + if i <= partialCount then + col = i - 1 + row = 0 + else + local bottomIdx = i - partialCount - 1 + col = bottomIdx % stride + row = 1 + math.floor(bottomIdx / stride) + end end - -- Only center the top row when it has fewer icons than stride + -- Center the partial row when it has fewer icons than stride if grow == "RIGHT" then local rowOffset = 0 - if row == 0 and topRowHasLess then - rowOffset = SnapForScale((stride - topRowCount) * stepW / 2, barScale) + if row == partialRow and partialHasLess then + rowOffset = SnapForScale((stride - partialCount) * stepW / 2, barScale) end icon:SetPoint("TOPLEFT", frame, "TOPLEFT", col * stepW + rowOffset, -(row * stepH)) elseif grow == "LEFT" then local rowOffset = 0 - if row == 0 and topRowHasLess then - rowOffset = SnapForScale((stride - topRowCount) * stepW / 2, barScale) + if row == partialRow and partialHasLess then + rowOffset = SnapForScale((stride - partialCount) * stepW / 2, barScale) end icon:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -(col * stepW + rowOffset), -(row * stepH)) elseif grow == "DOWN" then local rowOffset = 0 - if row == 0 and topRowHasLess then - rowOffset = SnapForScale((stride - topRowCount) * stepH / 2, barScale) + if row == partialRow and partialHasLess then + rowOffset = SnapForScale((stride - partialCount) * stepH / 2, barScale) end icon:SetPoint("TOPLEFT", frame, "TOPLEFT", row * stepW, -(col * stepH + rowOffset)) elseif grow == "UP" then local rowOffset = 0 - if row == 0 and topRowHasLess then - rowOffset = SnapForScale((stride - topRowCount) * stepH / 2, barScale) + if row == partialRow and partialHasLess then + rowOffset = SnapForScale((stride - partialCount) * stepH / 2, barScale) end icon:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", row * stepW, @@ -5919,26 +5935,31 @@ function ns.AddTrackedSpell(barKey, id, isExtra) for _, existing in ipairs(b.customSpells) do if existing == id then return false end end - -- Insert so the new spell fills the top row's next empty slot. - -- With bottom-up fill, icons 1..topRowCount go to the top row. + -- Insert position depends on row fill direction + local fillFirst = b.fillFirstRow local numRows = b.numRows or 1 if numRows < 1 then numRows = 1 end local curCount = #b.customSpells local stride = math.ceil(curCount / numRows) if stride < 1 then stride = 1 end - local topRowCount = curCount - (numRows - 1) * stride - if topRowCount < 0 then topRowCount = 0 end - -- New count after insert local newCount = curCount + 1 - local newStride = math.ceil(newCount / numRows) - if newStride < 1 then newStride = 1 end - local newTopRow = newCount - (numRows - 1) * newStride - if newTopRow < 0 then newTopRow = 0 end - -- If stride didn't change, insert at end of top row section - if newStride == stride and newTopRow > topRowCount then - table.insert(b.customSpells, topRowCount + 1, id) - else + + if fillFirst then + -- Partial row is last; appending fills it naturally b.customSpells[newCount] = id + else + -- Partial row is first (top); insert to fill top row + local topRowCount = curCount - (numRows - 1) * stride + if topRowCount < 0 then topRowCount = 0 end + local newStride = math.ceil(newCount / numRows) + if newStride < 1 then newStride = 1 end + local newTopRow = newCount - (numRows - 1) * newStride + if newTopRow < 0 then newTopRow = 0 end + if newStride == stride and newTopRow > topRowCount then + table.insert(b.customSpells, topRowCount + 1, id) + else + b.customSpells[newCount] = id + end end elseif isExtra then -- Default bar: store extras in a separate list @@ -5952,23 +5973,29 @@ function ns.AddTrackedSpell(barKey, id, isExtra) for _, existing in ipairs(b.trackedSpells) do if existing == id then return false end end - -- Insert so the new spell fills the top row's next empty slot + -- Insert position depends on row fill direction + local fillFirst = b.fillFirstRow local numRows = b.numRows or 1 if numRows < 1 then numRows = 1 end local curCount = #b.trackedSpells local stride = math.ceil(curCount / numRows) if stride < 1 then stride = 1 end - local topRowCount = curCount - (numRows - 1) * stride - if topRowCount < 0 then topRowCount = 0 end local newCount = curCount + 1 - local newStride = math.ceil(newCount / numRows) - if newStride < 1 then newStride = 1 end - local newTopRow = newCount - (numRows - 1) * newStride - if newTopRow < 0 then newTopRow = 0 end - if newStride == stride and newTopRow > topRowCount then - table.insert(b.trackedSpells, topRowCount + 1, id) - else + + if fillFirst then b.trackedSpells[newCount] = id + else + local topRowCount = curCount - (numRows - 1) * stride + if topRowCount < 0 then topRowCount = 0 end + local newStride = math.ceil(newCount / numRows) + if newStride < 1 then newStride = 1 end + local newTopRow = newCount - (numRows - 1) * newStride + if newTopRow < 0 then newTopRow = 0 end + if newStride == stride and newTopRow > topRowCount then + table.insert(b.trackedSpells, topRowCount + 1, id) + else + b.trackedSpells[newCount] = id + end end -- Clear removal flag so reconcile does not strip it if b.removedSpells then b.removedSpells[id] = nil end