TurtleColorPicker v1.0.0

This commit is contained in:
2026-09-12 17:59:32 +02:00
commit 1d59653015
7 changed files with 924 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# Changelog
All notable changes to this project are documented in this file.
## [1.0.0] - 2026-02-17
Initial public release: a standalone HSV color picker library for TurtleWoW
(WoW 1.12) addon developers.
### Added
- HSV saturation/value square with vertical hue bar.
- Optional hex color input (`#RRGGBB`).
- Old/new color preview swatches.
- Compact, draggable popup window (closable with ESC).
- Lazy initialization — no overhead until the picker is first opened.
- Public API with `onChange`, `onOk`, and `onCancel` callbacks.
- `opts.anchorFrame` option to position the picker next to a caller-supplied
frame (e.g. a color swatch button), with smart left/right and vertical
clamping so the popup stays fully on-screen.
- Frame strata raised to `FULLSCREEN_DIALOG` so the picker renders above
other addons' options panels.
- README with installation instructions, API reference, and integration
examples, including an in-game screenshot.
### Fixed
- Removed the alpha slider (not needed for pure color picking, simplified the UI).
- Worked around the Lua 5.0 32-upvalue limit by packing shared state into a
single table.
- Removed use of `HasFocus()` / `IsMouseButtonDown()`, which are not
available in the WoW 1.12 client API; switched the saturation/value and
hue controls to `Button` frames with `OnMouseDown`/`OnMouseUp` instead.
- Added a guard so the picker frame is only shown after its UI has been
created.
+207
View File
@@ -0,0 +1,207 @@
-- TurtleColorPicker - Standalone HSV Color Picker Library
-- Version 1.0.0 | WoW 1.12 / Lua 5.0
TurtleColorPicker = {}
TurtleColorPicker.version = "1.0.0"
TurtleColorPicker._session = nil
-- Pre-create UI namespace (UI.lua populates it)
TurtleColorPicker_UI = {}
-- ============================================================
-- MATH UTILITIES
-- ============================================================
local function Clamp(val, minV, maxV)
if val < minV then return minV end
if val > maxV then return maxV end
return val
end
-- HSV to RGB (all values 0-1, h wraps at 1)
local function HSVtoRGB(h, s, v)
if s == 0 then return v, v, v end
if h >= 1 then h = 0 end
local hSector = h * 6
local sextant = math.floor(hSector)
local f = hSector - sextant
local p = v * (1 - s)
local q = v * (1 - s * f)
local t = v * (1 - s * (1 - f))
if sextant == 0 then return v, t, p
elseif sextant == 1 then return q, v, p
elseif sextant == 2 then return p, v, t
elseif sextant == 3 then return p, q, v
elseif sextant == 4 then return t, p, v
else return v, p, q end
end
-- RGB to HSV (all values 0-1)
local function RGBtoHSV(r, g, b)
local maxC = math.max(r, g, b)
local minC = math.min(r, g, b)
local delta = maxC - minC
local h, s, vl
vl = maxC
if maxC == 0 then
return 0, 0, 0
end
s = delta / maxC
if delta == 0 then
return 0, s, vl
end
if maxC == r then
h = (g - b) / delta
if h < 0 then h = h + 6 end
elseif maxC == g then
h = 2 + (b - r) / delta
else
h = 4 + (r - g) / delta
end
h = h / 6
return h, s, vl
end
-- RGB (0-1) to Hex string "RRGGBB"
local function RGBtoHex(r, g, b)
local ri = math.floor(r * 255 + 0.5)
local gi = math.floor(g * 255 + 0.5)
local bi = math.floor(b * 255 + 0.5)
return string.format("%02X%02X%02X", ri, gi, bi)
end
-- Hex string "RRGGBB" to RGB (0-1), returns nil on invalid
local function HexToRGB(hex)
hex = string.gsub(hex, "^#", "")
if string.len(hex) ~= 6 then return nil, nil, nil end
local r = tonumber(string.sub(hex, 1, 2), 16)
local g = tonumber(string.sub(hex, 3, 4), 16)
local b = tonumber(string.sub(hex, 5, 6), 16)
if not r or not g or not b then return nil, nil, nil end
return r / 255, g / 255, b / 255
end
-- Expose utilities for other addons
TurtleColorPicker.HSVtoRGB = HSVtoRGB
TurtleColorPicker.RGBtoHSV = RGBtoHSV
TurtleColorPicker.RGBtoHex = RGBtoHex
TurtleColorPicker.HexToRGB = HexToRGB
TurtleColorPicker.Clamp = Clamp
-- ============================================================
-- PUBLIC API
-- ============================================================
function TurtleColorPicker:Open(opts)
opts = opts or {}
local ir = 1
local ig = 1
local ib = 1
if opts.color then
ir = opts.color.r or 1
ig = opts.color.g or 1
ib = opts.color.b or 1
end
local h, s, v = RGBtoHSV(ir, ig, ib)
self._session = {
h = h,
s = s,
v = v,
origR = ir,
origG = ig,
origB = ib,
hasHexInput = opts.hasHexInput or false,
anchorFrame = opts.anchorFrame or nil,
onChange = opts.onChange,
onOk = opts.onOk,
onCancel = opts.onCancel,
_confirmed = false,
}
if not TurtleColorPicker_UI.Show then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[TCP] ERROR: UI.lua failed to load. Check for Lua errors at login.|r")
return
end
TurtleColorPicker_UI:Show(self._session)
end
function TurtleColorPicker:Close()
if TurtleColorPicker_UI.Hide then
TurtleColorPicker_UI:Hide()
end
end
function TurtleColorPicker:_NotifyChange()
local s = self._session
if not s then return end
if s.onChange then
local r, g, b = HSVtoRGB(s.h, s.s, s.v)
s.onChange(r, g, b)
end
end
function TurtleColorPicker:_NotifyOk()
local s = self._session
if not s then return end
s._confirmed = true
if s.onOk then
local r, g, b = HSVtoRGB(s.h, s.s, s.v)
s.onOk(r, g, b)
end
self:Close()
end
function TurtleColorPicker:_NotifyCancel()
local s = self._session
if not s then return end
if s._confirmed then return end
s._confirmed = true
if s.onCancel then
s.onCancel()
end
end
-- ============================================================
-- SLASH COMMANDS
-- ============================================================
SLASH_TURTLECOLORPICKER1 = "/tcp"
SLASH_TURTLECOLORPICKER2 = "/turtlecolor"
SlashCmdList["TURTLECOLORPICKER"] = function(msg)
if msg == "test" then
TurtleColorPicker:Open({
color = {r = 0.4, g = 0.6, b = 0.85},
hasHexInput = true,
onOk = function(r, g, b)
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[TCP]|r OK: #%s", RGBtoHex(r, g, b))
)
end,
onCancel = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[TCP]|r Cancelled.")
end,
})
elseif msg == "minimal" then
TurtleColorPicker:Open({
color = {r = 0, g = 0.5, b = 1},
onOk = function(r, g, b)
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[TCP]|r OK: #%s", RGBtoHex(r, g, b))
)
end,
})
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[TurtleColorPicker]|r v" .. TurtleColorPicker.version)
DEFAULT_CHAT_FRAME:AddMessage(" /tcp test -- Full test (with hex input)")
DEFAULT_CHAT_FRAME:AddMessage(" /tcp minimal -- Minimal test (color only)")
end
end
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ShempError
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+124
View File
@@ -0,0 +1,124 @@
# TurtleColorPicker
Standalone HSV color picker library for [TurtleWoW](https://turtle-wow.org/) (WoW 1.12) addon developers.
![TurtleColorPicker](screenshot.png)
## Features
- HSV saturation/value square + vertical hue bar
- Optional hex color input (`#RRGGBB`)
- Old/New color preview swatches
- Compact, draggable popup (ESC to close)
- Lazy initialization (no overhead until first use)
- Clean API with `onChange`, `onOk`, `onCancel` callbacks
## Installation
Download and extract into your `Interface/AddOns/` folder so the structure is:
```
Interface/AddOns/TurtleColorPicker/
TurtleColorPicker.toc
Core.lua
UI.lua
```
## Integration
Add the dependency to your addon's `.toc` file:
```
## RequiredDeps: TurtleColorPicker
```
Then call the API from your addon:
```lua
TurtleColorPicker:Open({
color = { r = 0.5, g = 0.8, b = 1.0 }, -- initial color (0-1)
hasHexInput = true, -- show hex input field
onChange = function(r, g, b) -- fires during drag
myFrame:SetBackdropColor(r, g, b, 1)
end,
onOk = function(r, g, b) -- fires on OK
SaveColor(r, g, b)
end,
onCancel = function() -- fires on Cancel/ESC
RevertColor()
end,
})
```
All options are optional. Default color is white `(1, 1, 1)`.
## API Reference
### TurtleColorPicker:Open(opts)
Opens the color picker popup.
| Option | Type | Description |
|--------|------|-------------|
| `color` | `{r, g, b}` | Initial color, values 0-1. Default: white |
| `hasHexInput` | `boolean` | Show hex input field. Default: `false` |
| `anchorFrame` | `frame` | Position picker next to this frame. Default: screen center |
| `onChange` | `function(r, g, b)` | Called on every color change during interaction |
| `onOk` | `function(r, g, b)` | Called when OK is clicked |
| `onCancel` | `function()` | Called when Cancel is clicked or ESC pressed |
### TurtleColorPicker:Close()
Programmatically closes the picker.
### Utility Functions
```lua
local r, g, b = TurtleColorPicker.HSVtoRGB(h, s, v)
local h, s, v = TurtleColorPicker.RGBtoHSV(r, g, b)
local hex = TurtleColorPicker.RGBtoHex(r, g, b) -- "FF8000"
local r, g, b = TurtleColorPicker.HexToRGB("FF8000") -- returns 0-1
local clamped = TurtleColorPicker.Clamp(val, min, max)
```
## Example: Color Swatch Button
```lua
local swatch = CreateFrame("Button", nil, MyOptionsFrame)
swatch:SetWidth(20)
swatch:SetHeight(20)
swatch:RegisterForClicks("LeftButtonUp")
local tex = swatch:CreateTexture(nil, "BACKGROUND")
tex:SetAllPoints(swatch)
tex:SetTexture(MySettings.r, MySettings.g, MySettings.b)
swatch:SetScript("OnClick", function()
TurtleColorPicker:Open({
color = MySettings,
hasHexInput = true,
anchorFrame = swatch, -- picker appears next to the swatch
onChange = function(r, g, b)
tex:SetTexture(r, g, b, 1)
end,
onOk = function(r, g, b)
MySettings.r = r
MySettings.g = g
MySettings.b = b
end,
onCancel = function()
tex:SetTexture(MySettings.r, MySettings.g, MySettings.b, 1)
end,
})
end)
```
## Test Commands
- `/tcp test` - Opens picker with hex input
- `/tcp minimal` - Opens picker without hex input
- `/tcp` - Shows version info
## License
MIT
+8
View File
@@ -0,0 +1,8 @@
## Interface: 11200
## Title: TurtleColorPicker
## Notes: Standalone HSV color picker library for addon developers.
## Author: ShempError
## Version: 1.0.0
Core.lua
UI.lua
+531
View File
@@ -0,0 +1,531 @@
-- 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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB