Files
Nanami-UI/Movers.lua
rucky ec9e3c29d6 完成多出修改
修复拾取界面点击无效问题
修复宠物训练界面不显示训练点问题
新增天赋分享到聊天界面
修复飞行界面无法关闭问题
修复术士宠物的显示问题
为天赋界面添加默认数据库支持
框架现在也可自主选择是否启用
玩家框架和目标框架可取消显示3D头像以及透明度修改
背包和银行也添加透明度自定义支持
优化tooltip性能和背包物品显示方式
修复布局模式在ui缩放后不能正常定位的问题
添加硬核模式危险和死亡的工会通报
添加拾取和已拾取的框体
等等
2026-03-23 10:25:25 +08:00

999 lines
36 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
mover:SetWidth(math.max(w, 72))
mover:SetHeight(math.max(h, 40))
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()
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local newL = frame:GetLeft() or 0
local newB = frame:GetBottom() or 0
local dL = newL * scale - moverL
local dB = newB * scale - moverB
if math.abs(dL) > 2 or math.abs(dB) > 2 then
SFrames:Print(string.format(
"|cffff6666[位置偏差]|r |cffaaddff%s|r anchor=%s ofs=(%.1f,%.1f) dL=%.1f dB=%.1f scale=%.2f",
entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale))
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 }
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
controlBar:SetFrameLevel(200)
controlBar:SetWidth(480)
controlBar:SetHeight(44)
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, 0)
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, 0)
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
local bx = 128
-- Snap toggle
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
local cfg = GetLayoutCfg()
cfg.snapEnabled = not cfg.snapEnabled
if controlBar._updateSnap then controlBar._updateSnap() end
end)
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
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)
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
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
M:ResetAllMovers()
end)
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
-- Close
local closeBtn = CreateFrame("Button", nil, controlBar)
closeBtn:SetWidth(60)
closeBtn:SetHeight(26)
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
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
controlBar:Hide()
return controlBar
end
--------------------------------------------------------------------------------
-- Register mover
--------------------------------------------------------------------------------
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
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,
}
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, _ in pairs(registry) do
SyncMoverToFrame(name)
local mover = moverFrames[name]
if mover then mover:Show() 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
--------------------------------------------------------------------------------
-- 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()
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
return true
else
frame:ClearAllPoints()
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
defaultX or 0, defaultY or 0)
return false
end
end
--------------------------------------------------------------------------------
-- Utility
--------------------------------------------------------------------------------
function M:GetRegistry()
return registry
end