1040 lines
34 KiB
Lua
1040 lines
34 KiB
Lua
-- Freewood-OctoWoW-Core: UI-Bausteine im Stil "Azeroth Codex"
|
|
-- Alles aus Farbflaechen, 1-Pixel-Raendern und Spielschriften. Keine eigenen Bilddateien.
|
|
-- Farben stammen aus den DESIGN.md-Dateien der Projektideen.
|
|
|
|
local ADDON, FW = ...
|
|
|
|
local UI = {}
|
|
FW.UI = UI
|
|
|
|
------------------------------------------------------------------------
|
|
-- Farben
|
|
------------------------------------------------------------------------
|
|
|
|
UI.hex = {
|
|
bgDeep = "0c0e14",
|
|
bg = "0f131f",
|
|
panel = "141824",
|
|
card = "1a2030",
|
|
hover = "242b40",
|
|
gold = "c89b3c",
|
|
goldLight = "f0bf5c",
|
|
goldAntique = "dfb75c",
|
|
bronze = "9d7826",
|
|
text = "e6e2d6",
|
|
textDim = "9a978c",
|
|
danger = "e0645a",
|
|
success = "6cc070",
|
|
}
|
|
|
|
UI.rarity = {
|
|
[0] = "9d9d9d", -- Schlecht
|
|
[1] = "ffffff", -- Gewoehnlich
|
|
[2] = "1eff00", -- Ungewoehnlich
|
|
[3] = "0070dd", -- Selten
|
|
[4] = "a335ee", -- Episch
|
|
[5] = "ff8000", -- Legendaer
|
|
[6] = "e6cc80", -- Artefakt
|
|
[7] = "00ccff", -- Erbstueck
|
|
}
|
|
|
|
local colorCache = {}
|
|
|
|
local function HexToRGB(hex)
|
|
return tonumber(hex:sub(1, 2), 16) / 255,
|
|
tonumber(hex:sub(3, 4), 16) / 255,
|
|
tonumber(hex:sub(5, 6), 16) / 255
|
|
end
|
|
|
|
-- key: Name aus UI.hex oder ein Hex-Wert wie "ff8000". Rueckgabe: r, g, b, a
|
|
function UI.Color(key, alpha)
|
|
local c = colorCache[key]
|
|
if not c then
|
|
c = { HexToRGB(UI.hex[key] or key) }
|
|
colorCache[key] = c
|
|
end
|
|
return c[1], c[2], c[3], alpha or 1
|
|
end
|
|
|
|
function UI.Hex(key)
|
|
return UI.hex[key] or key
|
|
end
|
|
|
|
function UI.RarityHex(quality)
|
|
return UI.rarity[quality or 1] or UI.rarity[1]
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Schriften
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.GetFontPath(kind)
|
|
if kind == "heading" then
|
|
return "Fonts\\FRIZQT__.TTF"
|
|
end
|
|
return STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
|
|
end
|
|
|
|
function UI.SetFont(fontString, kind, size, flags)
|
|
local ok = fontString:SetFont(UI.GetFontPath(kind), size or 12, flags or "")
|
|
if not ok then
|
|
fontString:SetFont(STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF", size or 12, flags or "")
|
|
end
|
|
end
|
|
|
|
-- opts: font ("body"/"heading"), size, color, justify, justifyV, flags, layer
|
|
function UI.CreateLabel(parent, text, opts)
|
|
opts = opts or {}
|
|
local fs = parent:CreateFontString(nil, opts.layer or "OVERLAY")
|
|
UI.SetFont(fs, opts.font or "body", opts.size or 12, opts.flags)
|
|
fs:SetTextColor(UI.Color(opts.color or "text"))
|
|
fs:SetJustifyH(opts.justify or "LEFT")
|
|
fs:SetJustifyV(opts.justifyV or "MIDDLE")
|
|
fs:SetText(text or "")
|
|
return fs
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Flaechen und Raender
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.AddBorder(frame, colorKey, alpha, thickness)
|
|
thickness = thickness or 1
|
|
local b = {}
|
|
|
|
b.top = frame:CreateTexture(nil, "BORDER")
|
|
b.top:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
|
b.top:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
|
|
b.top:SetHeight(thickness)
|
|
|
|
b.bottom = frame:CreateTexture(nil, "BORDER")
|
|
b.bottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
|
|
b.bottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
|
|
b.bottom:SetHeight(thickness)
|
|
|
|
b.left = frame:CreateTexture(nil, "BORDER")
|
|
b.left:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -thickness)
|
|
b.left:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, thickness)
|
|
b.left:SetWidth(thickness)
|
|
|
|
b.right = frame:CreateTexture(nil, "BORDER")
|
|
b.right:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -thickness)
|
|
b.right:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, thickness)
|
|
b.right:SetWidth(thickness)
|
|
|
|
frame.fwBorder = b
|
|
UI.SetBorderColor(frame, colorKey, alpha)
|
|
return b
|
|
end
|
|
|
|
function UI.SetBorderColor(frame, colorKey, alpha)
|
|
local b = frame.fwBorder
|
|
if not b then
|
|
return
|
|
end
|
|
local r, g, bl, a = UI.Color(colorKey, alpha)
|
|
b.top:SetColorTexture(r, g, bl, a)
|
|
b.bottom:SetColorTexture(r, g, bl, a)
|
|
b.left:SetColorTexture(r, g, bl, a)
|
|
b.right:SetColorTexture(r, g, bl, a)
|
|
end
|
|
|
|
function UI.CreatePanel(parent, bgKey, borderKey)
|
|
local p = CreateFrame("Frame", nil, parent)
|
|
p.bg = p:CreateTexture(nil, "BACKGROUND")
|
|
p.bg:SetAllPoints()
|
|
p.bg:SetColorTexture(UI.Color(bgKey or "panel"))
|
|
if borderKey then
|
|
UI.AddBorder(p, borderKey)
|
|
end
|
|
return p
|
|
end
|
|
|
|
function UI.CreateDivider(parent, colorKey, alpha)
|
|
local t = parent:CreateTexture(nil, "ARTWORK")
|
|
t:SetHeight(1)
|
|
t:SetColorTexture(UI.Color(colorKey or "bronze", alpha or 0.6))
|
|
return t
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Tooltips
|
|
------------------------------------------------------------------------
|
|
|
|
-- title: Text oder Funktion; lines: Text, Liste von Texten oder Funktion
|
|
function UI.AttachTooltip(frame, title, lines, anchor)
|
|
frame:HookScript("OnEnter", function(self)
|
|
GameTooltip:SetOwner(self, anchor or "ANCHOR_RIGHT")
|
|
local t = type(title) == "function" and title() or title
|
|
GameTooltip:SetText(t or "", 1, 1, 1)
|
|
local l = type(lines) == "function" and lines() or lines
|
|
if type(l) == "string" then
|
|
l = { l }
|
|
end
|
|
for _, line in ipairs(l or {}) do
|
|
GameTooltip:AddLine(line, 0.8, 0.8, 0.8, true)
|
|
end
|
|
GameTooltip:Show()
|
|
end)
|
|
frame:HookScript("OnLeave", function()
|
|
GameTooltip:Hide()
|
|
end)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Knoepfe
|
|
------------------------------------------------------------------------
|
|
|
|
local BUTTON_STYLES = {
|
|
default = { bg = "card", bgHover = "hover", border = "bronze", borderHover = "gold", text = "text" },
|
|
primary = { bg = "gold", bgHover = "goldLight", border = "goldLight", borderHover = "goldAntique", text = "bgDeep" },
|
|
ghost = { bg = "bg", bgHover = "hover", border = "bg", borderHover = "bronze", text = "text" },
|
|
-- Zerstoerende Aktionen: normal unauffaellig, beim Darueberfahren rot
|
|
danger = { bg = "card", bgHover = "hover", border = "bronze", borderHover = "danger", text = "text" },
|
|
dangerActive = { bg = "card", bgHover = "hover", border = "danger", borderHover = "danger", text = "danger" },
|
|
}
|
|
|
|
local function RefreshButton(b)
|
|
local style = BUTTON_STYLES[b.fwStyleName] or BUTTON_STYLES.default
|
|
local enabled = b:IsEnabled() and true or false
|
|
local hovered = b.fwHover and enabled
|
|
b.bg:SetColorTexture(UI.Color(hovered and style.bgHover or style.bg))
|
|
UI.SetBorderColor(b, hovered and style.borderHover or style.border)
|
|
b.label:SetTextColor(UI.Color(style.text))
|
|
b:SetAlpha(enabled and 1 or 0.45)
|
|
end
|
|
|
|
-- styleName: "default", "primary" oder "ghost"
|
|
function UI.CreateButton(parent, text, width, height, onClick, styleName)
|
|
local b = CreateFrame("Button", nil, parent)
|
|
b:SetSize(width or 120, height or 28)
|
|
b.fwStyleName = styleName or "default"
|
|
|
|
b.bg = b:CreateTexture(nil, "BACKGROUND")
|
|
b.bg:SetAllPoints()
|
|
UI.AddBorder(b, (BUTTON_STYLES[b.fwStyleName] or BUTTON_STYLES.default).border)
|
|
|
|
b.label = UI.CreateLabel(b, text, { size = 12, justify = "CENTER" })
|
|
b.label:SetPoint("CENTER", 0, 0)
|
|
|
|
b:SetScript("OnEnter", function(self)
|
|
self = self or this
|
|
if not self then return end
|
|
self.fwHover = true
|
|
RefreshButton(self)
|
|
end)
|
|
b:SetScript("OnLeave", function(self)
|
|
self = self or this
|
|
if not self then return end
|
|
self.fwHover = false
|
|
RefreshButton(self)
|
|
end)
|
|
b:SetScript("OnClick", function(self, ...)
|
|
if onClick then
|
|
onClick(self, ...)
|
|
end
|
|
end)
|
|
|
|
function b:SetLabel(newText)
|
|
self.label:SetText(newText)
|
|
end
|
|
function b:SetStyle(name)
|
|
self.fwStyleName = name
|
|
RefreshButton(self)
|
|
end
|
|
function b:SetButtonEnabled(on)
|
|
if on then
|
|
self:Enable()
|
|
else
|
|
self:Disable()
|
|
end
|
|
RefreshButton(self)
|
|
end
|
|
|
|
RefreshButton(b)
|
|
return b
|
|
end
|
|
|
|
-- Tab-Knopf mit Goldlinie fuer den aktiven Zustand
|
|
function UI.CreateTabButton(parent, text)
|
|
local b = CreateFrame("Button", nil, parent)
|
|
b:SetHeight(34)
|
|
|
|
b.hl = b:CreateTexture(nil, "BACKGROUND")
|
|
b.hl:SetAllPoints()
|
|
b.hl:SetColorTexture(UI.Color("hover", 0.5))
|
|
b.hl:Hide()
|
|
|
|
b.under = b:CreateTexture(nil, "ARTWORK")
|
|
b.under:SetHeight(2)
|
|
b.under:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
|
|
b.under:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
|
|
b.under:SetColorTexture(UI.Color("gold"))
|
|
b.under:Hide()
|
|
|
|
b.label = UI.CreateLabel(b, text, { font = "heading", size = 13, color = "textDim", justify = "CENTER" })
|
|
b.label:SetPoint("CENTER", 0, 1)
|
|
b:SetWidth(math.max(90, b.label:GetStringWidth() + 36))
|
|
|
|
function b:SetActive(active)
|
|
self.fwActive = active
|
|
if active then
|
|
self.label:SetTextColor(UI.Color("goldLight"))
|
|
self.under:Show()
|
|
self.hl:Hide()
|
|
else
|
|
self.label:SetTextColor(UI.Color("textDim"))
|
|
self.under:Hide()
|
|
end
|
|
end
|
|
function b:SetLabel(newText)
|
|
self.label:SetText(newText)
|
|
self:SetWidth(math.max(90, self.label:GetStringWidth() + 36))
|
|
end
|
|
|
|
b:SetScript("OnEnter", function(self)
|
|
if not self.fwActive then
|
|
self.hl:Show()
|
|
self.label:SetTextColor(UI.Color("text"))
|
|
end
|
|
end)
|
|
b:SetScript("OnLeave", function(self)
|
|
self.hl:Hide()
|
|
if not self.fwActive then
|
|
self.label:SetTextColor(UI.Color("textDim"))
|
|
end
|
|
end)
|
|
return b
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Kontrollkaestchen
|
|
------------------------------------------------------------------------
|
|
|
|
-- Hoehe eines umbrochenen Textes (Zeilen mal Zeilenhoehe, falls die Schrift keine Hoehe meldet)
|
|
function UI.TextHeight(fs, minHeight)
|
|
local h = fs.GetStringHeight and fs:GetStringHeight()
|
|
if type(h) ~= "number" or h <= 0 then
|
|
h = 14
|
|
end
|
|
return math.max(h, minHeight or 0)
|
|
end
|
|
|
|
-- Text auf eine Breite setzen und umbrechen; gibt die Hoehe zurueck
|
|
function UI.FitLabel(fs, width, minHeight)
|
|
if width and width > 0 then
|
|
fs:SetWidth(width)
|
|
end
|
|
fs:SetWordWrap(true)
|
|
fs:SetJustifyV("TOP")
|
|
fs:SetHeight(0) -- feste Hoehe aufheben, sonst misst die Schrift nur die abgeschnittene Hoehe
|
|
local h = UI.TextHeight(fs, minHeight)
|
|
fs:SetHeight(h)
|
|
return h
|
|
end
|
|
|
|
-- Kontrollkaestchen. c:Fit(breite) setzt die Breite, bricht den Text um und passt die Hoehe an
|
|
-- (Rueckgabe: Hoehe). Haengt das Kaestchen links und rechts an einem Rahmen, passt es sich
|
|
-- beim Aendern der Fenstergroesse selbst an.
|
|
local CHECK_TEXT_X = 28
|
|
local CHECK_MIN_H = 22
|
|
|
|
function UI.CreateCheckbox(parent, labelText, getFn, setFn)
|
|
local c = CreateFrame("Button", nil, parent)
|
|
c:SetHeight(24)
|
|
|
|
local box = UI.CreatePanel(c, "card", "bronze")
|
|
box:SetSize(18, 18)
|
|
box:SetPoint("LEFT", c, "LEFT", 0, 0)
|
|
|
|
local check = box:CreateTexture(nil, "ARTWORK")
|
|
check:SetPoint("TOPLEFT", box, "TOPLEFT", 4, -4)
|
|
check:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -4, 4)
|
|
check:SetColorTexture(UI.Color("goldLight"))
|
|
|
|
c.label = UI.CreateLabel(c, labelText, { size = 12 })
|
|
c.label:SetPoint("LEFT", box, "RIGHT", 10, 0)
|
|
c:SetWidth(math.max(120, c.label:GetStringWidth() + 40))
|
|
c.fitWidth = nil
|
|
c.isFit = true
|
|
|
|
function c:Fit(width)
|
|
width = width or self:GetWidth()
|
|
if not width or width <= 0 then
|
|
return self:GetHeight()
|
|
end
|
|
if not self.fitWidth then
|
|
-- erstes Fit: Kaestchen und Text oben ausrichten (mehrzeiliger Text)
|
|
box:ClearAllPoints()
|
|
box:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -2)
|
|
self.label:ClearAllPoints()
|
|
self.label:SetPoint("TOPLEFT", self, "TOPLEFT", CHECK_TEXT_X, -4)
|
|
end
|
|
self.fitWidth = width
|
|
self:SetWidth(width)
|
|
self.label:SetWidth(math.max(40, width - CHECK_TEXT_X))
|
|
self.label:SetWordWrap(true)
|
|
self.label:SetJustifyV("TOP")
|
|
self.label:SetHeight(0)
|
|
local h = math.max(CHECK_MIN_H, UI.TextHeight(self.label, 14) + 8)
|
|
self.label:SetHeight(h - 4)
|
|
self:SetHeight(h)
|
|
return h
|
|
end
|
|
|
|
function c:SetLabel(text)
|
|
self.label:SetText(text or "")
|
|
if self.fitWidth then
|
|
self:Fit(self.fitWidth)
|
|
end
|
|
end
|
|
|
|
c:SetScript("OnSizeChanged", function(self)
|
|
-- Breite immer vom Rahmen selbst lesen (die Argumente des Ereignisses sind im Client nicht verlaesslich)
|
|
local w = self:GetWidth()
|
|
if self.fitWidth and type(w) == "number" and w > 0 and math.abs(w - self.fitWidth) > 0.5 then
|
|
self:Fit(w)
|
|
end
|
|
end)
|
|
|
|
function c:Refresh()
|
|
if getFn() then
|
|
check:Show()
|
|
else
|
|
check:Hide()
|
|
end
|
|
end
|
|
|
|
c:SetScript("OnClick", function(self)
|
|
setFn(not getFn())
|
|
self:Refresh()
|
|
end)
|
|
c:SetScript("OnEnter", function()
|
|
UI.SetBorderColor(box, "gold")
|
|
end)
|
|
c:SetScript("OnLeave", function()
|
|
UI.SetBorderColor(box, "bronze")
|
|
end)
|
|
|
|
c:Refresh()
|
|
return c
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Vertikaler Stapel: ordnet Bausteine untereinander an und passt sich der Breite an
|
|
-- local stack = UI.CreateStack(frame, { gap = 6, x = 0, y = 0 })
|
|
-- stack:Add(widget, { gap = 10, indent = 0, stretch = true, fit = true, height = 30 })
|
|
-- fit Kontrollkaestchen/Text: wird auf die Breite umgebrochen, Hoehe folgt dem Text
|
|
-- stretch Baustein haengt auch am rechten Rand (Trennlinien, Textblock)
|
|
-- height feste Hoehe (sonst die aktuelle Hoehe des Bausteins)
|
|
-- stack:Layout() ordnet neu; stack.height ist die Gesamthoehe. Aenderungen der Breite des
|
|
-- Rahmens ordnen automatisch neu an (stack.onHeight wird danach gerufen).
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateStack(parent, opts)
|
|
opts = opts or {}
|
|
local stack = { items = {}, gap = opts.gap or 6, x = opts.x or 0, y = opts.y or 0, pad = opts.pad or 0, height = 0 }
|
|
|
|
function stack:Add(widget, o)
|
|
o = o or {}
|
|
self.items[#self.items + 1] = { w = widget, gap = o.gap, indent = o.indent or 0, stretch = o.stretch,
|
|
fit = o.fit, height = o.height, minHeight = o.minHeight }
|
|
return widget
|
|
end
|
|
|
|
function stack:Width()
|
|
local w = parent.GetWidth and parent:GetWidth() or 0
|
|
return math.max(0, (w or 0) - self.x - self.pad)
|
|
end
|
|
|
|
function stack:Layout()
|
|
local width = self:Width()
|
|
local y = self.y
|
|
for _, it in ipairs(self.items) do
|
|
local w = it.w
|
|
if not w.stackHidden then
|
|
local avail = width - it.indent
|
|
local h
|
|
if it.fit then
|
|
if w.isFit then
|
|
h = w:Fit(avail)
|
|
else
|
|
h = UI.FitLabel(w, avail, it.minHeight)
|
|
end
|
|
elseif it.height then
|
|
h = it.height
|
|
w:SetHeight(h)
|
|
else
|
|
h = w:GetHeight() or 0
|
|
end
|
|
y = y + (it.gap or self.gap)
|
|
w:ClearAllPoints()
|
|
w:SetPoint("TOPLEFT", parent, "TOPLEFT", self.x + it.indent, -y)
|
|
if it.stretch then
|
|
w:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -self.pad, -y)
|
|
end
|
|
y = y + h
|
|
end
|
|
end
|
|
self.height = y + self.pad
|
|
if self.onHeight then
|
|
self.onHeight(self.height)
|
|
end
|
|
return self.height
|
|
end
|
|
|
|
function stack:Hook()
|
|
local old = parent.GetScript and parent:GetScript("OnSizeChanged")
|
|
parent:SetScript("OnSizeChanged", function(f, w, h)
|
|
if old then old(f, w, h) end
|
|
w = f:GetWidth()
|
|
if type(w) == "number" and w > 0 and math.abs(w - (stack.lastW or 0)) > 0.5 then
|
|
stack.lastW = w
|
|
stack:Layout()
|
|
end
|
|
end)
|
|
end
|
|
stack:Hook()
|
|
return stack
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Schieberegler (waagerecht). Der Wert wird beim Loslassen uebernommen (setFn), waehrend des
|
|
-- Ziehens zeigt nur die Anzeige den Wert; so ruckelt nichts, wenn der Regler selbst die Groesse aendert.
|
|
-- local s = UI.CreateSlider(parent, 150, { min = 50, max = 150, step = 5, get = fn, set = fn, format = fn })
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateSlider(parent, width, opts)
|
|
local s = CreateFrame("Button", nil, parent)
|
|
s:SetSize(width or 150, 20)
|
|
local lo, hi, step = opts.min or 0, opts.max or 100, opts.step or 1
|
|
|
|
local track = s:CreateTexture(nil, "BACKGROUND")
|
|
track:SetColorTexture(UI.Color("card"))
|
|
track:SetHeight(4)
|
|
track:SetPoint("LEFT", s, "LEFT", 0, 0)
|
|
track:SetPoint("RIGHT", s, "RIGHT", 0, 0)
|
|
local fill = s:CreateTexture(nil, "ARTWORK")
|
|
fill:SetColorTexture(UI.Color("bronze"))
|
|
fill:SetHeight(4)
|
|
fill:SetPoint("LEFT", s, "LEFT", 0, 0)
|
|
local thumb = s:CreateTexture(nil, "OVERLAY")
|
|
thumb:SetSize(8, 16)
|
|
thumb:SetColorTexture(UI.Color("goldLight"))
|
|
|
|
local value = lo
|
|
local function Snap(v)
|
|
v = math.floor((v - lo) / step + 0.5) * step + lo
|
|
return math.max(lo, math.min(hi, v))
|
|
end
|
|
local function Show(v)
|
|
value = Snap(v)
|
|
local frac = (hi > lo) and ((value - lo) / (hi - lo)) or 0
|
|
local travel = math.max(1, (s:GetWidth() or width or 150) - 8)
|
|
thumb:ClearAllPoints()
|
|
thumb:SetPoint("LEFT", s, "LEFT", frac * travel, 0)
|
|
fill:SetWidth(math.max(1, frac * travel + 4))
|
|
if s.onText then s.onText(value) end
|
|
end
|
|
|
|
function s:SetValue(v) Show(v) end
|
|
function s:GetValue() return value end
|
|
function s:Refresh() Show(opts.get and opts.get() or lo) end
|
|
|
|
local function FromCursor()
|
|
local x = GetCursorPosition()
|
|
local scale = s:GetEffectiveScale() or 1
|
|
local left = s:GetLeft()
|
|
if not x or not left then return value end
|
|
local travel = math.max(1, (s:GetWidth() or 150) - 8)
|
|
local frac = ((x / scale) - left - 4) / travel
|
|
return lo + math.max(0, math.min(1, frac)) * (hi - lo)
|
|
end
|
|
|
|
s:SetScript("OnMouseDown", function(self)
|
|
Show(FromCursor())
|
|
self:SetScript("OnUpdate", function() Show(FromCursor()) end)
|
|
end)
|
|
s:SetScript("OnMouseUp", function(self)
|
|
self:SetScript("OnUpdate", nil)
|
|
Show(FromCursor())
|
|
if opts.set then opts.set(value) end
|
|
end)
|
|
s:SetScript("OnSizeChanged", function() Show(value) end)
|
|
s:SetScript("OnEnter", function() thumb:SetColorTexture(UI.Color("gold")) end)
|
|
s:SetScript("OnLeave", function() thumb:SetColorTexture(UI.Color("goldLight")) end)
|
|
return s
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Scrollbereich mit schmaler eigener Leiste
|
|
-- Ergebnis: holder (Frame). holder.child ist der Inhalt, holder:SetContentHeight(h)
|
|
-- passt die Hoehe an. Groesse ueber SetSize oder Anker festlegen.
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateScrollFrame(parent, width, height)
|
|
local holder = CreateFrame("Frame", nil, parent)
|
|
if width and height then
|
|
holder:SetSize(width, height)
|
|
end
|
|
|
|
local sf = CreateFrame("ScrollFrame", nil, holder)
|
|
sf:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0)
|
|
sf:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -12, 0)
|
|
|
|
local child = CreateFrame("Frame", nil, sf)
|
|
child:SetSize(1, 1)
|
|
sf:SetScrollChild(child)
|
|
|
|
local track = holder:CreateTexture(nil, "BACKGROUND")
|
|
track:SetColorTexture(UI.Color("card", 0.7))
|
|
track:SetWidth(6)
|
|
track:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, 0)
|
|
track:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, 0)
|
|
|
|
local thumb = CreateFrame("Button", nil, holder)
|
|
thumb:SetWidth(6)
|
|
local thumbTex = thumb:CreateTexture(nil, "OVERLAY")
|
|
thumbTex:SetAllPoints()
|
|
thumbTex:SetColorTexture(UI.Color("bronze"))
|
|
thumb:Hide()
|
|
thumb:SetScript("OnEnter", function()
|
|
thumbTex:SetColorTexture(UI.Color("gold"))
|
|
end)
|
|
thumb:SetScript("OnLeave", function()
|
|
thumbTex:SetColorTexture(UI.Color("bronze"))
|
|
end)
|
|
|
|
local contentHeight = 1
|
|
|
|
local function MaxScroll()
|
|
return math.max(0, contentHeight - sf:GetHeight())
|
|
end
|
|
|
|
local function Update()
|
|
-- Breite des Inhalts nachziehen, falls OnSizeChanged sie nicht (rechtzeitig) gesetzt hat
|
|
local sw = sf:GetWidth()
|
|
if sw and sw > 1 and math.abs((child:GetWidth() or 0) - sw) > 0.5 then
|
|
child:SetWidth(sw)
|
|
end
|
|
local viewH = sf:GetHeight()
|
|
local range = MaxScroll()
|
|
if range <= 0 or viewH <= 0 then
|
|
thumb:Hide()
|
|
track:Hide()
|
|
sf:SetVerticalScroll(0)
|
|
return
|
|
end
|
|
track:Show()
|
|
local current = math.min(math.max(sf:GetVerticalScroll(), 0), range)
|
|
if current ~= sf:GetVerticalScroll() then
|
|
sf:SetVerticalScroll(current)
|
|
end
|
|
local thumbH = math.max(24, viewH * viewH / contentHeight)
|
|
thumb:SetHeight(thumbH)
|
|
thumb:ClearAllPoints()
|
|
thumb:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, -(current / range) * (viewH - thumbH))
|
|
thumb:Show()
|
|
end
|
|
|
|
sf:EnableMouseWheel(true)
|
|
sf:SetScript("OnMouseWheel", function(self, delta)
|
|
local target = self:GetVerticalScroll() - delta * 48
|
|
self:SetVerticalScroll(math.min(math.max(target, 0), MaxScroll()))
|
|
Update()
|
|
end)
|
|
sf:SetScript("OnSizeChanged", function(self, w)
|
|
w = tonumber(w) or self:GetWidth()
|
|
if w and w > 0 then
|
|
child:SetWidth(w)
|
|
end
|
|
Update()
|
|
end)
|
|
|
|
thumb:SetScript("OnMouseDown", function(self)
|
|
local _, startY = GetCursorPosition()
|
|
local startScroll = sf:GetVerticalScroll()
|
|
local scale = self:GetEffectiveScale()
|
|
self:SetScript("OnUpdate", function()
|
|
local _, y = GetCursorPosition()
|
|
local free = sf:GetHeight() - self:GetHeight()
|
|
if free <= 0 then
|
|
return
|
|
end
|
|
local target = startScroll + ((startY - y) / scale) * (MaxScroll() / free)
|
|
sf:SetVerticalScroll(math.min(math.max(target, 0), MaxScroll()))
|
|
Update()
|
|
end)
|
|
end)
|
|
thumb:SetScript("OnMouseUp", function(self)
|
|
self:SetScript("OnUpdate", nil)
|
|
end)
|
|
|
|
holder.scroll = sf
|
|
holder.child = child
|
|
holder.UpdateScroll = Update
|
|
|
|
function holder:SetContentHeight(h)
|
|
contentHeight = math.max(1, h)
|
|
child:SetHeight(contentHeight)
|
|
Update()
|
|
end
|
|
function holder:ScrollToTop()
|
|
sf:SetVerticalScroll(0)
|
|
Update()
|
|
end
|
|
|
|
return holder
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Mehrzeiliges Textfeld (zum Kopieren und Einfuegen von Codes)
|
|
-- Ergebnis: area (Frame) mit area.edit (EditBox)
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateTextArea(parent, width, height)
|
|
local area = UI.CreateScrollFrame(parent, width, height)
|
|
|
|
local back = area:CreateTexture(nil, "BACKGROUND", nil, -1)
|
|
back:SetPoint("TOPLEFT", area, "TOPLEFT", 0, 0)
|
|
back:SetPoint("BOTTOMRIGHT", area, "BOTTOMRIGHT", -12, 0)
|
|
back:SetColorTexture(UI.Color("bgDeep", 0.9))
|
|
|
|
local frame = CreateFrame("Frame", nil, area)
|
|
frame:SetPoint("TOPLEFT", area, "TOPLEFT", 0, 0)
|
|
frame:SetPoint("BOTTOMRIGHT", area, "BOTTOMRIGHT", -12, 0)
|
|
UI.AddBorder(frame, "bronze")
|
|
|
|
local edit = CreateFrame("EditBox", nil, area.child)
|
|
edit:SetMultiLine(true)
|
|
edit:SetAutoFocus(false)
|
|
edit:SetMaxLetters(420000)
|
|
edit:SetTextInsets(8, 8, 6, 6)
|
|
edit:SetPoint("TOPLEFT", area.child, "TOPLEFT", 0, 0)
|
|
edit:SetPoint("TOPRIGHT", area.child, "TOPRIGHT", 0, 0)
|
|
if not edit:SetFont(UI.GetFontPath("body"), 12, "") then
|
|
edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
|
|
end
|
|
edit:SetTextColor(UI.Color("text"))
|
|
|
|
local function Resize()
|
|
local h = math.max(area.scroll:GetHeight(), edit:GetHeight() or 0)
|
|
area:SetContentHeight(h)
|
|
end
|
|
|
|
edit:SetScript("OnTextChanged", function()
|
|
C_Timer.After(0, Resize)
|
|
end)
|
|
edit:SetScript("OnEscapePressed", function(self)
|
|
self:ClearFocus()
|
|
end)
|
|
|
|
-- Klick in den leeren Bereich setzt den Cursor ins Feld
|
|
area.scroll:EnableMouse(true)
|
|
area.scroll:SetScript("OnMouseDown", function()
|
|
edit:SetFocus()
|
|
end)
|
|
|
|
area.edit = edit
|
|
function area:SetText(text)
|
|
edit:SetText(text or "")
|
|
C_Timer.After(0, Resize)
|
|
end
|
|
function area:GetText()
|
|
return edit:GetText()
|
|
end
|
|
function area:SelectAll()
|
|
edit:SetFocus()
|
|
edit:HighlightText()
|
|
end
|
|
|
|
return area
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Item-Platz (Symbol mit Seltenheitsrahmen)
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateItemSlot(parent, size)
|
|
size = size or 44
|
|
local slot = UI.CreatePanel(parent, "bgDeep", "bronze")
|
|
slot:SetSize(size, size)
|
|
|
|
slot.icon = slot:CreateTexture(nil, "ARTWORK")
|
|
slot.icon:SetPoint("TOPLEFT", slot, "TOPLEFT", 2, -2)
|
|
slot.icon:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2)
|
|
slot.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
|
|
|
|
function slot:SetItem(itemID)
|
|
self.itemID = itemID
|
|
if not itemID then
|
|
self.icon:SetTexture(nil)
|
|
UI.SetBorderColor(self, "bronze")
|
|
return
|
|
end
|
|
FW.Util.RequestItem(itemID, function(info)
|
|
if self.itemID ~= itemID or not info then
|
|
return
|
|
end
|
|
self.icon:SetTexture(info.icon)
|
|
UI.SetBorderColor(self, UI.RarityHex(info.quality))
|
|
end)
|
|
end
|
|
|
|
return slot
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Kennzahl-Karte (kleine Beschriftung oben, Wert darunter)
|
|
-- card:SetValue(text, colorKey)
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateStatCard(parent, labelText, width, height)
|
|
local card = UI.CreatePanel(parent, "card", "bronze")
|
|
card:SetSize(width or 150, height or 54)
|
|
|
|
card.label = UI.CreateLabel(card, labelText, { size = 10, color = "textDim", flags = "" })
|
|
card.label:SetPoint("TOPLEFT", card, "TOPLEFT", 10, -8)
|
|
|
|
card.value = UI.CreateLabel(card, "-", { font = "heading", size = 15, color = "text" })
|
|
card.value:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 10, 8)
|
|
|
|
function card:SetValue(text, colorKey)
|
|
self.value:SetText(text or "-")
|
|
self.value:SetTextColor(UI.Color(colorKey or "text"))
|
|
end
|
|
function card:SetLabel(text)
|
|
self.label:SetText(text)
|
|
end
|
|
|
|
return card
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Marke (eckig, kein Pillenformat), z. B. fuer Raenge und Filter
|
|
-- chip:SetText(text, colorKey)
|
|
------------------------------------------------------------------------
|
|
|
|
function UI.CreateChip(parent, text, colorKey)
|
|
local chip = UI.CreatePanel(parent, "bgDeep", colorKey or "bronze")
|
|
chip:SetHeight(20)
|
|
|
|
chip.label = UI.CreateLabel(chip, text, { size = 11, color = colorKey or "goldLight", justify = "CENTER" })
|
|
chip.label:SetPoint("CENTER", chip, "CENTER", 0, 0)
|
|
chip:SetWidth(math.max(28, chip.label:GetStringWidth() + 16))
|
|
|
|
function chip:SetText(newText, newColorKey)
|
|
self.label:SetText(newText)
|
|
if newColorKey then
|
|
self.label:SetTextColor(UI.Color(newColorKey))
|
|
UI.SetBorderColor(self, newColorKey)
|
|
end
|
|
self:SetWidth(math.max(28, self.label:GetStringWidth() + 16))
|
|
end
|
|
|
|
return chip
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Dropdown-Auswahl
|
|
-- opts.prefix Text vor dem Wert, zum Beispiel "Typ" ergibt "Typ: Waffe"
|
|
-- opts.allText Text fuer "keine Auswahl" (Wert nil); fehlt er, gibt es diesen Eintrag nicht
|
|
-- opts.getOptions Funktion, die eine Liste { { value = ..., text = ... }, ... } liefert (beim Oeffnen gelesen)
|
|
-- opts.textOf Funktion(value) -> Text, spart das Lesen der ganzen Liste fuer die Beschriftung (optional)
|
|
-- opts.onChange Funktion(value) nach einer Auswahl durch den Benutzer
|
|
-- opts.maxRows sichtbare Zeilen, bevor gescrollt wird (Standard 12)
|
|
-- Rueckgabe: Button mit :SetValue(v), :GetValue(), :Refresh(), :Open(), :Close()
|
|
------------------------------------------------------------------------
|
|
|
|
local DD_ROW = 20
|
|
local ddPopup, ddCatcher, ddOwner
|
|
|
|
local function CloseDropdown()
|
|
if ddPopup then ddPopup:Hide() end
|
|
if ddCatcher then ddCatcher:Hide() end
|
|
local owner = ddOwner
|
|
ddOwner = nil
|
|
if owner and owner.Refresh then owner:Refresh() end
|
|
end
|
|
UI.CloseDropdown = CloseDropdown
|
|
|
|
local function EnsureDropdownPopup()
|
|
if ddPopup then
|
|
return
|
|
end
|
|
-- unsichtbare Flaeche hinter der Liste: ein Klick daneben schliesst sie
|
|
ddCatcher = CreateFrame("Button", nil, UIParent)
|
|
ddCatcher:SetAllPoints(UIParent)
|
|
ddCatcher:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
ddCatcher:RegisterForClicks("AnyUp")
|
|
ddCatcher:SetScript("OnClick", CloseDropdown)
|
|
ddCatcher:Hide()
|
|
|
|
ddPopup = UI.CreatePanel(UIParent, "bgDeep", "gold")
|
|
ddPopup:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
ddPopup:SetFrameLevel((ddCatcher:GetFrameLevel() or 1) + 5)
|
|
ddPopup:EnableMouse(true)
|
|
ddPopup.list = UI.CreateScrollFrame(ddPopup)
|
|
ddPopup.list:SetPoint("TOPLEFT", ddPopup, "TOPLEFT", 2, -2)
|
|
ddPopup.list:SetPoint("BOTTOMRIGHT", ddPopup, "BOTTOMRIGHT", -2, 2)
|
|
ddPopup.rows = {}
|
|
ddPopup:Hide()
|
|
end
|
|
|
|
local function DropdownRow(index)
|
|
local row = ddPopup.rows[index]
|
|
if row then
|
|
return row
|
|
end
|
|
local child = ddPopup.list.child
|
|
row = CreateFrame("Button", nil, child)
|
|
row:SetHeight(DD_ROW)
|
|
row:SetPoint("TOPLEFT", child, "TOPLEFT", 0, -(index - 1) * DD_ROW)
|
|
row:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, -(index - 1) * DD_ROW)
|
|
row.hl = row:CreateTexture(nil, "BACKGROUND")
|
|
row.hl:SetAllPoints()
|
|
row.hl:SetColorTexture(UI.Color("hover", 0.9))
|
|
row.hl:Hide()
|
|
row.label = UI.CreateLabel(row, "", { size = 12 })
|
|
row.label:SetPoint("LEFT", row, "LEFT", 8, 0)
|
|
row.label:SetPoint("RIGHT", row, "RIGHT", -6, 0)
|
|
row.label:SetWordWrap(false)
|
|
row:SetScript("OnEnter", function(self) self.hl:Show() end)
|
|
row:SetScript("OnLeave", function(self) self.hl:Hide() end)
|
|
row:SetScript("OnClick", function(self)
|
|
local owner = ddOwner
|
|
local value = self.value
|
|
CloseDropdown()
|
|
if owner then
|
|
owner:SetValue(value)
|
|
if owner.fwOnChange then owner.fwOnChange(value) end
|
|
end
|
|
end)
|
|
ddPopup.rows[index] = row
|
|
return row
|
|
end
|
|
|
|
function UI.CreateDropdown(parent, width, height, opts)
|
|
opts = opts or {}
|
|
local dd = UI.CreateButton(parent, "", width or 150, height or 20, nil, "default")
|
|
dd.fwOnChange = opts.onChange
|
|
dd.fwValue = nil
|
|
|
|
dd.label:ClearAllPoints()
|
|
dd.label:SetPoint("LEFT", dd, "LEFT", 8, 0)
|
|
dd.label:SetPoint("RIGHT", dd, "RIGHT", -20, 0)
|
|
dd.label:SetJustifyH("LEFT")
|
|
dd.label:SetWordWrap(false)
|
|
dd.arrow = dd:CreateTexture(nil, "OVERLAY")
|
|
dd.arrow:SetTexture("Interface\\Minimap\\ROTATING-MINIMAPARROW")
|
|
dd.arrow:SetTexCoord(0, 1, 1, 0) -- Pfeil nach unten
|
|
dd.arrow:SetVertexColor(UI.Color("goldLight"))
|
|
dd.arrow:SetSize(12, 12)
|
|
dd.arrow:SetPoint("RIGHT", dd, "RIGHT", -5, -1)
|
|
|
|
local function Options()
|
|
local list = {}
|
|
if opts.allText then
|
|
list[1] = { value = nil, text = opts.allText }
|
|
end
|
|
local given = opts.getOptions and opts.getOptions() or {}
|
|
for _, o in ipairs(given) do
|
|
list[#list + 1] = o
|
|
end
|
|
return list
|
|
end
|
|
|
|
local function TextOf(value)
|
|
if value == nil then
|
|
return opts.allText or ""
|
|
end
|
|
if opts.textOf then
|
|
return opts.textOf(value)
|
|
end
|
|
for _, o in ipairs(Options()) do
|
|
if o.value == value then
|
|
return o.text
|
|
end
|
|
end
|
|
return tostring(value)
|
|
end
|
|
|
|
-- Beschriftung und Hervorhebung nach dem Wert richten
|
|
function dd:Refresh()
|
|
local text = TextOf(self.fwValue)
|
|
if opts.prefix then
|
|
text = opts.prefix .. ": " .. text
|
|
end
|
|
self:SetLabel(text)
|
|
if self.fwValue ~= nil or ddOwner == self then
|
|
self:SetStyle("primary")
|
|
else
|
|
self:SetStyle("default")
|
|
end
|
|
end
|
|
|
|
function dd:SetValue(value)
|
|
self.fwValue = value
|
|
self:Refresh()
|
|
end
|
|
|
|
function dd:GetValue()
|
|
return self.fwValue
|
|
end
|
|
|
|
function dd:Close()
|
|
if ddOwner == self then CloseDropdown() end
|
|
end
|
|
|
|
function dd:Open()
|
|
EnsureDropdownPopup()
|
|
local list = Options()
|
|
if #list == 0 then
|
|
return
|
|
end
|
|
ddOwner = self
|
|
local rows = math.min(#list, opts.maxRows or 12)
|
|
local w = math.max(self:GetWidth() or 0, 150)
|
|
ddPopup:SetSize(w, rows * DD_ROW + 4)
|
|
ddPopup:ClearAllPoints()
|
|
local bottom = self:GetBottom()
|
|
if bottom and bottom - (rows * DD_ROW + 8) < 0 then
|
|
ddPopup:SetPoint("BOTTOMLEFT", self, "TOPLEFT", 0, 2) -- unten kein Platz: darueber oeffnen
|
|
else
|
|
ddPopup:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
|
|
end
|
|
for i, o in ipairs(list) do
|
|
local row = DropdownRow(i)
|
|
row.value = o.value
|
|
row.label:SetText(o.text)
|
|
if o.value == self.fwValue then
|
|
row.label:SetTextColor(UI.Color("goldLight"))
|
|
else
|
|
row.label:SetTextColor(UI.Color("text"))
|
|
end
|
|
row:Show()
|
|
end
|
|
for i = #list + 1, #ddPopup.rows do
|
|
ddPopup.rows[i]:Hide()
|
|
end
|
|
ddPopup.list:SetContentHeight(#list * DD_ROW)
|
|
ddPopup.list:ScrollToTop()
|
|
ddCatcher:Show()
|
|
ddPopup:Show()
|
|
self:Refresh()
|
|
end
|
|
|
|
dd:SetScript("OnClick", function(self)
|
|
if ddOwner == self then
|
|
CloseDropdown()
|
|
else
|
|
if ddOwner then CloseDropdown() end
|
|
self:Open()
|
|
end
|
|
end)
|
|
dd:SetScript("OnHide", function(self) self:Close() end)
|
|
|
|
dd:Refresh()
|
|
return dd
|
|
end
|