TurtleColorPicker v1.0.0
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,124 @@
|
||||
# TurtleColorPicker
|
||||
|
||||
Standalone HSV color picker library for [TurtleWoW](https://turtle-wow.org/) (WoW 1.12) addon developers.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Reference in New Issue
Block a user