-------------------------------------------------------------------------------- -- 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