Files
Nanami-UI/Movers.lua
2026-04-09 09:46:47 +08:00

1112 lines
40 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- Nanami-UI: Layout Mode (Mover System)
--------------------------------------------------------------------------------
SFrames.Movers = {}
local M = SFrames.Movers
local registry = {}
local moverFrames = {}
local gridFrame = nil
local controlBar = nil
local overlayFrame = nil
local isLayoutMode = false
local GRID_SPACING = 50
local SNAP_THRESHOLD = 10
local MOVER_BACKDROP = {
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
}
local MOVER_BACKDROP_2PX = {
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
edgeSize = 2,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
}
--------------------------------------------------------------------------------
-- Theme helper
--------------------------------------------------------------------------------
local function T()
return SFrames.ActiveTheme or {}
end
local function Font()
return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
end
--------------------------------------------------------------------------------
-- Config helpers
--------------------------------------------------------------------------------
local function GetLayoutCfg()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.layoutMode) ~= "table" then
SFramesDB.layoutMode = {
snapEnabled = true,
snapThreshold = SNAP_THRESHOLD,
showGrid = true,
}
end
return SFramesDB.layoutMode
end
local function EnsurePositions()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
return SFramesDB.Positions
end
--------------------------------------------------------------------------------
-- Snap logic
--------------------------------------------------------------------------------
local function GetFrameEdges(frame)
local l = frame:GetLeft() or 0
local r = frame:GetRight() or 0
local tp = frame:GetTop() or 0
local b = frame:GetBottom() or 0
return l, r, tp, b, (l + r) / 2, (tp + b) / 2
end
local function ApplySnap(mover)
local cfg = GetLayoutCfg()
if not cfg.snapEnabled then return end
if IsShiftKeyDown() then return end
local threshold = cfg.snapThreshold or SNAP_THRESHOLD
local ml, mr, mt, mb, mcx, mcy = GetFrameEdges(mover)
local mw, mh = mr - ml, mt - mb
local screenW = UIParent:GetRight() or UIParent:GetWidth()
local screenH = UIParent:GetTop() or UIParent:GetHeight()
local screenCX, screenCY = screenW / 2, screenH / 2
local snapX, snapY = nil, nil
local bestDX, bestDY = threshold + 1, threshold + 1
if math.abs(ml) < bestDX then bestDX = math.abs(ml); snapX = 0 end
if math.abs(mr - screenW) < bestDX then bestDX = math.abs(mr - screenW); snapX = screenW - mw end
if math.abs(mt - screenH) < bestDY then bestDY = math.abs(mt - screenH); snapY = screenH - mh end
if math.abs(mb) < bestDY then bestDY = math.abs(mb); snapY = 0 end
if math.abs(mcx - screenCX) < bestDX then bestDX = math.abs(mcx - screenCX); snapX = screenCX - mw / 2 end
if math.abs(mcy - screenCY) < bestDY then bestDY = math.abs(mcy - screenCY); snapY = screenCY - mh / 2 end
for name, _ in pairs(registry) do
local other = moverFrames[name]
if other and other ~= mover and other:IsShown() then
local ol, or2, ot, ob, ocx, ocy = GetFrameEdges(other)
local px = {
{ ml, or2, 0 }, { mr, ol, -mw }, { ml, ol, 0 },
{ mr, or2, -mw }, { mcx, ocx, -mw / 2 },
}
for _, p in ipairs(px) do
local d = math.abs(p[1] - p[2])
if d < bestDX then bestDX = d; snapX = p[2] + p[3] end
end
local py = {
{ math.abs(mb - ot), ot }, { math.abs(mt - ob), ob - mh },
{ math.abs(mt - ot), ot - mh }, { math.abs(mb - ob), ob },
{ math.abs(mcy - ocy), ocy - mh / 2 },
}
for _, p in ipairs(py) do
if p[1] < bestDY then bestDY = p[1]; snapY = p[2] end
end
end
end
if bestDX <= threshold and snapX then
mover:ClearAllPoints()
local curB = (bestDY <= threshold and snapY) or (mover:GetBottom() or 0)
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", snapX, curB)
return
end
if bestDY <= threshold and snapY then
mover:ClearAllPoints()
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", mover:GetLeft() or 0, snapY)
end
end
--------------------------------------------------------------------------------
-- Position save / load
--------------------------------------------------------------------------------
local function SaveMoverPosition(name, mover)
local positions = EnsurePositions()
local l = mover:GetLeft()
local b = mover:GetBottom()
local screenW = UIParent:GetRight() or UIParent:GetWidth()
local screenH = UIParent:GetTop() or UIParent:GetHeight()
if not l or not b then return end
local cx = l + (mover:GetWidth() or 0) / 2
local cy = b + (mover:GetHeight() or 0) / 2
local point, relPoint, xOfs, yOfs
if cx < screenW / 3 then
if cy > screenH * 2 / 3 then
point = "TOPLEFT"; relPoint = "TOPLEFT"
xOfs = l; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
elseif cy < screenH / 3 then
point = "BOTTOMLEFT"; relPoint = "BOTTOMLEFT"
xOfs = l; yOfs = b
else
point = "LEFT"; relPoint = "LEFT"
xOfs = l; yOfs = cy - screenH / 2
end
elseif cx > screenW * 2 / 3 then
local r = mover:GetRight() or 0
if cy > screenH * 2 / 3 then
point = "TOPRIGHT"; relPoint = "TOPRIGHT"
xOfs = r - screenW; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
elseif cy < screenH / 3 then
point = "BOTTOMRIGHT"; relPoint = "BOTTOMRIGHT"
xOfs = r - screenW; yOfs = b
else
point = "RIGHT"; relPoint = "RIGHT"
xOfs = r - screenW; yOfs = cy - screenH / 2
end
else
if cy > screenH * 2 / 3 then
point = "TOP"; relPoint = "TOP"
xOfs = cx - screenW / 2; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
elseif cy < screenH / 3 then
point = "BOTTOM"; relPoint = "BOTTOM"
xOfs = cx - screenW / 2; yOfs = b
else
point = "CENTER"; relPoint = "CENTER"
xOfs = cx - screenW / 2; yOfs = cy - screenH / 2
end
end
positions[name] = { point = point, relativePoint = relPoint, xOfs = xOfs, yOfs = yOfs }
end
--------------------------------------------------------------------------------
-- Sync mover <-> actual frame
--------------------------------------------------------------------------------
local function UpdateCoordText(mover)
if not mover or not mover._coordText then return end
local l = mover:GetLeft()
local b = mover:GetBottom()
if l and b then
mover._coordText:SetText(string.format("%.0f, %.0f", l, b))
end
end
local function SyncMoverToFrame(name)
local entry = registry[name]
local mover = moverFrames[name]
if not entry or not mover then return end
local frame = entry.frame
if not frame then return end
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local w = (frame:GetWidth() or 100) * scale
local h = (frame:GetHeight() or 50) * scale
local minW = 72
local minH = 40
if h > w * 3 then minW = 40 end
if w > h * 3 then minH = 24 end
mover:SetWidth(math.max(w, minW))
mover:SetHeight(math.max(h, minH))
local l = frame:GetLeft()
local b = frame:GetBottom()
if l and b then
mover:ClearAllPoints()
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l * scale, b * scale)
else
local positions = EnsurePositions()
local pos = positions[name]
if pos and pos.point and pos.relativePoint then
local tempF = CreateFrame("Frame", nil, UIParent)
tempF:SetWidth(w)
tempF:SetHeight(h)
tempF:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
local tl = tempF:GetLeft()
local tb = tempF:GetBottom()
if tl and tb then
mover:ClearAllPoints()
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", tl, tb)
end
tempF:Hide()
else
mover:ClearAllPoints()
local relTo = _G[entry.defaultRelativeTo] or UIParent
mover:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
entry.defaultX or 0, entry.defaultY or 0)
end
end
UpdateCoordText(mover)
end
local function SyncFrameToMover(name)
local entry = registry[name]
local mover = moverFrames[name]
if not entry or not mover then return end
local frame = entry.frame
if not frame then return end
local moverL = mover:GetLeft() or 0
local moverB = mover:GetBottom() or 0
SaveMoverPosition(name, mover)
local positions = EnsurePositions()
local pos = positions[name]
if pos then
frame:ClearAllPoints()
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
end
if entry.onMoved then entry.onMoved() end
UpdateCoordText(mover)
end
--------------------------------------------------------------------------------
-- Nudge helper
--------------------------------------------------------------------------------
local function NudgeMover(name, dx, dy)
local mover = moverFrames[name]
if not mover then return end
local l = mover:GetLeft()
local b = mover:GetBottom()
if not l or not b then return end
mover:ClearAllPoints()
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l + dx, b + dy)
SyncFrameToMover(name)
end
--------------------------------------------------------------------------------
-- Pixel-art triangle arrow helper
-- Draws a triangle using 4 horizontal/vertical strips for a clean geometric look
--------------------------------------------------------------------------------
local function CreateTriangle(parent, direction, r, g, b, a)
local strips = {}
if direction == "UP" then
local widths = { 2, 5, 8, 11 }
local heights = { 2, 2, 2, 2 }
local yOfs = { 3, 1, -1, -3 }
for i = 1, 4 do
local s = parent:CreateTexture(nil, "OVERLAY")
s:SetTexture("Interface\\Buttons\\WHITE8x8")
s:SetWidth(widths[i])
s:SetHeight(heights[i])
s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
s:SetVertexColor(r, g, b, a)
table.insert(strips, s)
end
elseif direction == "DOWN" then
local widths = { 11, 8, 5, 2 }
local heights = { 2, 2, 2, 2 }
local yOfs = { 3, 1, -1, -3 }
for i = 1, 4 do
local s = parent:CreateTexture(nil, "OVERLAY")
s:SetTexture("Interface\\Buttons\\WHITE8x8")
s:SetWidth(widths[i])
s:SetHeight(heights[i])
s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
s:SetVertexColor(r, g, b, a)
table.insert(strips, s)
end
elseif direction == "LEFT" then
local widths = { 2, 2, 2, 2 }
local heights = { 2, 5, 8, 11 }
local xOfs = { -3, -1, 1, 3 }
for i = 1, 4 do
local s = parent:CreateTexture(nil, "OVERLAY")
s:SetTexture("Interface\\Buttons\\WHITE8x8")
s:SetWidth(widths[i])
s:SetHeight(heights[i])
s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
s:SetVertexColor(r, g, b, a)
table.insert(strips, s)
end
elseif direction == "RIGHT" then
local widths = { 2, 2, 2, 2 }
local heights = { 11, 8, 5, 2 }
local xOfs = { -3, -1, 1, 3 }
for i = 1, 4 do
local s = parent:CreateTexture(nil, "OVERLAY")
s:SetTexture("Interface\\Buttons\\WHITE8x8")
s:SetWidth(widths[i])
s:SetHeight(heights[i])
s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
s:SetVertexColor(r, g, b, a)
table.insert(strips, s)
end
end
return strips
end
local function SetTriangleColor(strips, r, g, b, a)
for _, s in ipairs(strips) do
s:SetVertexColor(r, g, b, a)
end
end
--------------------------------------------------------------------------------
-- Arrow nudge button (triangle-based)
--------------------------------------------------------------------------------
local ARROW_SIZE = 16
local ARROW_REPEAT_DELAY = 0.45
local ARROW_REPEAT_RATE = 0.04
local function CreateArrowButton(parent, moverName, anchor, ofsX, ofsY, direction, dx, dy)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(ARROW_SIZE)
btn:SetHeight(ARROW_SIZE)
btn:SetPoint(anchor, parent, anchor, ofsX, ofsY)
btn:SetFrameLevel(parent:GetFrameLevel() + 5)
btn:SetMovable(true)
btn:RegisterForDrag("LeftButton")
local th = T()
local dimC = th.dimText or { 0.5, 0.5, 0.55 }
local accentC = th.accent or { 1, 0.5, 0.8 }
local bgTex = btn:CreateTexture(nil, "BACKGROUND")
bgTex:SetTexture("Interface\\Buttons\\WHITE8x8")
bgTex:SetAllPoints(btn)
bgTex:SetVertexColor(0, 0, 0, 0)
btn._bg = bgTex
local tri = CreateTriangle(btn, direction, dimC[1], dimC[2], dimC[3], 0.55)
btn._tri = tri
btn._elapsed = 0
btn._held = false
btn._dragging = false
btn._delay = ARROW_REPEAT_DELAY
btn:SetScript("OnClick", function()
if not this._dragging then
NudgeMover(moverName, dx, dy)
end
this._dragging = false
end)
btn:SetScript("OnMouseDown", function()
this._held = true
this._dragging = false
this._elapsed = 0
this._delay = ARROW_REPEAT_DELAY
end)
btn:SetScript("OnMouseUp", function()
this._held = false
end)
btn:SetScript("OnDragStart", function()
this._held = false
this._dragging = true
parent:StartMoving()
end)
btn:SetScript("OnDragStop", function()
parent:StopMovingOrSizing()
ApplySnap(parent)
SyncFrameToMover(moverName)
this._dragging = false
end)
btn:SetScript("OnUpdate", function()
if not this._held then return end
this._elapsed = this._elapsed + arg1
if this._elapsed >= this._delay then
this._elapsed = 0
this._delay = ARROW_REPEAT_RATE
NudgeMover(moverName, dx, dy)
end
end)
btn:SetScript("OnEnter", function()
this._bg:SetVertexColor(accentC[1], accentC[2], accentC[3], 0.18)
SetTriangleColor(this._tri, 1, 1, 1, 1)
if parent._SetHover then parent:_SetHover(true) end
end)
btn:SetScript("OnLeave", function()
this._held = false
this._bg:SetVertexColor(0, 0, 0, 0)
SetTriangleColor(this._tri, dimC[1], dimC[2], dimC[3], 0.55)
if parent._SetHover then parent:_SetHover(false) end
end)
return btn
end
--------------------------------------------------------------------------------
-- Create individual mover frame
--------------------------------------------------------------------------------
local function CreateMoverFrame(name, entry)
if moverFrames[name] then return moverFrames[name] end
local mover = CreateFrame("Button", "SFramesMover_" .. name, UIParent)
mover:SetFrameStrata("FULLSCREEN")
mover:SetFrameLevel(100)
mover:SetWidth(100)
mover:SetHeight(40)
mover:SetClampedToScreen(true)
mover:SetMovable(true)
mover:EnableMouse(true)
mover:RegisterForDrag("LeftButton")
mover:RegisterForClicks("RightButtonUp")
mover:SetBackdrop(MOVER_BACKDROP_2PX)
local th = T()
local secBg = th.sectionBg or { 0.12, 0.10, 0.18, 0.82 }
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
local accentLine = th.accentLine or { 0.6, 0.9, 1, 0.9 }
mover:SetBackdropColor(secBg[1], secBg[2], secBg[3], secBg[4] or 0.82)
mover:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)
-- Top accent stripe
local stripe = mover:CreateTexture(nil, "ARTWORK")
stripe:SetTexture("Interface\\Buttons\\WHITE8x8")
stripe:SetHeight(2)
stripe:SetPoint("TOPLEFT", mover, "TOPLEFT", 2, -2)
stripe:SetPoint("TOPRIGHT", mover, "TOPRIGHT", -2, -2)
stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
mover._stripe = stripe
-- Label
local label = mover:CreateFontString(nil, "OVERLAY")
label:SetFont(Font(), 11, "OUTLINE")
label:SetPoint("CENTER", mover, "CENTER", 0, 5)
label:SetText(entry.label or name)
local titleC = th.title or { 1, 0.9, 1 }
label:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
mover._label = label
-- Coordinate readout
local coord = mover:CreateFontString(nil, "OVERLAY")
coord:SetFont(Font(), 9, "OUTLINE")
coord:SetPoint("CENTER", mover, "CENTER", 0, -7)
local dimC = th.dimText or { 0.5, 0.5, 0.55 }
coord:SetTextColor(dimC[1], dimC[2], dimC[3], 0.9)
coord:SetText("")
mover._coordText = coord
-- Triangle arrow buttons
CreateArrowButton(mover, name, "TOP", 0, -1, "UP", 0, 1)
CreateArrowButton(mover, name, "BOTTOM", 0, 1, "DOWN", 0, -1)
CreateArrowButton(mover, name, "LEFT", 1, 0, "LEFT", -1, 0)
CreateArrowButton(mover, name, "RIGHT", -1, 0, "RIGHT", 1, 0)
-- Hover state manager (tracks child enter/leave)
local hoverCount = 0
function mover:_SetHover(isEnter)
if isEnter then
hoverCount = hoverCount + 1
else
hoverCount = hoverCount - 1
if hoverCount < 0 then hoverCount = 0 end
end
if hoverCount > 0 then
self:SetBackdropBorderColor(1, 1, 1, 0.95)
stripe:SetVertexColor(1, 1, 1, 1)
else
self:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)
stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
end
end
mover:SetScript("OnDragStart", function()
this:StartMoving()
end)
mover:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
ApplySnap(this)
SyncFrameToMover(name)
end)
mover:SetScript("OnClick", function()
if arg1 == "RightButton" then
M:ResetMover(name)
end
end)
mover:SetScript("OnEnter", function()
this:_SetHover(true)
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine(entry.label or name, 1, 0.84, 0.94)
GameTooltip:AddDoubleLine("拖拽", "移动位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
GameTooltip:AddDoubleLine("箭头", "微调 1 像素", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
GameTooltip:AddDoubleLine("右键", "重置位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
GameTooltip:AddDoubleLine("Shift+拖拽", "禁用磁吸", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
mover:SetScript("OnLeave", function()
this:_SetHover(false)
GameTooltip:Hide()
end)
mover:Hide()
moverFrames[name] = mover
return mover
end
--------------------------------------------------------------------------------
-- Dark overlay
--------------------------------------------------------------------------------
local function CreateOverlay()
if overlayFrame then return overlayFrame end
overlayFrame = CreateFrame("Frame", "SFramesLayoutOverlay", UIParent)
overlayFrame:SetFrameStrata("DIALOG")
overlayFrame:SetFrameLevel(0)
overlayFrame:SetAllPoints(UIParent)
overlayFrame:EnableMouse(false)
local bg = overlayFrame:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Buttons\\WHITE8x8")
bg:SetAllPoints(overlayFrame)
bg:SetVertexColor(0, 0, 0, 0.45)
overlayFrame._bg = bg
overlayFrame:Hide()
return overlayFrame
end
--------------------------------------------------------------------------------
-- Grid overlay
--------------------------------------------------------------------------------
local function CreateGrid()
if gridFrame then return gridFrame end
gridFrame = CreateFrame("Frame", "SFramesLayoutGrid", UIParent)
gridFrame:SetFrameStrata("DIALOG")
gridFrame:SetFrameLevel(1)
gridFrame:SetAllPoints(UIParent)
gridFrame:EnableMouse(false)
gridFrame._lines = {}
local th = T()
local axisColor = th.accent or { 1, 0.5, 0.8 }
local lineColor = th.dimText or { 0.5, 0.5, 0.55 }
local function MakeLine(r, g, b, a)
local tex = gridFrame:CreateTexture(nil, "BACKGROUND")
tex:SetTexture("Interface\\Buttons\\WHITE8x8")
tex:SetVertexColor(r, g, b, a)
table.insert(gridFrame._lines, tex)
return tex
end
local screenW = UIParent:GetRight() or UIParent:GetWidth()
local screenH = UIParent:GetTop() or UIParent:GetHeight()
local halfW = math.floor(screenW / 2)
local halfH = math.floor(screenH / 2)
local cv = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
cv:SetWidth(1)
cv:SetPoint("TOP", gridFrame, "TOP", 0, 0)
cv:SetPoint("BOTTOM", gridFrame, "BOTTOM", 0, 0)
local ch = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
ch:SetHeight(1)
ch:SetPoint("LEFT", gridFrame, "LEFT", 0, 0)
ch:SetPoint("RIGHT", gridFrame, "RIGHT", 0, 0)
local i = GRID_SPACING
while i < halfW do
local vl = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
vl:SetWidth(1)
vl:SetPoint("TOP", gridFrame, "TOP", -i, 0)
vl:SetPoint("BOTTOM", gridFrame, "BOTTOM", -i, 0)
local vr = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
vr:SetWidth(1)
vr:SetPoint("TOP", gridFrame, "TOP", i, 0)
vr:SetPoint("BOTTOM", gridFrame, "BOTTOM", i, 0)
i = i + GRID_SPACING
end
i = GRID_SPACING
while i < halfH do
local ha = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
ha:SetHeight(1)
ha:SetPoint("LEFT", gridFrame, "LEFT", 0, i)
ha:SetPoint("RIGHT", gridFrame, "RIGHT", 0, i)
local hb = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
hb:SetHeight(1)
hb:SetPoint("LEFT", gridFrame, "LEFT", 0, -i)
hb:SetPoint("RIGHT", gridFrame, "RIGHT", 0, -i)
i = i + GRID_SPACING
end
gridFrame:Hide()
return gridFrame
end
--------------------------------------------------------------------------------
-- Rounded button helper (for control bar) matches UI-wide rounded style
--------------------------------------------------------------------------------
local ROUND_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
}
local function MakeControlButton(parent, text, width, xOfs, onClick)
local th = T()
local btnBg = th.buttonBg or { 0.16, 0.12, 0.22, 0.94 }
local btnBd = th.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
local btnHBg = th.buttonHoverBg or { 0.22, 0.18, 0.30, 0.96 }
local btnDnBg = th.buttonDownBg or { 0.08, 0.04, 0.06, 0.95 }
local btnText = th.buttonText or { 0.85, 0.82, 0.92 }
local btnHTxt = th.buttonActiveText or { 1, 0.92, 0.96 }
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(width)
btn:SetHeight(26)
btn:SetPoint("LEFT", parent, "LEFT", xOfs, 0)
btn:SetBackdrop(ROUND_BACKDROP)
btn:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
btn:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)
local fs = btn:CreateFontString(nil, "OVERLAY")
fs:SetFont(Font(), 10, "OUTLINE")
fs:SetPoint("CENTER", 0, 0)
fs:SetText(text)
fs:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
btn._text = fs
btn:SetScript("OnClick", onClick)
btn:SetScript("OnEnter", function()
this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
local a = th.accent or { 1, 0.5, 0.8 }
this:SetBackdropBorderColor(a[1], a[2], a[3], 0.95)
this._text:SetTextColor(btnHTxt[1], btnHTxt[2], btnHTxt[3], 1)
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
this:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)
this._text:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(btnDnBg[1], btnDnBg[2], btnDnBg[3], btnDnBg[4] or 0.95)
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
end)
return btn
end
--------------------------------------------------------------------------------
-- Control bar
--------------------------------------------------------------------------------
local function CreateControlBar()
if controlBar then return controlBar end
local th = T()
local panelBg = th.headerBg or th.panelBg or { 0.08, 0.06, 0.12, 0.98 }
local panelBd = th.panelBorder or { 0.35, 0.30, 0.50, 0.90 }
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
local titleC = th.title or { 1, 0.88, 1 }
local ROW_Y_TOP = 12
local ROW_Y_BOT = -12
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
controlBar:SetFrameLevel(200)
controlBar:SetWidth(480)
controlBar:SetHeight(72)
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
controlBar:SetClampedToScreen(true)
controlBar:SetMovable(true)
controlBar:EnableMouse(true)
controlBar:RegisterForDrag("LeftButton")
controlBar:SetScript("OnDragStart", function() this:StartMoving() end)
controlBar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
controlBar:SetBackdrop(ROUND_BACKDROP)
controlBar:SetBackdropColor(panelBg[1], panelBg[2], panelBg[3], panelBg[4] or 0.98)
controlBar:SetBackdropBorderColor(panelBd[1], panelBd[2], panelBd[3], panelBd[4] or 0.90)
local title = controlBar:CreateFontString(nil, "OVERLAY")
title:SetFont(Font(), 13, "OUTLINE")
title:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_TOP)
title:SetText("Nanami 布局")
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
local sep = controlBar:CreateTexture(nil, "ARTWORK")
sep:SetTexture("Interface\\Buttons\\WHITE8x8")
sep:SetWidth(1)
sep:SetHeight(24)
sep:SetPoint("LEFT", controlBar, "LEFT", 118, ROW_Y_TOP)
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
local bx = 128
-- Snap toggle (row 1)
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
local cfg = GetLayoutCfg()
cfg.snapEnabled = not cfg.snapEnabled
if controlBar._updateSnap then controlBar._updateSnap() end
end)
snapBtn:ClearAllPoints()
snapBtn:SetPoint("LEFT", controlBar, "LEFT", bx, ROW_Y_TOP)
controlBar.snapBtn = snapBtn
local function UpdateSnapBtnText()
local cfg = GetLayoutCfg()
local a2 = T().accent or { 1, 0.5, 0.8 }
if cfg.snapEnabled then
snapBtn._text:SetText("|cff66ee66开|r 磁吸")
snapBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
else
snapBtn._text:SetText("|cffee6666关|r 磁吸")
snapBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
end
end
controlBar._updateSnap = UpdateSnapBtnText
-- Grid toggle (row 1)
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
local cfg = GetLayoutCfg()
cfg.showGrid = not cfg.showGrid
if controlBar._updateGrid then controlBar._updateGrid() end
if gridFrame then
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
end
end)
gridBtn:ClearAllPoints()
gridBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 84, ROW_Y_TOP)
controlBar.gridBtn = gridBtn
local function UpdateGridBtnText()
local cfg = GetLayoutCfg()
local a2 = T().accent or { 1, 0.5, 0.8 }
if cfg.showGrid then
gridBtn._text:SetText("|cff66ee66开|r 网格")
gridBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
else
gridBtn._text:SetText("|cffee6666关|r 网格")
gridBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
end
end
controlBar._updateGrid = UpdateGridBtnText
-- Reset all (row 1)
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
M:ResetAllMovers()
end)
resetBtn:ClearAllPoints()
resetBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 176, ROW_Y_TOP)
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
-- Close (row 1)
local closeBtn = CreateFrame("Button", nil, controlBar)
closeBtn:SetWidth(60)
closeBtn:SetHeight(26)
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, ROW_Y_TOP)
closeBtn:SetBackdrop(ROUND_BACKDROP)
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
local closeText = closeBtn:CreateFontString(nil, "OVERLAY")
closeText:SetFont(Font(), 10, "OUTLINE")
closeText:SetPoint("CENTER", 0, 0)
closeText:SetText("关闭")
closeText:SetTextColor(1, 0.65, 0.65, 1)
closeBtn:SetScript("OnClick", function() M:ExitLayoutMode() end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
this:SetBackdropBorderColor(1, 0.35, 0.40, 1)
closeText:SetTextColor(1, 1, 1, 1)
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
this:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
closeText:SetTextColor(1, 0.65, 0.65, 1)
end)
closeBtn:SetScript("OnMouseDown", function()
this:SetBackdropColor(0.25, 0.04, 0.06, 0.98)
end)
closeBtn:SetScript("OnMouseUp", function()
this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
end)
controlBar.closeBtn = closeBtn
-- Row separator
local rowSep = controlBar:CreateTexture(nil, "ARTWORK")
rowSep:SetTexture("Interface\\Buttons\\WHITE8x8")
rowSep:SetHeight(1)
rowSep:SetPoint("LEFT", controlBar, "LEFT", 10, 0)
rowSep:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
rowSep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.35)
-- Row 2: Preset buttons
local presetLabel = controlBar:CreateFontString(nil, "OVERLAY")
presetLabel:SetFont(Font(), 10, "OUTLINE")
presetLabel:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_BOT)
presetLabel:SetText("预设:")
presetLabel:SetTextColor(0.7, 0.68, 0.78, 1)
controlBar._presetBtns = {}
local AB = SFrames.ActionBars
local presets = AB and AB.PRESETS or {}
local px = 56
for idx = 1, 3 do
local p = presets[idx]
local pName = p and p.name or ("方案" .. idx)
local pDesc = p and p.desc or ""
local pId = idx
local pbtn = MakeControlButton(controlBar, pName, 80, px + (idx - 1) * 88, function()
if AB and AB.ApplyPreset then
AB:ApplyPreset(pId)
end
end)
pbtn:ClearAllPoints()
pbtn:SetPoint("LEFT", controlBar, "LEFT", px + (idx - 1) * 88, ROW_Y_BOT)
pbtn._text:SetFont(Font(), 9, "OUTLINE")
pbtn._presetId = pId
pbtn:SetScript("OnEnter", function()
local a2 = T().accent or { 1, 0.5, 0.8 }
this:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.95)
this._text:SetTextColor(1, 1, 1, 1)
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine(pName, 1, 0.85, 0.55)
GameTooltip:AddLine(pDesc, 0.75, 0.75, 0.85, true)
GameTooltip:Show()
end)
pbtn:SetScript("OnLeave", function()
local th2 = T()
local bd2 = th2.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
local tc2 = th2.buttonText or { 0.85, 0.82, 0.92 }
this:SetBackdropColor(th2.buttonBg and th2.buttonBg[1] or 0.16,
th2.buttonBg and th2.buttonBg[2] or 0.12,
th2.buttonBg and th2.buttonBg[3] or 0.22, 0.94)
this:SetBackdropBorderColor(bd2[1], bd2[2], bd2[3], bd2[4] or 0.90)
this._text:SetTextColor(tc2[1], tc2[2], tc2[3], 1)
GameTooltip:Hide()
end)
controlBar._presetBtns[idx] = pbtn
end
controlBar:Hide()
return controlBar
end
--------------------------------------------------------------------------------
-- Register mover
--------------------------------------------------------------------------------
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved, opts)
if not name or not frame then return end
registry[name] = {
frame = frame,
label = label or name,
defaultPoint = defaultPoint or "CENTER",
defaultRelativeTo = defaultRelativeTo or "UIParent",
defaultRelPoint = defaultRelPoint or "CENTER",
defaultX = defaultX or 0,
defaultY = defaultY or 0,
onMoved = onMoved,
alwaysShowInLayout = opts and opts.alwaysShowInLayout or false,
}
CreateMoverFrame(name, registry[name])
end
--------------------------------------------------------------------------------
-- Enter / Exit layout mode
--------------------------------------------------------------------------------
function M:EnterLayoutMode()
if isLayoutMode then return end
isLayoutMode = true
if SFrames.ConfigUI and SFramesConfigPanel and SFramesConfigPanel:IsShown() then
SFramesConfigPanel:Hide()
end
CreateOverlay()
CreateGrid()
CreateControlBar()
overlayFrame:Show()
local cfg = GetLayoutCfg()
if cfg.showGrid then gridFrame:Show() end
if controlBar._updateSnap then controlBar._updateSnap() end
if controlBar._updateGrid then controlBar._updateGrid() end
controlBar:Show()
SFrames:Print(string.format(
"|cff66eeff[布局]|r UIScale=%.2f screenW=%.0f screenH=%.0f (GetRight=%.0f GetTop=%.0f)",
UIParent:GetEffectiveScale(),
UIParent:GetWidth(), UIParent:GetHeight(),
UIParent:GetRight() or 0, UIParent:GetTop() or 0))
for name, entry in pairs(registry) do
local frame = entry and entry.frame
local shouldShow = entry.alwaysShowInLayout
or (frame and frame.IsShown and frame:IsShown())
SyncMoverToFrame(name)
local mover = moverFrames[name]
if mover then
if shouldShow then
mover:Show()
else
mover:Hide()
end
end
end
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
end
function M:ExitLayoutMode()
if not isLayoutMode then return end
isLayoutMode = false
for name, _ in pairs(registry) do
SyncFrameToMover(name)
local mover = moverFrames[name]
if mover then mover:Hide() end
end
if gridFrame then gridFrame:Hide() end
if controlBar then controlBar:Hide() end
if overlayFrame then overlayFrame:Hide() end
SFrames:Print("布局模式已关闭 - 所有位置已保存")
end
function M:ToggleLayoutMode()
if isLayoutMode then self:ExitLayoutMode() else self:EnterLayoutMode() end
end
function M:IsLayoutMode()
return isLayoutMode
end
function M:SetMoverAlwaysShow(name, alwaysShow)
local entry = registry[name]
if entry then
entry.alwaysShowInLayout = alwaysShow
end
local mover = moverFrames[name]
if mover and isLayoutMode then
if alwaysShow then
SyncMoverToFrame(name)
mover:Show()
else
mover:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Reset movers
--------------------------------------------------------------------------------
function M:ResetMover(name)
local entry = registry[name]
if not entry then return end
local positions = EnsurePositions()
positions[name] = nil
local frame = entry.frame
if frame then
frame:ClearAllPoints()
local relTo = _G[entry.defaultRelativeTo] or UIParent
frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
entry.defaultX, entry.defaultY)
end
if entry.onMoved then entry.onMoved() end
if isLayoutMode then SyncMoverToFrame(name) end
SFrames:Print((entry.label or name) .. " 位置已重置")
end
function M:ResetAllMovers()
for name, _ in pairs(registry) do
local entry = registry[name]
local positions = EnsurePositions()
positions[name] = nil
local frame = entry.frame
if frame then
frame:ClearAllPoints()
local relTo = _G[entry.defaultRelativeTo] or UIParent
frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
entry.defaultX, entry.defaultY)
end
if entry.onMoved then entry.onMoved() end
end
if isLayoutMode then
for name, _ in pairs(registry) do SyncMoverToFrame(name) end
end
SFrames:Print("所有位置已重置为默认")
end
--------------------------------------------------------------------------------
-- Apply saved position to a frame (for modules to call on init)
--------------------------------------------------------------------------------
function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoint, defaultX, defaultY)
local positions = EnsurePositions()
local pos = positions[name]
if pos and pos.point and pos.relativePoint then
frame:ClearAllPoints()
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(pos.point, UIParent, pos.relativePoint,
(pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
else
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
return true
else
frame:ClearAllPoints()
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
if relFrame == UIParent then
local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
(defaultX or 0) / fScale, (defaultY or 0) / fScale)
else
frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
end
else
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
end
return false
end
end
--------------------------------------------------------------------------------
-- Utility
--------------------------------------------------------------------------------
function M:GetRegistry()
return registry
end
function M:SyncMoverToFrame(name)
SyncMoverToFrame(name)
end