-- DopingControl ui/widgets.lua -- Shared UI building blocks: panel backdrop, pooled-widget factories -- (cell / pill / badge / button), tooltip helpers, pfUI-aware font helper, -- change-guarded setters (only touch a region when the value changed). -- -- Factory + pooling patterns follow common 1.12 addon practice -- (pfUI-style WHITE8X8 backdrops, pooled regions, save-and-call hooks). -- -- Pure Lua 5.0 dofile-loadable: top level only defines tables/functions; -- CreateFrame & friends are only referenced INSIDE factory functions, which -- are never called offline. DopingControl = DopingControl or {} local DC = DopingControl DC_Widgets = DC_Widgets or {} local W = DC_Widgets -- ------------------------------------------------------------------ -- Texture / font constants -- ------------------------------------------------------------------ W.SOLID_TEX = "Interface\\Buttons\\WHITE8X8" -- 1px-border backdrop base W.FONT = "Fonts\\FRIZQT__.TTF" -- default client font W.CHECK_TEX = "Interface\\Buttons\\UI-CheckBox-Check" -- HAS glyph (tinted green) W.CROSS_TEX = "Interface\\Buttons\\UI-GroupLoot-Pass-Up" -- MISSING glyph (tinted red) -- Main window backdrop (textures shipped in DopingControl\textures\). W.PANEL_BACKDROP = { bgFile = "Interface\\AddOns\\DopingControl\\textures\\panel_bg", edgeFile = "Interface\\AddOns\\DopingControl\\textures\\panel_border", tile = true, tileSize = 128, edgeSize = 16, insets = { left = 5, right = 5, top = 5, bottom = 5 }, } -- Solid 1px-border backdrop for cells/pills/badges/buttons (pfUI-style -- WHITE8X8 recipe; bg + border colored via SetBackdropColor / -- SetBackdropBorderColor). W.SOLID_BACKDROP = { bgFile = W.SOLID_TEX, edgeFile = W.SOLID_TEX, tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, } W.WHITE = { r = 1, g = 1, b = 1 } -- ------------------------------------------------------------------ -- Cell-box fallback palette: variant name -> (bg, border) color reference, -- used ONLY when ui/cellsheet.lua's baked sheet is unavailable (see -- W.HasCellSheet below). `border` is a DC.COLORS key, or a number that -- indexes DC.QUALITY -- same convention as DC_Cells.VARIANTS in -- ui/cellsheet.lua, which this table intentionally mirrors: colors are -- pulled LIVE from DC.COLORS/DC.QUALITY (core/const.lua, already loaded -- -- ui/cellsheet.lua only bakes that same palette into a texture, it does -- not own it), so nothing here duplicates a color VALUE. The pairing -- itself (which key goes with which variant) can't be derived -- mechanically -- it is the one piece of information that has to exist -- twice given the failure mode this guards (ui/cellsheet.lua not loaded -- at all). tools/luatests/test_widgets.lua cross-checks every entry -- against the real DC_Cells.VARIANTS so the two cannot silently drift. W.CELL_FALLBACK_VARIANTS = { HAS = { bg = "hatBg", border = "hatBorder" }, MISSING = { bg = "fehltBg", border = "fehltBorder" }, UNKNOWN = { bg = "unbekBg", border = "unbekBorder" }, SKIP = { bg = "skip", border = "skipBorder" }, NEUTRAL = { bg = "theadBg", border = "line" }, ITEM_HAS = { bg = "theadBg", border = "hatBorder" }, ITEM_MISSING = { bg = "theadBg", border = "fehltBorder" }, Q_RARE = { bg = "theadBg", border = "rareBorder" }, Q_EPIC = { bg = "theadBg", border = "epicBorder" }, Q_0 = { bg = "theadBg", border = 0 }, Q_1 = { bg = "theadBg", border = 1 }, Q_2 = { bg = "theadBg", border = 2 }, Q_5 = { bg = "theadBg", border = 5 }, Q_6 = { bg = "theadBg", border = 6 }, } -- ------------------------------------------------------------------ -- DC_Cells fallback shim -- covers a mid-session /reload where THIS file -- (ui/widgets.lua, an EXISTING file whose new content /reload happily -- re-executes) deploys, but ui/cellsheet.lua (a NEW file added in the same -- change) does not: 1.12's /reload re-runs existing files but never loads -- a file that was not already part of the running session (verified -- in-game). Left alone, DC_Cells stays nil, and it is not only -- W.MakeCell/W.SetCellBox below that touch it -- ui/matrix.lua calls -- DC_Cells.VariantFor(...) directly at two paint sites (M.PaintItemTile -- and the legacy trinket path, ui/matrix.lua ~2182/2381 -- out of scope -- for this change) and would error on the very first painted item/trinket -- cell regardless of anything done here. -- -- The shim supplies ONLY the pure classification function -- state/kind/ -- quality -> variant NAME string -- no sheet, no texture, no coordinate -- math, because none of that can work without ui/cellsheet.lua's baked -- sheet file. It is a verbatim copy of DC_Cells.VariantFor's logic (see -- ui/cellsheet.lua); test_widgets.lua cross-checks the two stay in sync. -- Installs only when DC_Cells does not already exist, so the normal -- (full-restart) load path -- where ui/cellsheet.lua runs before this file -- per DopingControl.toc and sets the real DC_Cells -- is untouched. if not DC_Cells then DC_Cells = { VariantFor = function(state, kind, quality) if kind == "ench" or kind == "trinket" then if quality == 3 then return "Q_RARE" end if quality == 4 then return "Q_EPIC" end if quality ~= nil then return "Q_" .. quality end if kind == "trinket" then return "Q_1" end if state == "HAS" then return "ITEM_HAS" end if state == "MISSING" then return "ITEM_MISSING" end return "NEUTRAL" end if state == "HAS" then return "HAS" end if state == "MISSING" then return "MISSING" end if state == "UNKNOWN" then return "UNKNOWN" end return "SKIP" end, } end -- Whether ui/cellsheet.lua's baked sheet is actually available (as -- opposed to the shim above, which never sets .SHEET). One seam shared by -- W.MakeCell and W.SetCellBox, and directly testable offline without -- CreateFrame (unlike W.MakeCell itself). function W.HasCellSheet() return DC_Cells ~= nil and DC_Cells.SHEET ~= nil end -- ------------------------------------------------------------------ -- Font helper (we keep OUR pixel -- size -- the matrix layout depends on it -- and only take pfUI's font -- FAMILY when pfUI is present; forcing pfUI's global font size would break -- the 30x23 cell grid). -- ------------------------------------------------------------------ function W.ApplyFont(fs, size) size = size or 10 if pfUI and pfUI.font_default then fs:SetFont(pfUI.font_default, size) else fs:SetFont(W.FONT, size) end end -- ------------------------------------------------------------------ -- Change-guarded setters (convention: cache last-applied value on the -- region as dcLast*; only call the real setter when the value changed. -- Color guards compare by TABLE REFERENCE -- callers always pass the -- shared DC.COLORS / DC.QUALITY tables, so this is exact and zero-alloc). -- ------------------------------------------------------------------ function W.SetTextG(fs, text) if fs.dcLastText ~= text then fs.dcLastText = text fs:SetText(text) end end function W.SetTextColorG(fs, c) if fs.dcLastColor ~= c then fs.dcLastColor = c fs:SetTextColor(c.r, c.g, c.b) end end function W.SetBackdropG(f, bg, border) if f.dcLastBg ~= bg then f.dcLastBg = bg f:SetBackdropColor(bg.r, bg.g, bg.b, bg.a or 1) end if f.dcLastBorder ~= border then f.dcLastBorder = border f:SetBackdropBorderColor(border.r, border.g, border.b, 1) end end function W.SetTextureG(tex, path) if tex.dcLastTex ~= path then tex.dcLastTex = path tex:SetTexture(path) end end function W.SetVertexG(tex, c) if tex.dcLastVertex ~= c then tex.dcLastVertex = c tex:SetVertexColor(c.r, c.g, c.b) end end function W.SetAlphaG(region, a) if region.dcLastAlpha ~= a then region.dcLastAlpha = a region:SetAlpha(a) end end -- ------------------------------------------------------------------ -- FontString factory -- ------------------------------------------------------------------ function W.MakeText(parent, size, color, layer) local fs = parent:CreateFontString(nil, layer or "OVERLAY") W.ApplyFont(fs, size) if color then W.SetTextColorG(fs, color) end return fs end -- ------------------------------------------------------------------ -- Cell factory: 30x23 state cell (the 30x23 size is part of the fixed -- visual layout). The box (background + 1px border) is one texture region -- sampled from the baked sheet in ui/cellsheet.lua -- SetBackdrop with an -- edgeFile costs NINE texture regions per cell, this costs one. The icon -- (check/cross glyph OR trinket item icon) and FontString (?, skip dot, -- trinket monogram) are created on demand via W.CellIcon / W.CellText; -- the matrix paint code shows exactly one of them per state. -- Pool children stay anonymous (documented factory exception). -- ------------------------------------------------------------------ function W.MakeCell(parent, name, w, h) local c = CreateFrame("Frame", name, parent) c:SetWidth(w or 30) c:SetHeight(h or 23) c:EnableMouse(true) c.dcW = w or 30 if W.HasCellSheet() then -- The state box (background + 1px border) comes from one baked sheet: -- SetBackdrop with an edgeFile costs NINE texture regions per cell, -- this costs one. See ui/cellsheet.lua. c.boxTex = c:CreateTexture(nil, "BACKGROUND") c.boxTex:SetAllPoints(c) c.boxTex:SetTexture(DC_Cells.SHEET.path) -- Without an initial texcoord the region would show the WHOLE sheet -- stretched across the cell until the first paint. Start on NEUTRAL. local l, r, t, b = DC_Cells.Coords("NEUTRAL", c.dcW) if l then c.boxTex:SetTexCoord(l, r, t, b) c.dcBoxVariant = "NEUTRAL" c.dcBoxW = c.dcW end else -- Fallback: ui/cellsheet.lua did not load this session (see -- W.HasCellSheet above) -- pre-refactor SetBackdrop path (no -- boxTex region at all) so the matrix still draws instead of -- erroring on a nil DC_Cells.SHEET. W.SetBackdropG and -- W.SOLID_BACKDROP are untouched by the sprite refactor, so this -- is exactly what W.MakeCell did before it. c:SetBackdrop(W.SOLID_BACKDROP) W.SetCellBox(c, "NEUTRAL") end return c end -- Icon and FontString on demand: a cell shows one or the other, and in 1.12 -- eagerly created regions cost even while hidden. function W.CellIcon(cell) if not cell.icon then cell.icon = cell:CreateTexture(nil, "ARTWORK") cell.icon:SetPoint("CENTER", cell, "CENTER", 0, 0) cell.icon:SetWidth(16) cell.icon:SetHeight(16) cell.icon:Hide() end return cell.icon end function W.CellText(cell) if not cell.text then cell.text = cell:CreateFontString(nil, "OVERLAY") W.ApplyFont(cell.text, 9) cell.text:SetPoint("CENTER", cell, "CENTER", 0, 0) end return cell.text end -- Guards on variant AND width (cell.dcBoxW, the width the box was last -- painted at) -- NOT variant alone. ui/matrix.lua resizes a pooled cell in -- place when its column width changes (trinket columns are 34, normal -- cells 30: see ui/matrix.lua's column-layout path around SetWidth(col.w)) -- and only ever writes cell.dcW; it has no notion of dcBoxVariant and isn't -- expected to invalidate it. If this guard only checked variant, a cell -- that kept its variant across such a resize would keep a stale texcoord -- sized for the OLD width -- the baked border would sit off the new cell -- edge. Tracking dcBoxW here keeps the guard self-contained: correctness -- does not depend on any caller convention. function W.SetCellBox(cell, variant) if not W.HasCellSheet() then -- Fallback: same pre-refactor SetBackdrop recipe, colors pulled -- LIVE from DC.COLORS/DC.QUALITY via W.CELL_FALLBACK_VARIANTS -- above. Never touches cell.boxTex -- a fallback cell (see -- W.MakeCell) does not have one. W.SetBackdropG already -- change-guards by table reference, so no separate -- dcBoxVariant/dcBoxW bookkeeping is needed here. local v = W.CELL_FALLBACK_VARIANTS[variant] if not v then return end local bg = DC.COLORS[v.bg] local border = v.border if type(border) == "number" then border = DC.QUALITY[border] else border = DC.COLORS[border] end if not bg or not border then return end W.SetBackdropG(cell, bg, border) return end if cell.dcBoxVariant == variant and cell.dcBoxW == cell.dcW then return end local l, r, t, b = DC_Cells.Coords(variant, cell.dcW) if not l then return end cell.dcBoxVariant = variant cell.dcBoxW = cell.dcW cell.boxTex:SetTexCoord(l, r, t, b) end -- ------------------------------------------------------------------ -- Pill factory (ready pill, coverage pill): bordered mini frame with a -- centered FontString; width is adjusted by the caller to fit the text. -- ------------------------------------------------------------------ function W.MakePill(parent, name) local p = CreateFrame("Frame", name, parent) p:SetWidth(70) p:SetHeight(16) p:SetBackdrop(W.SOLID_BACKDROP) p:EnableMouse(true) p.text = p:CreateFontString(nil, "OVERLAY") W.ApplyFont(p.text, 9) p.text:SetPoint("CENTER", p, "CENTER", 0, 0) return p end -- ------------------------------------------------------------------ -- Role badge factory: clickable bordered button with centered label. -- Suggested roles render with a "?" suffix + dimmer border (matrix paints -- that -- 1.12 has no dashed borders, see the adaptation note there). -- ------------------------------------------------------------------ function W.MakeBadge(parent, name) local b = CreateFrame("Button", name, parent) b:SetWidth(56) b:SetHeight(16) b:SetBackdrop(W.SOLID_BACKDROP) b.text = b:CreateFontString(nil, "OVERLAY") W.ApplyFont(b.text, 8) b.text:SetPoint("CENTER", b, "CENTER", 0, 0) b:RegisterForClicks("LeftButtonUp") return b end -- ------------------------------------------------------------------ -- Dark gold-trim button (titlebar Scan/Report/Options/X). -- ------------------------------------------------------------------ function W.MakeButton(parent, name, label, width, height) local C = DC.COLORS local b = CreateFrame("Button", name, parent) b:SetWidth(width or 64) b:SetHeight(height or 19) b:SetBackdrop(W.SOLID_BACKDROP) W.SetBackdropG(b, C.panel2, C.goldSoft) b.text = b:CreateFontString(nil, "OVERLAY") W.ApplyFont(b.text, 10) b.text:SetPoint("CENTER", b, "CENTER", 0, 0) W.SetTextColorG(b.text, C.goldHi) b.text:SetText(label) b:SetScript("OnEnter", function() W.SetBackdropG(this, C.panel3, C.gold) end) b:SetScript("OnLeave", function() W.SetBackdropG(this, C.panel2, C.goldSoft) end) return b end -- ------------------------------------------------------------------ -- Tooltip helpers. Convention: the builder only ADDS -- lines; AttachTooltip calls SetOwner before and Show after -- AddLine -- never calls Show itself. -- AttachTooltip CHAINS any existing OnEnter/OnLeave (no HookScript on -- 1.12 -- standard save-and-call pattern). -- ------------------------------------------------------------------ function W.TipLine(text, c, wrap) if not GameTooltip then return end local r, g, b = 1, 1, 1 if c then r, g, b = c.r, c.g, c.b end if wrap then GameTooltip:AddLine(text, r, g, b, 1) else GameTooltip:AddLine(text, r, g, b) end end -- Two-column tooltip line: label left, value flush RIGHT. This is what -- makes a breakdown read like an addition on paper -- the values line up -- in a column instead of trailing their labels at ragged positions. 1.12 -- has no monospace font and no tab stops; AddDoubleLine is the only way to -- get a right-aligned column, and it exists in this client's FrameXML. -- `cr` defaults to the label color when omitted. function W.TipDouble(left, right, c, cr) if not GameTooltip then return end local r, g, b = 1, 1, 1 if c then r, g, b = c.r, c.g, c.b end local r2, g2, b2 = r, g, b if cr then r2, g2, b2 = cr.r, cr.g, cr.b end GameTooltip:AddDoubleLine(left, right, r, g, b, r2, g2, b2) end -- W.dcTipOwner: our own record of which of OUR frames currently owns -- GameTooltip. WoW 1.12's GameTooltip has no "GetOwner" method (confirmed -- against FrameXML-1.12.1: zero real hits -- the only "GetOwner" match -- anywhere in that tree is the unrelated global function -- GetOwnerAuctionItems). Every caller in this addon that ever needs "who -- currently owns the tooltip" goes through W.AttachTooltip to open one in -- the first place, so this is the single place that needs to record it. -- Set right after SetOwner -- (matching order: SetOwner, then the builder runs, then Show -- a builder -- that itself re-SetOwner()s the SAME frame, like M.CellTooltip's -- ANCHOR_LEFT override, does not change who owns it). Cleared in OnLeave, -- guarded on identity so a late/out-of-order OnLeave can never clobber a -- newer owner. W.dcTipOwner = nil function W.AttachTooltip(frame, builder, anchor) frame.dcTipBuilder = builder frame.dcTipAnchor = anchor or "ANCHOR_LEFT" frame.dcOldEnter = frame:GetScript("OnEnter") frame.dcOldLeave = frame:GetScript("OnLeave") frame:SetScript("OnEnter", function() if this.dcOldEnter then this.dcOldEnter() end if GameTooltip and this.dcTipBuilder then GameTooltip:SetOwner(this, this.dcTipAnchor) W.dcTipOwner = this this.dcTipBuilder(this) GameTooltip:Show() end end) frame:SetScript("OnLeave", function() if this.dcOldLeave then this.dcOldLeave() end if W.dcTipOwner == this then W.dcTipOwner = nil end if GameTooltip then GameTooltip:Hide() end end) end