完成多出修改

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

998
Movers.lua Normal file
View File

@@ -0,0 +1,998 @@
--------------------------------------------------------------------------------
-- 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