Files
WeakestAuras/MoverSizer.lua
github-actions[bot] 9588a9cf0a WeakestAuras 0.18.0
ClassicAPI 1.18 is now the only requirement

Requirements
- Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional.
- On an older ClassicAPI the addon stays off, and a popup links the download.
- Custom triggers on SuperWoW or Nampower events only fire while that mod is installed.

Triggers
- New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs.
  WeakAuras2 auras that use one now import.
- Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT.
- Crowd Control matches a specific type (stun, fear, root, silence, disarm and
  more) or a school lockout, with the effect's real timer.
- Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and
  Snare become "any type", and snares are no longer detected.
- Cast bars follow pushback and channel updates.

Displays
- Icon cooldown swipes are drawn by the client's own cooldown frame.
- Sound actions play on a chosen channel, and a new Stop sound type ends a
  looping sound.

Fixes
- Cooldown auras no longer sweep on every wand shot.
- The Global Cooldown trigger ignores wand and Auto Shot shots.
2026-10-29 20:41:53 +00:00

809 lines
30 KiB
Lua

-- WeakestAuras -- the in-world mover/sizer: drag the selected aura to
-- reposition or resize it, with a live anchor-chain visual showing its
-- selfPoint -> anchor relationship, and magnetic snapping to nearby auras.
-- Mirrors WA2's MoverSizer; no off-screen arrow.
--
-- Dragging is cursor-delta based rather than WA2's region:StartMoving()/GetPoint
-- translation -- our position is the composed offset (RegionPrototype
-- UpdatePosition), so writing data.xOffset/yOffset from the cursor delta and
-- re-applying through the region's own SetOffset keeps that composition
-- authoritative instead of fighting it.
if WeakestAuras.disabled then return end
local WA = WeakestAuras
WA.Mover = {}
local M = WA.Mover
-- math.* (radians) throughout -- the degree-based global atan2/cos/sin that
-- retail WA2 uses aren't evidenced on this 1.12 client (DoiteAuras/pfUI use
-- math.atan2/cos/sin); atan2's radian output feeds cos/sin directly, so the
-- chain math is internally consistent either way.
local atan2, cos, sin, sqrt, floor = math.atan2, math.cos, math.sin, math.sqrt, math.floor
local INTERIM_SPACING = 40 -- px between anchor-chain dots (matches WA2)
-- The outline's SetBackdrop edge is painted inward from the frame boundary, so
-- sizing the mover to the exact region would draw the outline on top of the
-- content (same reason the group border needs padding -- see Regions.lua). Pad
-- the outline outward by half the edge so it straddles the content edge.
local OUTLINE_EDGE = 12
local OUTLINE_PAD = OUTLINE_EDGE / 2
-- Magnetism: a dragged edge/center within this many *physical* pixels of another
-- aura's edge/center snaps onto it. WA2's AlignmentLines do the same, but its
-- lines are built on frame:CreateLine (absent on 1.12) -- since alignment lines
-- are always axis-aligned (vertical for an x-match, horizontal for a y-match),
-- plain thin textures replace it with no rotated-texture rebuild needed.
local SNAP_THRESHOLD = 8
local mover
local overlay -- holds the two alignment-line textures (screen-space, TOOLTIP strata)
local updateHandles
local ANCHOR_ICON_TEXTURE = "Interface\\GLUES\\CharacterSelect\\Glues-AddOn-Icons.blp"
local function ensureDot(m, i)
if m.dots[i] then return m.dots[i] end
local dot = m:CreateTexture(nil, "OVERLAY")
dot:SetWidth(16)
dot:SetHeight(16)
dot:SetTexture(ANCHOR_ICON_TEXTURE)
dot:SetTexCoord(0, 0.25, 0, 1)
dot:SetVertexColor(1, 1, 1, 0.25)
m.dots[i] = dot
return dot
end
-- Draws the selfPoint icon, the anchorPoint icon, the dotted line between them,
-- and the (dx, dy) label -- all read live from the region's current anchor
-- (region.selfPoint/anchorFrame/anchorPoint, set by RegionPrototype SetAnchor).
-- Suppressed entirely for a dynamicgroup child: its position is grower-owned, so
-- the anchor relationship isn't user-actionable and the numbers would just be
-- noise (WA2 hides the two point symbols there; we hide the whole chain).
local function updateChain(m)
local region = m.region
if not region then return end
if m.childOfDynamic then
m.selfIcon:Hide()
m.anchorIcon:Hide()
m.label:Hide()
for i = 1, #m.dots do m.dots[i]:Hide() end
return
end
m.selfIcon:Show()
m.anchorIcon:Show()
-- A group's frame sits on its children's bounding box rather than on the point
-- its anchor pins (RegionPrototype UpdatePosition's box slot), so the pinned
-- point is that far back from the frame's centre. Backing the icon out by it
-- is what keeps the chain -- and the offset it reports -- about the anchor
-- instead of about wherever the children happen to have pushed the box. The
-- offsets are the mover's own units and the box is the region's, hence the
-- scale; a leaf carries no box and lands on its selfPoint unchanged.
local scale = (region:GetEffectiveScale() or 1) / (UIParent:GetEffectiveScale() or 1)
m.selfIcon:ClearAllPoints()
m.selfIcon:SetPoint("CENTER", region, region.selfPoint or "CENTER",
-(region.xOffsetBox or 0) * scale, -(region.yOffsetBox or 0) * scale)
m.anchorIcon:ClearAllPoints()
m.anchorIcon:SetPoint("CENTER", region.anchorFrame or UIParent, region.anchorPoint or "CENTER")
local sx, sy = m.selfIcon:GetCenter()
local ax, ay = m.anchorIcon:GetCenter()
for i = 1, #m.dots do m.dots[i]:Hide() end
if not sx or not ax then
m.label:Hide()
return
end
local dX, dY = sx - ax, sy - ay
local dist = sqrt(dX * dX + dY * dY)
local angle = atan2(dY, dX)
local num = floor(dist / INTERIM_SPACING)
for i = 1, num do
local dot = ensureDot(m, i)
local x = (dist - i * INTERIM_SPACING) * cos(angle)
local y = (dist - i * INTERIM_SPACING) * sin(angle)
dot:ClearAllPoints()
dot:SetPoint("CENTER", m.anchorIcon, "CENTER", x, y)
dot:Show()
end
-- Report the offset in the region's own (unscaled) coordinates -- the same
-- numbers the Position sliders and data.xOffset/yOffset carry.
m.label:SetText(string.format("(%d, %d)", floor(dX / scale + 0.5), floor(dY / scale + 0.5)))
m.label:ClearAllPoints()
m.label:SetPoint("CENTER", m.anchorIcon, "CENTER", (dist / 2) * cos(angle), (dist / 2) * sin(angle))
m.label:Show()
end
-- Positions/sizes the outline over the region. Two-point anchoring for both a
-- leaf and a group: a group's frame is fitted to its children's bounding box
-- (Regions.lua drawGroupBox), so its rectangle is the thing to outline just as a
-- leaf's is. Reading the corners instead -- upstream's SetToRegion does, and
-- pays for it by scaling every one of them by the region's effective scale over
-- UIParent's -- would put the box's own coordinates into a frame that is a child
-- of UIParent and does not share the group's scale. Split out so both Attach and
-- the per-frame re-resolve reuse it.
local function anchorToRegion(m, region)
m:ClearAllPoints()
-- Pad outward so the inward-painted outline straddles the region edge rather
-- than sitting inside it.
m:SetPoint("TOPLEFT", region, "TOPLEFT", -OUTLINE_PAD, OUTLINE_PAD)
m:SetPoint("BOTTOMRIGHT", region, "BOTTOMRIGHT", OUTLINE_PAD, -OUTLINE_PAD)
end
-- Keeps the outline synced and redraws the chain. Re-resolves the live frame by
-- id every tick so a regionType switch (which rebuilds the frame under the same
-- id, without a selection change) re-attaches the mover instead of stranding it
-- on the old hidden frame -- and so a display whose visible frame is a clone
-- rather than the base one is followed as its clone set changes.
local function moverOnUpdate()
local m = mover
if not m.id then return end
local region = WA.MoverRegion(m.id)
if not region then return end
if region ~= m.region then
m.region = region
anchorToRegion(m, region)
m.resizable = (not m.isGroup) and m.data and m.data.width ~= nil and region.SetRegionWidth ~= nil
updateHandles(m)
end
updateChain(m)
end
-- ---------------------------------------------------------------------------
-- Magnetism (WA2's AlignmentLines, scaled). Guides are
-- snapshotted once at drag start (other auras don't move mid-drag); each frame
-- the dragged region's left/center/right and bottom/center/top are tested
-- against them and the nearest within SNAP_THRESHOLD wins per axis. All math is
-- in physical pixels so regions at different effective scales compare correctly.
-- ---------------------------------------------------------------------------
-- Both toggles persist (WA2 keeps its equivalents in WeakAurasOptionsSaved) and
-- default on/unlocked. Magnetism is additionally suppressed by holding Shift
-- during a drag, the WA2 convention. M.locked gates startDragging/startSizing,
-- so it disables moving *and* resizing from the in-world mover.
M.magnetism = WA.Options().magnetism ~= false
M.locked = WA.Options().locked == true
function M.SetMagnetism(v)
M.magnetism = v and true or false
WA.Options().magnetism = M.magnetism
end
function M.SetLocked(v)
M.locked = v and true or false
WA.Options().locked = M.locked
if mover then updateHandles(mover) end
end
local function ensureOverlay()
if overlay then return overlay end
overlay = CreateFrame("Frame", nil, UIParent)
overlay:SetAllPoints(UIParent)
overlay:SetFrameStrata("TOOLTIP")
local v = overlay:CreateTexture(nil, "OVERLAY")
v:SetTexture(0.2, 0.9, 1, 0.85); v:Hide()
overlay.vLine = v
local h = overlay:CreateTexture(nil, "OVERLAY")
h:SetTexture(0.2, 0.9, 1, 0.85); h:Hide()
overlay.hLine = h
return overlay
end
local function hideLines()
if overlay then overlay.vLine:Hide(); overlay.hLine:Hide() end
end
-- Lines span the full screen dimension (vertical = top-to-bottom at x,
-- horizontal = left-to-right at y), matching WA2 -- the two-anchor stretch fixes
-- the extent while SetWidth/SetHeight sets the thin dimension. A texture's
-- coordinates are in its parent's (UIParent) space, so a physical pixel value
-- divides back down by UIParent's scale to position it.
local function drawVLine(x)
local ov = ensureOverlay()
local us = UIParent:GetEffectiveScale() or 1
local v = ov.vLine
v:ClearAllPoints()
v:SetWidth(2)
v:SetPoint("TOP", UIParent, "TOPLEFT", x / us, 0)
v:SetPoint("BOTTOM", UIParent, "BOTTOMLEFT", x / us, 0)
v:Show()
end
local function drawHLine(y)
local ov = ensureOverlay()
local us = UIParent:GetEffectiveScale() or 1
local h = ov.hLine
h:ClearAllPoints()
h:SetHeight(2)
h:SetPoint("LEFT", UIParent, "BOTTOMLEFT", 0, y / us)
h:SetPoint("RIGHT", UIParent, "BOTTOMRIGHT", 0, y / us)
h:Show()
end
-- A frame's edges in physical pixels (nil until it has been laid out).
local function regionBoundsPhys(frame)
local s = frame:GetEffectiveScale() or 1
local l, r, t, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
if not l or not r or not t or not b then return nil end
return l * s, r * s, t * s, b * s
end
local function collectDescendants(id, set)
set[id] = true
local data = WeakestAurasDB.displays[id]
local kids = data and data.controlledChildren
if kids then
for i = 1, #kids do collectDescendants(kids[i], set) end
end
end
-- Snapshot every other visible leaf clone's edge/center coordinates as snap
-- targets, plus the screen center. Skips the dragged region and (if it's a
-- group) its whole subtree -- those move with the drag. Coordinates only: the
-- lines are full-screen (WA2), so a guide needs no perpendicular extent.
local function buildGuides(m)
local xg, yg = {}, {}
local excl = {}
if m.id then collectDescendants(m.id, excl) end
for id, data in pairs(WeakestAurasDB.displays) do
if not excl[id] and not WA.IsGroup(data) then
WA.ForEachClone(id, function(frame)
if frame:IsShown() then
local l, r, t, b = regionBoundsPhys(frame)
if l then
table.insert(xg, l); table.insert(xg, (l + r) / 2); table.insert(xg, r)
table.insert(yg, b); table.insert(yg, (t + b) / 2); table.insert(yg, t)
end
end
end)
end
end
local us = UIParent:GetEffectiveScale() or 1
local cx, cy = UIParent:GetCenter()
if cx then
table.insert(xg, cx * us)
table.insert(yg, cy * us)
end
m.xGuides = xg
m.yGuides = yg
end
-- Nearest guide (smallest absolute gap) within threshold across the three
-- dragged candidates, or nil. Returns the signed delta to close the gap and the
-- guide coordinate (where the line is drawn).
local function nearestGuide(guides, cands)
local bestAbs, bestDelta, bestCoord
for gi = 1, #guides do
local gc = guides[gi]
for ci = 1, #cands do
local delta = gc - cands[ci]
local a = delta < 0 and -delta or delta
if a <= SNAP_THRESHOLD and (not bestAbs or a < bestAbs) then
bestAbs, bestDelta, bestCoord = a, delta, gc
end
end
end
return bestDelta, bestCoord
end
-- Snap the just-computed offset onto the nearest guide per axis and draw the
-- alignment line(s). Deltas are in physical px; dividing by the region's scale
-- converts back to its own offset units.
-- Cursor deltas are continuous, so a drag would otherwise write 37.4213 into
-- data.width and leave the options sliders showing it. Everything the mover and
-- sizer store is snapped to whole pixels: a fractional edge only buys a blurry
-- half-pixel seam, and the sliders all step by 1 anyway. (Deliberately NOT
-- applied after applySnap -- an exact guide alignment is the one case that
-- should win over the grid.)
local function round(v)
return math.floor(v + 0.5)
end
local function applySnap(m, region, data, scale)
local l, r, t, b = regionBoundsPhys(region)
if not l then hideLines(); return end
local hc, vc = (l + r) / 2, (t + b) / 2
local dxDelta, xCoord = nearestGuide(m.xGuides, { l, hc, r })
local dyDelta, yCoord = nearestGuide(m.yGuides, { b, vc, t })
if dxDelta then
data.xOffset = data.xOffset + dxDelta / scale
end
if dyDelta then
data.yOffset = data.yOffset + dyDelta / scale
end
if dxDelta or dyDelta then
region:SetOffset(data.xOffset, data.yOffset)
end
if xCoord then
drawVLine(xCoord)
elseif overlay then
overlay.vLine:Hide()
end
if yCoord then
drawHLine(yCoord)
elseif overlay then
overlay.hLine:Hide()
end
end
local function onDragUpdate()
local m = mover
local region, data = m.region, m.data
if not region or not data then return end
local cx, cy = GetCursorPosition()
local scale = region:GetEffectiveScale() or 1
local dx = (cx - m.startCx) / scale
local dy = (cy - m.startCy) / scale
data.xOffset = round(m.startX + dx)
data.yOffset = round(m.startY + dy)
region:SetOffset(data.xOffset, data.yOffset)
if M.magnetism and not IsShiftKeyDown() then
applySnap(m, region, data, scale)
else
hideLines()
end
end
local function startDragging()
local m = mover
local region, data = m.region, m.data
if region and WA.CancelAnimation then WA.CancelAnimation(region, true, true, true, true, true, false) end
if not region or not data then return end
if M.locked then return end
-- A dynamicgroup owns its children's positions; dragging one is meaningless.
if m.childOfDynamic then return end
m.dragging = true
m.startCx, m.startCy = GetCursorPosition()
m.startX, m.startY = data.xOffset or 0, data.yOffset or 0
buildGuides(m) -- snapshot snap targets once; they don't move mid-drag
m:SetScript("OnUpdate", function() onDragUpdate(); moverOnUpdate() end)
end
local function stopDragging()
local m = mover
if not m.dragging then return end
m.dragging = false
m:SetScript("OnUpdate", moverOnUpdate)
hideLines()
if m.data then
WA.Add(m.data, true)
if WA.Animate and m.region then WA.Animate("display", m.data.uid, "main", m.data.animation and m.data.animation.main, m.region, false, nil, true, m.region.cloneId) end
end
end
-- ---------------------------------------------------------------------------
-- Sizer: eight edge/corner handles that resize the region. Like the mover,
-- cursor-delta driven (avoids the unverified StartSizing/SetResizable). The
-- selfPoint-aware offset adjustment keeps the non-dragged edge anchored while
-- the dragged one follows the cursor (WA2's SizingSetData). A group isn't
-- resizable (its box follows its children); a dynamicgroup *child* is (its size
-- is its own even though the grower owns its position -- so the handles show for
-- it while the body drag doesn't).
-- ---------------------------------------------------------------------------
local MIN_DIM = 4
local HANDLE_POINTS = { "TOPLEFT", "TOP", "TOPRIGHT", "RIGHT", "BOTTOMRIGHT", "BOTTOM", "BOTTOMLEFT", "LEFT" }
local HANDLE_TEXTURE = "Interface\\AddOns\\WeakestAuras\\textures\\UI-Listbox-Highlight.tga"
local CORNER_SIZE = 16
local EDGE_SIZE = 8
local function resizeSnap(m, region, width, height, hasL, hasR, hasT, hasB, scale)
local l, r, t, b = regionBoundsPhys(region)
if not l then hideLines(); return width, height end
local xEdge = hasL and l or (hasR and r)
local yEdge = hasB and b or (hasT and t)
local xDelta, xCoord
local yDelta, yCoord
if xEdge then xDelta, xCoord = nearestGuide(m.xGuides or {}, { xEdge }) end
if yEdge then yDelta, yCoord = nearestGuide(m.yGuides or {}, { yEdge }) end
if not xDelta and not yDelta then hideLines(); return width, height, false end
local useX = xDelta and (not yDelta or math.abs(xDelta) <= math.abs(yDelta))
if useX then
width = round(width + (hasR and xDelta or -xDelta) / scale)
if m.preserveAspect then height = width / m.aspectRatio end
elseif yDelta then
height = round(height + (hasT and yDelta or -yDelta) / scale)
if m.preserveAspect then width = height * m.aspectRatio end
end
if width < MIN_DIM then width = MIN_DIM end
if height < MIN_DIM then height = MIN_DIM end
if useX then drawVLine(xCoord) elseif xDelta then hideLines() end
if not m.preserveAspect and yDelta and not useX then drawHLine(yCoord) end
return width, height, true
end
local function onSizeUpdate()
local m = mover
local region, data = m.region, m.data
if not region or not data then return end
local cx, cy = GetCursorPosition()
local scale = region:GetEffectiveScale() or 1
local dx = (cx - m.startCx) / scale
local dy = (cy - m.startCy) / scale
local sp = m.sizePoint
local hasL, hasR = string.find(sp, "LEFT"), string.find(sp, "RIGHT")
local hasT, hasB = string.find(sp, "TOP"), string.find(sp, "BOTTOM")
-- Rounded before the clamp so MIN_DIM stays the real floor, and before the
-- offset math below reads dW/dH -- an edge that lands on a whole pixel must
-- shift the opposite edge by a whole pixel too.
local newW = m.startW
if hasR then newW = m.startW + dx elseif hasL then newW = m.startW - dx end
newW = round(newW)
if newW < MIN_DIM then newW = MIN_DIM end
local newH = m.startH
if hasT then newH = m.startH + dy elseif hasB then newH = m.startH - dy end
if m.preserveAspect then
local ratio = m.aspectRatio
local dW, dH = newW - m.startW, newH - m.startH
if math.abs(dW) >= math.abs(dH * ratio) then
newW = round(newW)
newH = newW / ratio
else
newH = round(newH)
newW = newH * ratio
end
if newW < MIN_DIM then
newW = MIN_DIM
newH = newW / ratio
elseif newH < MIN_DIM then
newH = MIN_DIM
newW = newH * ratio
end
end
if not m.preserveAspect then
newH = round(newH)
if newH < MIN_DIM then newH = MIN_DIM end
elseif newW < MIN_DIM or newH < MIN_DIM then
newW, newH = MIN_DIM, MIN_DIM / m.aspectRatio
end
data.width, data.height = newW, newH
region:SetRegionWidth(newW)
region:SetRegionHeight(newH)
-- A dynamicgroup child's position is grower-owned; only resize it (the grower
-- repositions it with the new size when we commit on release). For everything
-- else, shift the offset so the anchored (selfPoint) edge stays put -- only
-- when the dragged edge is on the same side as the anchor does it move.
if m.childOfDynamic then return end
local dW, dH = newW - m.startW, newH - m.startH
local asp = region.selfPoint or "CENTER"
local xoff = m.startXOff
if string.find(asp, "LEFT") then
if hasL then xoff = m.startXOff - dW end
elseif string.find(asp, "RIGHT") then
if hasR then xoff = m.startXOff + dW end
else -- CENTER
if hasL then xoff = m.startXOff - dW / 2 elseif hasR then xoff = m.startXOff + dW / 2 end
end
local yoff = m.startYOff
if string.find(asp, "BOTTOM") then
if hasB then yoff = m.startYOff - dH end
elseif string.find(asp, "TOP") then
if hasT then yoff = m.startYOff + dH end
else -- CENTER
if hasB then yoff = m.startYOff - dH / 2 elseif hasT then yoff = m.startYOff + dH / 2 end
end
-- A CENTER-anchored resize halves dW/dH, so the offset can still land on a
-- half pixel even though both dimensions are whole. Recomputed from
-- startXOff every frame rather than accumulated, so rounding it can't drift.
xoff, yoff = round(xoff), round(yoff)
data.xOffset, data.yOffset = xoff, yoff
region:SetOffset(xoff, yoff)
if M.magnetism and (m.preserveAspect or not IsShiftKeyDown()) then
local snapW, snapH, snapped = resizeSnap(m, region, newW, newH, hasL, hasR, hasT, hasB, scale)
if snapped then
newW, newH = snapW, snapH
data.width, data.height = newW, newH
region:SetRegionWidth(newW)
region:SetRegionHeight(newH)
local ndW, ndH = newW - m.startW, newH - m.startH
if string.find(asp, "LEFT") then
if hasL then xoff = m.startXOff - ndW end
elseif string.find(asp, "RIGHT") then
if hasR then xoff = m.startXOff + ndW end
else
if hasL then xoff = m.startXOff - ndW / 2 elseif hasR then xoff = m.startXOff + ndW / 2 end
end
if string.find(asp, "BOTTOM") then
if hasB then yoff = m.startYOff - ndH end
elseif string.find(asp, "TOP") then
if hasT then yoff = m.startYOff + ndH end
else
if hasB then yoff = m.startYOff - ndH / 2 elseif hasT then yoff = m.startYOff + ndH / 2 end
end
xoff, yoff = round(xoff), round(yoff)
data.xOffset, data.yOffset = xoff, yoff
region:SetOffset(xoff, yoff)
end
else
hideLines()
end
end
local function startSizing(point)
local m = mover
local region, data = m.region, m.data
if region and WA.CancelAnimation then WA.CancelAnimation(region, true, true, true, true, true, false) end
if not region or not data then return end
if M.locked then return end
m.sizing = true
m.sizePoint = point
m.preserveAspect = (string.find(point, "LEFT") or string.find(point, "RIGHT"))
and (string.find(point, "TOP") or string.find(point, "BOTTOM"))
and IsShiftKeyDown() and true or false
m.startCx, m.startCy = GetCursorPosition()
m.startW = data.width or (region:GetWidth() or 8)
m.startH = data.height or (region:GetHeight() or 8)
m.aspectRatio = m.startW > 0 and m.startH > 0 and m.startW / m.startH or 1
m.startXOff = data.xOffset or 0
m.startYOff = data.yOffset or 0
buildGuides(m)
m:SetScript("OnUpdate", function() onSizeUpdate(); moverOnUpdate() end)
end
local function stopSizing()
local m = mover
if not m.sizing then return end
m.sizing = false
m:SetScript("OnUpdate", moverOnUpdate)
if m.data then
WA.Add(m.data, true)
if WA.Animate and m.region then WA.Animate("display", m.data.uid, "main", m.data.animation and m.data.animation.main, m.region, false, nil, true, m.region.cloneId) end
end
end
local function makeHandleTexture(handle, coords)
local tex = handle:CreateTexture(nil, "OVERLAY")
tex:SetTexture(HANDLE_TEXTURE)
tex:SetBlendMode("ADD")
if coords then tex:SetTexCoord(unpack(coords)) end
tex:Hide()
return tex
end
local function hideHandleVisuals(handle)
for i = 1, #(handle.visuals or {}) do handle.visuals[i]:Hide() end
end
local function showHandleVisuals(handle)
if not handle._active then return end
for i = 1, #(handle.visuals or {}) do handle.visuals[i]:Show() end
end
updateHandles = function(m)
local active = m.resizable and not M.locked
for i = 1, #(m.handles or {}) do
local handle = m.handles[i]
handle._active = active
if active then handle:Show() else handle:Hide() end
hideHandleVisuals(handle)
end
end
local function addCorner(handle, firstCoords, firstPoints, secondCoords, secondPoints)
local first = makeHandleTexture(handle, firstCoords)
first:SetPoint(unpack(firstPoints[1]))
first:SetPoint(unpack(firstPoints[2]))
local second = makeHandleTexture(handle, secondCoords)
second:SetPoint(unpack(secondPoints[1]))
second:SetPoint(unpack(secondPoints[2]))
handle.visuals = { first, second }
end
local function addEdge(handle, coords, firstPoint, secondPoint)
local tex = makeHandleTexture(handle, coords)
tex:SetPoint(unpack(firstPoint))
tex:SetPoint(unpack(secondPoint))
handle.visuals = { tex }
end
local function configureCorner(handle, parent, point, firstCoords, secondCoords)
handle:SetWidth(CORNER_SIZE)
handle:SetHeight(CORNER_SIZE)
handle:SetPoint(point, parent, point)
local first, second
if point == "TOPRIGHT" then
first = makeHandleTexture(handle, firstCoords)
first:SetPoint("TOPRIGHT", handle, "TOPRIGHT", -3, -3)
first:SetPoint("BOTTOMLEFT", handle, "BOTTOM")
second = makeHandleTexture(handle, secondCoords)
second:SetPoint("TOPRIGHT", first, "TOPLEFT")
second:SetPoint("BOTTOMLEFT", handle, "LEFT")
elseif point == "BOTTOMRIGHT" then
first = makeHandleTexture(handle, firstCoords)
first:SetPoint("BOTTOMRIGHT", handle, "BOTTOMRIGHT", -3, 3)
first:SetPoint("TOPLEFT", handle, "TOP")
second = makeHandleTexture(handle, secondCoords)
second:SetPoint("BOTTOMRIGHT", first, "BOTTOMLEFT")
second:SetPoint("TOPLEFT", handle, "LEFT")
elseif point == "BOTTOMLEFT" then
first = makeHandleTexture(handle, firstCoords)
first:SetPoint("BOTTOMLEFT", handle, "BOTTOMLEFT", 3, 3)
first:SetPoint("TOPRIGHT", handle, "TOP")
second = makeHandleTexture(handle, secondCoords)
second:SetPoint("BOTTOMLEFT", first, "BOTTOMRIGHT")
second:SetPoint("TOPRIGHT", handle, "RIGHT")
else
first = makeHandleTexture(handle, firstCoords)
first:SetPoint("TOPLEFT", handle, "TOPLEFT", 3, -3)
first:SetPoint("BOTTOMRIGHT", handle, "BOTTOM")
second = makeHandleTexture(handle, secondCoords)
second:SetPoint("TOPLEFT", first, "TOPRIGHT")
second:SetPoint("BOTTOMRIGHT", handle, "RIGHT")
end
handle.visuals = { first, second }
end
local function configureEdge(handle, point, leftOrTop, rightOrBottom, coords)
if point == "TOP" then
handle:SetHeight(EDGE_SIZE)
handle:SetPoint("TOPRIGHT", rightOrBottom, "TOPLEFT")
handle:SetPoint("TOPLEFT", leftOrTop, "TOPRIGHT")
addEdge(handle, coords, { "TOPRIGHT", rightOrBottom, "TOPRIGHT", -3, -3 }, { "BOTTOMLEFT", leftOrTop, "LEFT", 3, 0 })
elseif point == "RIGHT" then
handle:SetWidth(EDGE_SIZE)
handle:SetPoint("BOTTOMRIGHT", rightOrBottom, "TOPRIGHT")
handle:SetPoint("TOPRIGHT", leftOrTop, "BOTTOMRIGHT")
addEdge(handle, coords, { "BOTTOMRIGHT", rightOrBottom, "BOTTOMRIGHT", -3, 3 }, { "TOPLEFT", leftOrTop, "TOP", 0, -3 })
elseif point == "BOTTOM" then
handle:SetHeight(EDGE_SIZE)
handle:SetPoint("BOTTOMLEFT", leftOrTop, "BOTTOMRIGHT")
handle:SetPoint("BOTTOMRIGHT", rightOrBottom, "BOTTOMLEFT")
addEdge(handle, coords, { "BOTTOMLEFT", leftOrTop, "BOTTOMLEFT", 3, 3 }, { "TOPRIGHT", rightOrBottom, "RIGHT", -3, 0 })
else
handle:SetWidth(EDGE_SIZE)
handle:SetPoint("TOPLEFT", leftOrTop, "BOTTOMLEFT")
handle:SetPoint("BOTTOMLEFT", rightOrBottom, "TOPLEFT")
addEdge(handle, coords, { "BOTTOMLEFT", rightOrBottom, "BOTTOMLEFT", 3, 3 }, { "TOPRIGHT", leftOrTop, "TOP", 0, -3 })
end
end
local function ensureMover()
if mover then return mover end
local m = CreateFrame("Frame", nil, UIParent)
m:SetFrameStrata("HIGH")
m:EnableMouse(true)
m:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = OUTLINE_EDGE,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
m:SetBackdropBorderColor(1, 0.82, 0, 0.9)
m.dots = {}
local selfIcon = m:CreateTexture(nil, "OVERLAY")
selfIcon:SetWidth(16); selfIcon:SetHeight(16)
selfIcon:SetTexture(ANCHOR_ICON_TEXTURE)
selfIcon:SetTexCoord(0, 0.25, 0, 1)
m.selfIcon = selfIcon
local anchorIcon = m:CreateTexture(nil, "OVERLAY")
anchorIcon:SetWidth(16); anchorIcon:SetHeight(16)
anchorIcon:SetTexture(ANCHOR_ICON_TEXTURE)
anchorIcon:SetTexCoord(0, 0.25, 0, 1)
m.anchorIcon = anchorIcon
local label = m:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:Hide()
m.label = label
-- Eight resize handles, one per edge/corner. The buttons remain the hit areas;
-- their child textures carry WA2's L-shaped visual treatment and stay hidden
-- until the pointer enters a live handle.
m.handles = {}
local corners = {}
local cornerCoords = {
TOPLEFT = { { 0.5, 0, 0, 0, 0.5, 1, 0, 1 }, { 0.5, 0, 0.5, 1, 1, 0, 1, 1 } },
TOPRIGHT = { { 0.5, 0, 0, 0, 0.5, 1, 0, 1 }, { 0, 0, 0, 1, 0.5, 0, 0.5, 1 } },
BOTTOMRIGHT = { { 1, 0, 0.5, 0, 1, 1, 0.5, 1 }, { 0, 0, 0, 1, 0.5, 0, 0.5, 1 } },
BOTTOMLEFT = { { 1, 0, 0.5, 0, 1, 1, 0.5, 1 }, { 0.5, 0, 0.5, 1, 1, 0, 1, 1 } },
}
for i = 1, #HANDLE_POINTS do
local point = HANDLE_POINTS[i]
local h = CreateFrame("Button", nil, m)
h:SetWidth(CORNER_SIZE)
h:SetHeight(CORNER_SIZE)
if point == "TOPLEFT" then
corners[point] = h
configureCorner(h, m, point, cornerCoords[point][1], cornerCoords[point][2])
elseif point == "TOPRIGHT" then
corners[point] = h
configureCorner(h, m, point, cornerCoords[point][1], cornerCoords[point][2])
elseif point == "BOTTOMRIGHT" then
corners[point] = h
configureCorner(h, m, point, cornerCoords[point][1], cornerCoords[point][2])
elseif point == "BOTTOMLEFT" then
corners[point] = h
configureCorner(h, m, point, cornerCoords[point][1], cornerCoords[point][2])
end
m.handles[i] = h
end
for i = 1, #HANDLE_POINTS do
local point = HANDLE_POINTS[i]
local h = m.handles[i]
if point == "TOP" then
configureEdge(h, point, corners.TOPLEFT, corners.TOPRIGHT, nil)
elseif point == "RIGHT" then
configureEdge(h, point, corners.TOPRIGHT, corners.BOTTOMRIGHT, nil)
elseif point == "BOTTOM" then
configureEdge(h, point, corners.BOTTOMLEFT, corners.BOTTOMRIGHT, { 1, 0, 0, 0, 1, 1, 0, 1 })
elseif point == "LEFT" then
configureEdge(h, point, corners.TOPLEFT, corners.BOTTOMLEFT, { 1, 0, 0, 0, 1, 1, 0, 1 })
end
h:EnableMouse(true)
h:SetScript("OnEnter", function() showHandleVisuals(h) end)
h:SetScript("OnLeave", function() hideHandleVisuals(h) end)
h:SetScript("OnMouseDown", function() startSizing(point) end)
h:SetScript("OnMouseUp", stopSizing)
hideHandleVisuals(h)
end
m:SetScript("OnMouseDown", startDragging)
m:SetScript("OnMouseUp", stopDragging)
m:Hide()
m.resizable = false
mover = m
return m
end
-- Attach the mover to a display's live region (the base "" clone). Called by the
-- options list on every selection change; the selection is force-shown (see
-- WA.SetPreview) so there's always a visible region to grab. nil/unrenderable
-- detaches.
function M.Attach(id)
local m = ensureMover()
local data = id and WeakestAurasDB.displays[id]
local region = id and WA.MoverRegion(id)
if not data or not region then
M.Detach()
return
end
m.id = id
m.region = region
m.data = data
m.isGroup = WA.IsGroup(data) or false
local pdata = data.parent and WeakestAurasDB.displays[data.parent]
m.childOfDynamic = (pdata and pdata.regionType == "dynamicgroup") or false
anchorToRegion(m, region)
-- A dynamicgroup child can't be dragged; make that visible by dimming the
-- outline (green = movable, grey = layout-controlled).
if m.childOfDynamic then
m:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.7)
else
m:SetBackdropBorderColor(1, 0.82, 0, 0.9)
end
-- Resize handles show for a leaf that carries width/height + the setters
-- (icon/progressbar) -- including a dynamicgroup child (its size is its own);
-- a group's box is derived from its children, so it isn't resizable.
m.resizable = (not m.isGroup) and data.width ~= nil and region.SetRegionWidth ~= nil
updateHandles(m)
m:SetScript("OnUpdate", moverOnUpdate)
m:Show()
end
function M.Detach()
if not mover then return end
mover.dragging = false
mover.id = nil
mover.region = nil
mover.data = nil
mover.resizable = false
updateHandles(mover)
mover:SetScript("OnUpdate", nil)
mover:Hide()
hideLines()
end