Files
TurtleColorPicker/UI.lua
T
2026-09-12 17:59:32 +02:00

532 lines
16 KiB
Lua

-- TurtleColorPicker UI
-- WoW 1.12 / Lua 5.0
-- All state is stored in the F table to avoid 32-upvalue limit.
local TCP = TurtleColorPicker
local F = {
initialized = false,
session = nil,
draggingSV = false,
draggingHue = false,
hexHasFocus = false,
main = nil,
svFrame = nil, svBase = nil, svCursor = nil,
hueFrame = nil, hueCursor = nil,
oldSwatch = nil, newSwatch = nil, previewBorder = nil,
hexContainer = nil, hexEditBox = nil,
okBtn = nil, cancelBtn = nil,
}
-- Layout constants
F.SV_SIZE = 150
F.HUE_WIDTH = 20
F.HUE_HEIGHT = 150
F.PAD = 12
F.SPACING = 8
F.SWATCH_H = 22
F.BTN_W = 55
F.BTN_H = 22
-- Frame width: PAD | SV | GAP | HUE | PAD
F.HUE_GAP = 8
F.CONTENT_W = F.SV_SIZE + F.HUE_GAP + F.HUE_WIDTH
F.FRAME_W = F.CONTENT_W + F.PAD * 2
F.HUE_COLORS = {
{1, 0, 0},
{1, 1, 0},
{0, 1, 0},
{0, 1, 1},
{0, 0, 1},
{1, 0, 1},
{1, 0, 0},
}
-- ============================================================
-- CURSOR
-- ============================================================
local function CreateCrosshairCursor(parent, size)
local f = CreateFrame("Frame", nil, parent)
f:SetWidth(size)
f:SetHeight(size)
f:SetFrameLevel(parent:GetFrameLevel() + 5)
local len = (size / 2) - 2
-- Black shadow (drawn first, behind white)
local dirs = {
{"RIGHT", "CENTER", -1, 0, len + 1, 3},
{"LEFT", "CENTER", 1, 0, len + 1, 3},
{"BOTTOM","CENTER", 0, 1, 3, len + 1},
{"TOP", "CENTER", 0,-1, 3, len + 1},
}
for i = 1, 4 do
local d = dirs[i]
local t = f:CreateTexture(nil, "ARTWORK")
t:SetWidth(d[5])
t:SetHeight(d[6])
t:SetPoint(d[1], f, d[2], d[3], d[4])
t:SetTexture(0, 0, 0, 0.5)
end
-- White cross
local wdirs = {
{"RIGHT", "CENTER", -2, 0, len, 1},
{"LEFT", "CENTER", 2, 0, len, 1},
{"BOTTOM","CENTER", 0, 2, 1, len},
{"TOP", "CENTER", 0,-2, 1, len},
}
for i = 1, 4 do
local d = wdirs[i]
local t = f:CreateTexture(nil, "OVERLAY")
t:SetWidth(d[5])
t:SetHeight(d[6])
t:SetPoint(d[1], f, d[2], d[3], d[4])
t:SetTexture(1, 1, 1, 0.85)
end
return f
end
-- ============================================================
-- UPDATE
-- ============================================================
local function UpdateColor()
local s = F.session
if not s then return end
local r, g, b = TCP.HSVtoRGB(s.h, s.s, s.v)
local hr, hg, hb = TCP.HSVtoRGB(s.h, 1, 1)
F.svBase:SetTexture(hr, hg, hb, 1)
F.svCursor:ClearAllPoints()
F.svCursor:SetPoint("CENTER", F.svFrame, "BOTTOMLEFT", s.s * F.SV_SIZE, s.v * F.SV_SIZE)
F.hueCursor:ClearAllPoints()
F.hueCursor:SetPoint("CENTER", F.hueFrame, "TOPLEFT", F.HUE_WIDTH / 2, -(s.h * F.HUE_HEIGHT))
F.newSwatch:SetTexture(r, g, b, 1)
if F.hexEditBox and F.hexEditBox:IsShown() then
if not F.hexHasFocus then
F.hexEditBox:SetText(TCP.RGBtoHex(r, g, b))
end
end
TCP:_NotifyChange()
end
local function UpdateSVFromMouse()
local s = F.session
if not s then return end
local cx, cy = GetCursorPosition()
local scale = F.svFrame:GetEffectiveScale()
cx = cx / scale
cy = cy / scale
local newS = TCP.Clamp((cx - F.svFrame:GetLeft()) / F.SV_SIZE, 0, 1)
local newV = TCP.Clamp((cy - F.svFrame:GetBottom()) / F.SV_SIZE, 0, 1)
if newS == s.s and newV == s.v then return end
s.s = newS
s.v = newV
UpdateColor()
end
local function UpdateHueFromMouse()
local s = F.session
if not s then return end
local cx, cy = GetCursorPosition()
local scale = F.hueFrame:GetEffectiveScale()
cy = cy / scale
local newH = TCP.Clamp((F.hueFrame:GetTop() - cy) / F.HUE_HEIGHT, 0, 0.9999)
if newH == s.h then return end
s.h = newH
UpdateColor()
end
-- ============================================================
-- LAYOUT (forward declaration, defined after Init)
-- ============================================================
local RecalcLayout
-- ============================================================
-- INIT
-- ============================================================
local function Init()
if F.initialized then return end
F.initialized = true
-- Main Frame
F.main = CreateFrame("Frame", "TurtleColorPickerFrame", UIParent)
F.main:SetFrameStrata("FULLSCREEN_DIALOG")
F.main:SetWidth(F.FRAME_W)
F.main:SetHeight(300)
F.main:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
F.main:SetMovable(true)
F.main:EnableMouse(true)
F.main:RegisterForDrag("LeftButton")
F.main:SetClampedToScreen(true)
F.main:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
F.main:SetBackdropColor(0.06, 0.06, 0.06, 0.92)
F.main:SetBackdropBorderColor(0.45, 0.45, 0.45, 1)
F.main:Hide()
F.main:SetScript("OnDragStart", function() this:StartMoving() end)
F.main:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
table.insert(UISpecialFrames, "TurtleColorPickerFrame")
F.main:SetScript("OnHide", function()
F.draggingSV = false
F.draggingHue = false
TCP:_NotifyCancel()
F.session = nil
TCP._session = nil
end)
F.main:SetScript("OnUpdate", function()
if F.draggingSV then
UpdateSVFromMouse()
elseif F.draggingHue then
UpdateHueFromMouse()
end
end)
-- Title bar
local title = F.main:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
title:SetPoint("TOP", F.main, "TOP", 0, -4)
title:SetText("Color Picker")
title:SetTextColor(0.8, 0.8, 0.8, 0.5)
-- Offset top content below mini-title
local topOff = F.PAD + 8
-- SV Square
F.svFrame = CreateFrame("Button", nil, F.main)
F.svFrame:SetWidth(F.SV_SIZE)
F.svFrame:SetHeight(F.SV_SIZE)
F.svFrame:SetPoint("TOPLEFT", F.main, "TOPLEFT", F.PAD, -topOff)
F.svFrame:RegisterForClicks("LeftButtonDown", "LeftButtonUp")
F.svBase = F.svFrame:CreateTexture(nil, "BACKGROUND")
F.svBase:SetAllPoints(F.svFrame)
F.svBase:SetTexture(1, 0, 0, 1)
local svWhite = F.svFrame:CreateTexture(nil, "BORDER")
svWhite:SetAllPoints(F.svFrame)
svWhite:SetTexture(1, 1, 1, 1)
svWhite:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 1, 1, 1, 1, 0)
local svBlack = F.svFrame:CreateTexture(nil, "ARTWORK")
svBlack:SetAllPoints(F.svFrame)
svBlack:SetTexture(0, 0, 0, 1)
svBlack:SetGradientAlpha("VERTICAL", 0, 0, 0, 1, 0, 0, 0, 0)
local svBorder = CreateFrame("Frame", nil, F.svFrame)
svBorder:SetPoint("TOPLEFT", F.svFrame, "TOPLEFT", -1, 1)
svBorder:SetPoint("BOTTOMRIGHT", F.svFrame, "BOTTOMRIGHT", 1, -1)
svBorder:SetFrameLevel(F.svFrame:GetFrameLevel() + 3)
svBorder:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 9,
})
svBorder:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.7)
F.svCursor = CreateCrosshairCursor(F.svFrame, 15)
F.svFrame:SetScript("OnMouseDown", function()
F.draggingSV = true
UpdateSVFromMouse()
end)
F.svFrame:SetScript("OnMouseUp", function()
F.draggingSV = false
end)
-- Hue Bar
F.hueFrame = CreateFrame("Button", nil, F.main)
F.hueFrame:SetWidth(F.HUE_WIDTH)
F.hueFrame:SetHeight(F.HUE_HEIGHT)
F.hueFrame:SetPoint("TOPLEFT", F.svFrame, "TOPRIGHT", F.HUE_GAP, 0)
F.hueFrame:RegisterForClicks("LeftButtonDown", "LeftButtonUp")
local segH = F.HUE_HEIGHT / 6
for i = 1, 6 do
local seg = F.hueFrame:CreateTexture(nil, "ARTWORK")
seg:SetWidth(F.HUE_WIDTH)
seg:SetHeight(segH)
seg:SetPoint("TOPLEFT", F.hueFrame, "TOPLEFT", 0, -(i - 1) * segH)
seg:SetTexture(1, 1, 1, 1)
local t = F.HUE_COLORS[i]
local b = F.HUE_COLORS[i + 1]
seg:SetGradient("VERTICAL", b[1], b[2], b[3], t[1], t[2], t[3])
end
local hueBorder = CreateFrame("Frame", nil, F.hueFrame)
hueBorder:SetPoint("TOPLEFT", F.hueFrame, "TOPLEFT", -1, 1)
hueBorder:SetPoint("BOTTOMRIGHT", F.hueFrame, "BOTTOMRIGHT", 1, -1)
hueBorder:SetFrameLevel(F.hueFrame:GetFrameLevel() + 3)
hueBorder:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 9,
})
hueBorder:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.7)
-- Hue Cursor (small arrows / bar)
F.hueCursor = CreateFrame("Frame", nil, F.hueFrame)
F.hueCursor:SetWidth(F.HUE_WIDTH + 4)
F.hueCursor:SetHeight(5)
F.hueCursor:SetFrameLevel(F.hueFrame:GetFrameLevel() + 5)
local hcBg = F.hueCursor:CreateTexture(nil, "ARTWORK")
hcBg:SetAllPoints(F.hueCursor)
hcBg:SetTexture(0, 0, 0, 0.8)
local hcInner = F.hueCursor:CreateTexture(nil, "OVERLAY")
hcInner:SetPoint("TOPLEFT", F.hueCursor, "TOPLEFT", 1, -1)
hcInner:SetPoint("BOTTOMRIGHT", F.hueCursor, "BOTTOMRIGHT", -1, 1)
hcInner:SetTexture(1, 1, 1, 0.85)
F.hueFrame:SetScript("OnMouseDown", function()
F.draggingHue = true
UpdateHueFromMouse()
end)
F.hueFrame:SetScript("OnMouseUp", function()
F.draggingHue = false
end)
F._topOff = topOff
-- Preview Swatches
F.previewBorder = CreateFrame("Frame", nil, F.main)
F.previewBorder:SetHeight(F.SWATCH_H)
F.oldSwatch = F.previewBorder:CreateTexture(nil, "ARTWORK")
F.oldSwatch:SetHeight(F.SWATCH_H)
F.oldSwatch:SetPoint("TOPLEFT", F.previewBorder, "TOPLEFT", 0, 0)
F.oldSwatch:SetTexture(1, 1, 1, 1)
F.newSwatch = F.previewBorder:CreateTexture(nil, "ARTWORK")
F.newSwatch:SetHeight(F.SWATCH_H)
F.newSwatch:SetPoint("TOPRIGHT", F.previewBorder, "TOPRIGHT", 0, 0)
F.newSwatch:SetTexture(1, 0, 0, 1)
-- Thin divider between swatches
local swatchDiv = F.previewBorder:CreateTexture(nil, "OVERLAY")
swatchDiv:SetWidth(1)
swatchDiv:SetHeight(F.SWATCH_H)
swatchDiv:SetPoint("CENTER", F.previewBorder, "CENTER", 0, 0)
swatchDiv:SetTexture(0.15, 0.15, 0.15, 1)
-- Hex Input
F.hexContainer = CreateFrame("Frame", nil, F.main)
F.hexContainer:SetHeight(F.BTN_H)
F.hexContainer:Hide()
local hexLabel = F.hexContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
hexLabel:SetPoint("LEFT", F.hexContainer, "LEFT", 0, 0)
hexLabel:SetText("#")
hexLabel:SetTextColor(0.6, 0.6, 0.6, 1)
F.hexEditBox = CreateFrame("EditBox", "TurtleColorPickerHexInput", F.hexContainer)
F.hexEditBox:SetWidth(48)
F.hexEditBox:SetHeight(F.BTN_H)
F.hexEditBox:SetPoint("LEFT", hexLabel, "RIGHT", 3, 0)
F.hexEditBox:SetFontObject(GameFontHighlightSmall)
F.hexEditBox:SetAutoFocus(false)
F.hexEditBox:SetMaxLetters(6)
F.hexEditBox:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
F.hexEditBox:SetBackdropColor(0.05, 0.05, 0.05, 0.9)
F.hexEditBox:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.8)
F.hexEditBox:SetTextInsets(4, 4, 0, 0)
F.hexEditBox:SetScript("OnEnterPressed", function()
local sess = F.session
if not sess then return end
local text = F.hexEditBox:GetText()
local r, g, b = TCP.HexToRGB(text)
if r then
sess.h, sess.s, sess.v = TCP.RGBtoHSV(r, g, b)
UpdateColor()
else
local cr, cg, cb = TCP.HSVtoRGB(sess.h, sess.s, sess.v)
F.hexEditBox:SetText(TCP.RGBtoHex(cr, cg, cb))
end
F.hexEditBox:ClearFocus()
end)
F.hexEditBox:SetScript("OnEscapePressed", function()
local sess = F.session
if not sess then return end
local r, g, b = TCP.HSVtoRGB(sess.h, sess.s, sess.v)
F.hexEditBox:SetText(TCP.RGBtoHex(r, g, b))
F.hexEditBox:ClearFocus()
end)
F.hexEditBox:SetScript("OnEditFocusGained", function()
F.hexHasFocus = true
end)
F.hexEditBox:SetScript("OnEditFocusLost", function()
F.hexHasFocus = false
end)
F.hexEditBox:SetScript("OnTextChanged", function()
local sess = F.session
if not sess then return end
if not F.hexHasFocus then return end
local text = F.hexEditBox:GetText()
if string.len(text) == 6 then
local r, g, b = TCP.HexToRGB(text)
if r then
sess.h, sess.s, sess.v = TCP.RGBtoHSV(r, g, b)
UpdateColor()
end
end
end)
-- OK / Cancel
F.okBtn = CreateFrame("Button", nil, F.main, "UIPanelButtonTemplate")
F.okBtn:SetWidth(F.BTN_W)
F.okBtn:SetHeight(F.BTN_H)
F.okBtn:SetText("OK")
F.okBtn:SetScript("OnClick", function()
TCP:_NotifyOk()
end)
F.cancelBtn = CreateFrame("Button", nil, F.main, "UIPanelButtonTemplate")
F.cancelBtn:SetWidth(F.BTN_W)
F.cancelBtn:SetHeight(F.BTN_H)
F.cancelBtn:SetText("Cancel")
F.cancelBtn:SetScript("OnClick", function()
TCP:_NotifyCancel()
F.main:Hide()
end)
end
-- ============================================================
-- LAYOUT IMPLEMENTATION
-- ============================================================
RecalcLayout = function()
local s = F.session
if not s then return end
local cw = F.CONTENT_W
local topOff = F._topOff or (F.PAD + 8)
local yOff = -topOff - F.SV_SIZE
-- Preview swatches
yOff = yOff - F.SPACING
F.previewBorder:ClearAllPoints()
F.previewBorder:SetPoint("TOPLEFT", F.main, "TOPLEFT", F.PAD, yOff)
F.previewBorder:SetWidth(cw)
F.previewBorder:Show()
local swatchW = math.floor((cw - 1) / 2)
F.oldSwatch:SetWidth(swatchW)
F.newSwatch:SetWidth(swatchW)
yOff = yOff - F.SWATCH_H
-- Bottom row: hex input (left) + buttons (right) on same line
yOff = yOff - F.SPACING
local rowH = F.BTN_H
F.okBtn:ClearAllPoints()
F.cancelBtn:ClearAllPoints()
local btnGap = 4
-- Anchor buttons to right edge of content area (flush with swatches)
local rightEdge = F.PAD + cw + 2
F.cancelBtn:SetPoint("TOPRIGHT", F.main, "TOPLEFT", rightEdge, yOff)
F.okBtn:SetPoint("TOPRIGHT", F.cancelBtn, "TOPLEFT", -btnGap, 0)
if s.hasHexInput then
F.hexContainer:ClearAllPoints()
F.hexContainer:SetPoint("TOPLEFT", F.main, "TOPLEFT", F.PAD, yOff)
F.hexContainer:SetPoint("TOPRIGHT", F.okBtn, "TOPLEFT", -btnGap, 0)
F.hexContainer:Show()
else
F.hexContainer:Hide()
end
yOff = yOff - rowH
F.main:SetHeight(math.abs(yOff) + F.PAD)
end
-- ============================================================
-- PUBLIC
-- ============================================================
function TurtleColorPicker_UI:Show(sess)
Init()
F.session = sess
F.oldSwatch:SetTexture(sess.origR, sess.origG, sess.origB, 1)
if sess.hasHexInput then
local r, g, b = TCP.HSVtoRGB(sess.h, sess.s, sess.v)
F.hexEditBox:SetText(TCP.RGBtoHex(r, g, b))
F.hexEditBox:ClearFocus()
end
RecalcLayout()
UpdateColor()
-- Position near anchor frame if provided
if sess.anchorFrame then
local anchor = sess.anchorFrame
local scale = F.main:GetEffectiveScale()
local aScale = anchor:GetEffectiveScale()
-- Get anchor position in screen coords
local aLeft = anchor:GetLeft() * aScale
local aRight = anchor:GetRight() * aScale
local aTop = anchor:GetTop() * aScale
-- Convert to our scale
local screenW = GetScreenWidth() * UIParent:GetEffectiveScale()
local fw = F.main:GetWidth() * scale
local fh = F.main:GetHeight() * scale
-- Try to place to the right of the anchor with a small gap
local gap = 8
local targetX = aRight + gap
local targetY = aTop
-- If it would go off the right edge, place to the left instead
if (targetX + fw) > screenW then
targetX = aLeft - fw - gap
end
-- Clamp Y so it stays on screen
local screenH = GetScreenHeight() * UIParent:GetEffectiveScale()
if targetY > screenH then targetY = screenH end
if (targetY - fh) < 0 then targetY = fh end
-- Convert back to our frame's scale and set position from BOTTOMLEFT of UIParent
F.main:ClearAllPoints()
F.main:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", targetX / scale, targetY / scale)
end
F.main:Show()
end
function TurtleColorPicker_UI:Hide()
if F.main then
F.main:Hide()
end
end