Repository navigation
Expand file tree
/
Copy pathEllesmereUI_Panel.lua
More file actions
5352 lines (4935 loc) · 244 KB
/
Copy pathEllesmereUI_Panel.lua
File metadata and controls
5352 lines (4935 loc) · 244 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_ClientGate.lua)
-------------------------------------------------------------------------------
-- EllesmereUI_Panel.lua
-- The options panel shell: main frame, sidebar, header, tabs, content
-- area, inline search, module registration, page selection, show/hide
-- and the sidebar unlock tip. Loads right after EllesmereUI.lua.
-------------------------------------------------------------------------------
local EllesmereUI = _G.EllesmereUI
-- Private namespace shared with EllesmereUI.lua (module registry, sidebar model).
local _, EUI_NS = ...
EUI_NS = EUI_NS.__euiCoreNS or EUI_NS -- standalone builds: the core's own table (EllesmereUI.lua)
local PP = EllesmereUI.PP
local PanelPP = EllesmereUI.PanelPP
local MakeFont = EllesmereUI.MakeFont
local MakeBorder = EllesmereUI.MakeBorder
local SolidTex = EllesmereUI.SolidTex
local lerp = EllesmereUI.lerp
local RegAccent = EllesmereUI.RegAccent
local ResetRowCounters = EllesmereUI.ResetRowCounters
local ResolveFactionTheme = EllesmereUI._ResolveFactionTheme
local ELLESMERE_GREEN = EllesmereUI.ELLESMERE_GREEN
local BORDER_COLOR = EllesmereUI.BORDER_COLOR
local DARK_BG = EllesmereUI.DARK_BG
local TEXT_DIM = EllesmereUI.TEXT_DIM
local TEXT_SECTION = EllesmereUI.TEXT_SECTION
local CONTENT_PAD = EllesmereUI.CONTENT_PAD
local STYLE = EllesmereUI._STYLE
local THEME_BG_FILES = EllesmereUI._THEME_BG_FILES
local IS_STANDALONE = EllesmereUI._IS_STANDALONE
local MEDIA_PATH = EllesmereUI.MEDIA_PATH
local ICONS_PATH = EllesmereUI.ICONS_PATH
local ADDON_ROSTER = EllesmereUI.ADDON_ROSTER
local CLASS_ART_MAP = EllesmereUI.CLASS_ART_MAP
local playerClass = EllesmereUI._playerClass
local modules = EUI_NS.modules
local _widgetRefreshList = EllesmereUI._widgetRefreshList
local IsAddonLoaded = C_AddOns.IsAddOnLoaded
-- Sidebar nav states
local NAV_SELECTED_TEXT = { r = STYLE.TEXT_WHITE_R, g = STYLE.TEXT_WHITE_G, b = STYLE.TEXT_WHITE_B, a = 1 }
local NAV_SELECTED_ICON_A = 1
local NAV_ENABLED_TEXT = { r = STYLE.TEXT_WHITE_R, g = STYLE.TEXT_WHITE_G, b = STYLE.TEXT_WHITE_B, a = 0.6 }
local NAV_ENABLED_ICON_A = 0.60
local NAV_DISABLED_TEXT = { r = 1, g = 1, b = 1, a = 0.11 }
local NAV_DISABLED_ICON_A = 0.20
local NAV_HOVER_ENABLED_TEXT = { r = 1, g = 1, b = 1, a = 0.86 }
local NAV_HOVER_DISABLED_TEXT = { r = 1, g = 1, b = 1, a = 0.39 }
local BG_WIDTH, BG_HEIGHT = 1500, 1154
local CLICK_W, CLICK_H = 1300, 946
local SIDEBAR_W = 295
local HEADER_H = 138 -- title + desc + banner glow + dark band for tabs
local TAB_BAR_H = 40
local FOOTER_H = 82
local mainFrame, bgFrame, clickArea, sidebar, contentFrame
local headerFrame, tabBar, scrollFrame, scrollChild, footerFrame, contentHeaderFrame
local sidebarButtons = {}
EUI_NS.sidebarButtons = sidebarButtons
local activeModule, activePage
local _lastPagePerModule = {}
local scrollTarget = 0
local isSmoothing = false
local smoothFrame
local UpdateScrollThumb
local suppressScrollRangeChanged = false
local lastHeaderPadded = false
local skipScrollChildReanchor = false
local function ClearWidgetRefreshList()
for i = 1, #_widgetRefreshList do _widgetRefreshList[i] = nil end
end
-- Snapshot/restore the refresh registry around off-screen widget builds (search-index
-- pre-build) so an unviewed page never leaks refresh closures into the displayed list.
function EllesmereUI._SnapshotAndClearWidgetRefreshList()
local snap = {}
for i = 1, #_widgetRefreshList do snap[i] = _widgetRefreshList[i] end
ClearWidgetRefreshList()
return snap
end
function EllesmereUI._RestoreWidgetRefreshList(snap)
ClearWidgetRefreshList()
for i = 1, #snap do _widgetRefreshList[i] = snap[i] end
end
-- Hide all children/regions of a frame without orphaning them
local HideAllChildren
do
local _hideAllScratch = {}
local function _packIntoScratch(...)
local n = select("#", ...)
for i = 1, n do _hideAllScratch[i] = select(i, ...) end
return n
end
HideAllChildren = function(parent, keepSet)
-- Pack children into reusable scratch table (one GetChildren call)
local n = _packIntoScratch(parent:GetChildren())
for i = 1, n do
if not (keepSet and keepSet[_hideAllScratch[i]]) then _hideAllScratch[i]:Hide() end
_hideAllScratch[i] = nil
end
n = _packIntoScratch(parent:GetRegions())
for i = 1, n do
if not (keepSet and keepSet[_hideAllScratch[i]]) then _hideAllScratch[i]:Hide() end
_hideAllScratch[i] = nil
end
-- Also hide custom root frames parented to scrollFrame (bypass scroll child)
if EllesmereUI._hideScrollFrameRoots then
EllesmereUI._hideScrollFrameRoots()
end
end
end
-- OnShow callbacks -- available immediately; mainFrame hooks in when created
local _onShowCallbacks = {}
function EllesmereUI:RegisterOnShow(fn)
_onShowCallbacks[#_onShowCallbacks + 1] = fn
end
-- OnHide callbacks -- fired when the settings panel closes
local _onHideCallbacks = {}
function EllesmereUI:RegisterOnHide(fn)
_onHideCallbacks[#_onHideCallbacks + 1] = fn
end
-------------------------------------------------------------------------------
-- Tab helpers (forward-declared, defined before CreateMainFrame uses them)
-------------------------------------------------------------------------------
local ClearTabs, CreateTabButton, BuildTabs, UpdateTabHighlight
local UpdateSidebarHighlight, ClearContent
-------------------------------------------------------------------------------
-- Build Main Frame
-------------------------------------------------------------------------------
local function CreateMainFrame()
if mainFrame then return mainFrame end
-----------------------------------------------------------------------
-- Root frame + scaling
-----------------------------------------------------------------------
mainFrame = CreateFrame("Frame", "EllesmereUIFrame", UIParent)
EllesmereUI._mainFrame = mainFrame
mainFrame:SetSize(BG_WIDTH, BG_HEIGHT)
mainFrame:SetPoint("CENTER")
mainFrame:SetFrameStrata("DIALOG")
mainFrame:SetFrameLevel(100)
mainFrame:Hide()
mainFrame:EnableMouse(false)
mainFrame:SetMovable(true)
mainFrame:SetScript("OnShow", function()
-- Recalculate pixel-perfect base scale on every open so resolution / UIParent scale changes apply
local physW2 = (GetPhysicalScreenSize())
local baseScale2 = GetScreenWidth() / physW2
local userScale2 = (EllesmereUIDB and EllesmereUIDB.panelScale) or 1.0
mainFrame:SetScale(baseScale2 * userScale2)
-- Re-sync PanelPP mult for the (possibly new) scale
if EllesmereUI.PanelPP then EllesmereUI.PanelPP.UpdateMult() end
-- The mini window was dragged and then closed: open with the panel on screen.
if EllesmereUI._panelNudge then EllesmereUI._KeepPanelOnScreen() end
-- Panel borders are resnapped by the tab-switch ResnapBordersUnder (active page only, ~2ms);
-- a global ResnapAllBorders here costs ~74ms and is unnecessary, since non-panel borders have their own scale and resnap triggers.
for _, fn in ipairs(_onShowCallbacks) do fn() end
-- Controller cursor: every open is one the player asked for (slash, button,
-- binding, pause menu), so bring the gamepad pointer up.
if EllesmereUI.PadNative() then EllesmereUI.RaiseGamePadCursor() end
end)
mainFrame:SetScript("OnHide", function()
-- Every close path (Escape, combat, the mini window's X) unfolds a collapsed
-- window, so the next open is always the full panel. A parent hiding it
-- (Alt-Z, a cinematic) leaves it shown: it stays folded and comes back as it was.
if EllesmereUI._panelCollapsed and not mainFrame:IsShown() then
EllesmereUI._SetPanelCollapsed(false)
end
-- Close the sidebar sync popup so it never lingers after the window is dismissed.
EllesmereUI.CloseSyncPopup()
if _onHideCallbacks then
for _, fn in ipairs(_onHideCallbacks) do fn() end
end
-- Built pages stay cached for the session: frames can never be freed, so
-- dropping them on close would only turn every revisit into a rebuild.
end)
-- Pixel-perfect scale: make 1 WoW unit = 1 screen pixel
local physW = (GetPhysicalScreenSize())
local baseScale = GetScreenWidth() / physW
local userScale = (EllesmereUIDB and EllesmereUIDB.panelScale) or 1.0
mainFrame:SetScale(baseScale * userScale)
-- Initialize PanelPP mult for the saved user scale
if EllesmereUI.PanelPP then EllesmereUI.PanelPP.UpdateMult() end
EllesmereUI.RegisterEscapeClose(mainFrame)
-- Panel body: every surface of the window hangs under it (parent panel-owned
-- frames to EllesmereUI._panelBody, not to mainFrame), so collapsing hides the
-- whole window while mainFrame -- the open options session -- stays shown.
local body = CreateFrame("Frame", nil, mainFrame)
body:SetAllPoints(mainFrame)
body:SetFrameLevel(mainFrame:GetFrameLevel())
EllesmereUI._panelBody = body
-----------------------------------------------------------------------
-- Background texture (dual-layer crossfade for smooth transitions)
-----------------------------------------------------------------------
bgFrame = CreateFrame("Frame", nil, body)
bgFrame:SetAllPoints(mainFrame)
bgFrame:SetFrameLevel(mainFrame:GetFrameLevel())
bgFrame:EnableMouse(false)
bgFrame:SetAlpha(1) -- mainFrame controls overall window opacity
-- Permanent base background: backdrop shadow (always visible behind everything)
local bgBase = bgFrame:CreateTexture(nil, "BACKGROUND", nil, -1)
bgBase:SetTexture(MEDIA_PATH .. "backgrounds\\eui-bg.png")
bgBase:SetAllPoints()
bgBase:SetAlpha(1)
-- Two crossfade layers (A = current, B = incoming). Only the active layer holds a
-- texture; the idle one is cleared after each transition to free GPU memory.
local bgA = bgFrame:CreateTexture(nil, "BACKGROUND", nil, 0)
bgA:SetAllPoints()
bgA:SetAlpha(1)
local bgB = bgFrame:CreateTexture(nil, "BACKGROUND", nil, 1)
bgB:SetAllPoints()
bgB:SetAlpha(0)
-- Track which layer is "front" (the one fading in)
local bgFront, bgBack = bgA, bgB
local bgFadeProgress = 1 -- 1 = fully transitioned (front is fully visible)
local BG_FADE_DURATION = 0.5
-- Title-bar theme boxes (the collapse button's box and the collapsed mini
-- window): one table, so they cost this function a single local. See tbox.Paint.
local tbox = {}
-- Accent hue via desaturate + vertex tint: base images are teal, so desaturating
-- removes the hue and vertex color re-tints to the chosen accent. Horde/Alliance
-- have dedicated background images and are used as-is (never desaturated/tinted).
-- TintColor returns nil for those, else the vertex colour of the desaturated art.
function tbox.TintColor(theme, r, g, b)
if theme == "EllesmereUI" or theme == "EllesmereUI Original" or theme == "EllesmereUI Forever"
or theme == "Horde" or theme == "Alliance" or theme == "Midnight" or theme == "Dark"
or theme == "Pixels" then
-- These themes use their native bg as-is (or no bg for Dark)
return nil
end
local minBright = 1.10
local maxBright = 1.60
local floor = 0.08
local lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
local darkFactor = 1 - lum
local bright = minBright + darkFactor * (maxBright - minBright)
return math.min(floor + r * bright, 1), math.min(floor + g * bright, 1), math.min(floor + b * bright, 1)
end
local function ApplyBgTintToLayer(layer, theme, r, g, b)
local fr, fg, fb = tbox.TintColor(theme, r, g, b)
if fr then
layer:SetDesaturated(true)
layer:SetVertexColor(fr, fg, fb, 1)
else
layer:SetDesaturated(false)
layer:SetVertexColor(1, 1, 1, 1)
end
end
-- A colour picked off the art (r, g, b), as the art shows it under the theme's tint.
function tbox.Tinted(theme, tr, tg, tb, r, g, b)
local fr, fg, fb = tbox.TintColor(theme, tr, tg, tb)
if not fr then return r, g, b end
local lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
return lum * fr, lum * fg, lum * fb
end
-- Active theme and its colour (the tint input), for painting outside a theme change.
function tbox.Theme()
local theme = ResolveFactionTheme(EllesmereUIDB and EllesmereUIDB.activeTheme or EllesmereUI.DEFAULT_THEME)
return theme, EllesmereUI.ResolveThemeColor(theme)
end
-- Glyph cuts of EllesmereUI.RESIZE_ICON (texcoords), plus an optical nudge
-- (x, y canvas units; y down): the expand arrow 1 toward its tail, the
-- collapse arrow 1 right and 1 down. Field 7: size change from the art's
-- glyph size (the collapse arrow 1 smaller).
tbox.GLYPH = {
collapse = { 5 / 30, 15 / 30, 16 / 30, 26 / 30, 1, 1, -1 }, -- down-left
expand = { 17 / 30, 27 / 30, 4 / 30, 14 / 30, -1, 1 }, -- up-right
}
-- Canvas distance from the painted close box to its copy (the collapse box).
-- A multiple of 4 keeps the copy on the art's pixel grid at the 75% and 125%
-- panel scales as well as at 100/150/200%, so its border matches the original.
tbox.DX = 48
-- A theme box is a copy of the close box painted into the theme art
-- (EllesmereUI.THEME_CLOSE_BOX), cut from the same art: its border as four
-- strips (top and bottom carry the corners; the open centre shows the art
-- below), dx canvas units left of the painted box, plus an arrow glyph in the
-- X's colours. owner holds the textures; canvas point (ox, oy) sits on
-- anchor's TOPLEFT. With no glyph it is a hover glow instead: the border
-- ring alone, additive, in owner's highlight layer.
function tbox.New(owner, anchor, ox, oy, dx, glyph)
local set = { anchor = anchor, ox = ox, oy = oy, dx = dx, glyph = glyph, alpha = 1, s = {} }
for i = 1, 4 do
local t = owner:CreateTexture(nil, glyph and "BACKGROUND" or "HIGHLIGHT")
if not glyph then t:SetBlendMode("ADD") end
set.s[i] = t
end
if glyph then
local tc = tbox.GLYPH[glyph]
local icon = EllesmereUI.RESIZE_ICON
set.shadow = owner:CreateTexture(nil, "BORDER")
set.base = owner:CreateTexture(nil, "BORDER")
set.acc = owner:CreateTexture(nil, "BORDER")
set.shadow:SetTexture(icon)
set.base:SetTexture(icon)
set.acc:SetTexture(icon)
set.shadow:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
set.base:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
set.acc:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
set.shadow:SetVertexColor(0, 0, 0, 1)
local acc = set.acc
RegAccent({ type = "callback", fn = function(r, g, b)
acc:SetVertexColor(r, g, b, acc:GetAlpha())
end })
end
return set
end
-- Draw order of a glyph set: strips at BACKGROUND stripSub, glyph pieces
-- (rim, colour, accent) at BORDER glyphSub, +2, +4.
function tbox.Layer(set, stripSub, glyphSub)
for i = 1, 4 do set.s[i]:SetDrawLayer("BACKGROUND", stripSub) end
set.shadow:SetDrawLayer("BORDER", glyphSub)
set.base:SetDrawLayer("BORDER", glyphSub + 2)
set.acc:SetDrawLayer("BORDER", glyphSub + 4)
end
-- Show a set at alpha a (0 hides it). Texture alpha and vertex alpha are one
-- channel, so every repaint ends here to put the set's alpha back.
function tbox.Alpha(set, a)
set.alpha = a
local spec = set.spec
local shown = a > 0 and spec ~= nil
for i = 1, 4 do
local t = set.s[i]
t:SetShown(shown)
t:SetAlpha(a)
end
if set.base then
set.base:SetShown(shown)
set.base:SetAlpha(a)
local sh = shown and spec.sh or 0
set.shadow:SetShown(sh > 0)
set.shadow:SetAlpha(sh * a)
local acc = shown and spec.acc or 0
set.acc:SetShown(acc > 0)
set.acc:SetAlpha(acc * a)
end
end
-- Paint a set from the theme's art (tr, tg, tb = the theme colour, the tint input).
function tbox.Paint(set, theme, tr, tg, tb)
local file = THEME_BG_FILES[theme] or THEME_BG_FILES["EllesmereUI"]
local spec = EllesmereUI.THEME_CLOSE_BOX[file]
set.spec = spec
if spec then
local path = MEDIA_PATH .. file
-- A copy takes the whole tight rect at a 6 unit depth (halo, border and
-- inner line, clear of the X); a glow takes just the border line, so the
-- flat fill and the corners outside the rounded border never light up.
local x, y, w, h, S = spec.x, spec.y, spec.w, spec.h, 6
if not set.base then x, y, w, h, S = spec.bx, spec.by, spec.bw, spec.bh, spec.bd end
local px, py = x - set.dx - set.ox, y - set.oy
for i = 1, 4 do
local sx, sy, sw, sh = 0, 0, w, S -- top
if i == 2 then sy = h - S -- bottom
elseif i == 3 then sy, sw, sh = S, S, h - 2 * S -- left
elseif i == 4 then sx, sy, sw, sh = w - S, S, S, h - 2 * S -- right
end
local t = set.s[i]
t:SetTexture(path)
t:SetTexCoord((x + sx) / BG_WIDTH, (x + sx + sw) / BG_WIDTH, (y + sy) / BG_HEIGHT, (y + sy + sh) / BG_HEIGHT)
t:SetSize(sw, sh)
t:ClearAllPoints()
t:SetPoint("TOPLEFT", set.anchor, "TOPLEFT", px + sx, -(py + sy))
ApplyBgTintToLayer(t, theme, tr, tg, tb)
end
if set.base then
local tc = tbox.GLYPH[set.glyph]
local cx, cy = spec.gx - set.dx + tc[5] - set.ox, spec.gy + tc[6] - set.oy
local gs = (spec.gs or 16) + (tc[7] or 0) -- the rim is 2 units wider than the glyph
set.shadow:SetSize(gs + 2, gs + 2)
set.base:SetSize(gs, gs)
set.acc:SetSize(gs, gs)
set.shadow:ClearAllPoints()
set.shadow:SetPoint("CENTER", set.anchor, "TOPLEFT", cx, -cy)
set.base:ClearAllPoints()
set.base:SetPoint("CENTER", set.anchor, "TOPLEFT", cx, -cy)
set.acc:ClearAllPoints()
set.acc:SetPoint("CENTER", set.anchor, "TOPLEFT", cx, -cy)
local r, g, b = tbox.Tinted(theme, tr, tg, tb, spec.r, spec.g, spec.b)
set.base:SetVertexColor(r, g, b, 1)
set.acc:SetVertexColor(ELLESMERE_GREEN.r, ELLESMERE_GREEN.g, ELLESMERE_GREEN.b, 1)
end
end
tbox.Alpha(set, set.alpha)
end
-- Hover glow and tooltip for a title-bar box's hit area: the box border again,
-- additive, shown by the button's highlight layer and brighter while pressed.
-- Painted on enter from the art in use, so it costs nothing until hovered.
function tbox.Hover(btn, anchor, ox, oy, dx, tip)
local glow = tbox.New(btn, anchor, ox, oy, dx)
btn:SetScript("OnEnter", function(self)
glow.alpha = 0.5
tbox.Paint(glow, tbox.Theme())
if tip then EllesmereUI.ShowWidgetTooltip(self, tip) end
end)
btn:SetScript("OnLeave", function()
if tip then EllesmereUI.HideWidgetTooltip() end
end)
btn:SetScript("OnMouseDown", function() tbox.Alpha(glow, 0.85) end)
btn:SetScript("OnMouseUp", function() tbox.Alpha(glow, 0.5) end)
end
-- The collapse box: a front/back pair on bgFrame that swaps and crossfades
-- with the theme art it is cut from (ApplyThemeBG, bgFadeTicker).
tbox.front = tbox.New(bgFrame, bgFrame, 0, 0, tbox.DX, "collapse")
tbox.back = tbox.New(bgFrame, bgFrame, 0, 0, tbox.DX, "collapse")
tbox.Layer(tbox.front, 4, 1)
tbox.Layer(tbox.back, 3, 0)
-- Background crossfade ticker: old stays solid, new fades in on top with the same
-- ease-in-out curve as the accent transition, so both animations track visually.
local bgFadeTicker = CreateFrame("Frame", nil, bgFrame)
bgFadeTicker:Hide()
bgFadeTicker:SetScript("OnUpdate", function(self, elapsed)
bgFadeProgress = bgFadeProgress + elapsed / BG_FADE_DURATION
-- The accent overlay sits above both layers, so it is driven here only while
-- a theme that has one is entering (dir 1) or leaving (dir -1).
local ov = bgFrame._accentOverlayDir and bgFrame._accentOverlay
if bgFadeProgress >= 1 then
bgFadeProgress = 1
bgFront:SetAlpha(1)
-- The collapse box pair is not occluded (its rect differs per art), so
-- the outgoing one hides.
tbox.Alpha(tbox.front, 1)
tbox.Alpha(tbox.back, 0)
-- bgBack stays solid behind bgFront (occluded, zero cost). NEVER clear it
-- here (single-frame flash); the next ApplyThemeBG replaces its texture.
if ov then
if bgFrame._accentOverlayDir > 0 then
ov:SetAlpha(1)
else
-- Faded out over the new theme: empty it so it holds no texture.
ov:Hide()
ov:SetTexture(nil)
bgFrame._accentOverlayPath = nil
end
bgFrame._accentOverlayDir = nil
end
self:Hide()
else
-- Ease-in-out: slow start, fast middle, slow end
local t = bgFadeProgress
t = t < 0.5 and (2 * t * t) or (1 - (-2 * t + 2) * (-2 * t + 2) / 2)
bgBack:SetAlpha(1)
bgFront:SetAlpha(t)
tbox.Alpha(tbox.front, t)
-- The outgoing box stays near solid until late, like bgBack: a plain
-- 1 - t leaves both boxes part-transparent mid-fade and they dim.
tbox.Alpha(tbox.back, 1 - t * t * t)
if ov then
local from = bgFrame._accentOverlayFrom
if bgFrame._accentOverlayDir > 0 then
ov:SetAlpha(from + (1 - from) * t)
else
ov:SetAlpha(from * (1 - t))
end
end
end
end)
-- Accent overlay (EllesmereUI.THEME_ACCENT_OVERLAYS): one texture above the
-- crossfade pair (BACKGROUND 2), on our own bgFrame, tinted with the live UI
-- accent through a single RegAccent entry made when it is created. It is built
-- on the first apply of a theme that has one; a theme without one fades it out
-- and empties it, so it costs nothing while unused. faded = false (the panel's
-- first build) shows it at once instead of fading it in.
local function ApplyThemeAccentOverlay(theme, faded)
local spec = EllesmereUI.THEME_ACCENT_OVERLAYS[theme]
local ov = bgFrame._accentOverlay
if not spec then
if ov and ov:IsShown() then
bgFrame._accentOverlayFrom = ov:GetAlpha()
bgFrame._accentOverlayDir = -1
end
return
end
if not ov then
ov = bgFrame:CreateTexture(nil, "BACKGROUND", nil, 2)
bgFrame._accentOverlay = ov
ov:SetVertexColor(ELLESMERE_GREEN.r, ELLESMERE_GREEN.g, ELLESMERE_GREEN.b, 1)
-- A callback, not a "vertex" entry: on a texture the vertex alpha IS its
-- alpha, and the vertex entry writes 1, which would stomp the crossfade
-- (every theme pick re-applies the accent right after starting it).
RegAccent({ type = "callback", fn = function(r, g, b)
ov:SetVertexColor(r, g, b, ov:GetAlpha())
end })
end
local path = MEDIA_PATH .. spec.file
-- A re-pick (or a return mid fade-out) keeps the art and fades on from its alpha.
if not (ov:IsShown() and bgFrame._accentOverlayPath == path) then
ov:ClearAllPoints()
ov:SetPoint("TOPLEFT", bgFrame, "TOPLEFT", spec.x, -spec.y)
ov:SetSize(spec.w, spec.h)
ov:SetTexture(path)
bgFrame._accentOverlayPath = path
ov:SetAlpha(0)
ov:Show()
end
if faded then
bgFrame._accentOverlayFrom = ov:GetAlpha()
bgFrame._accentOverlayDir = 1
else
ov:SetAlpha(1)
bgFrame._accentOverlayDir = nil
end
end
--- Apply the full theme: crossfade to new background image + tint
local function ApplyThemeBG(theme, r, g, b)
theme = ResolveFactionTheme(theme)
local file = THEME_BG_FILES[theme] or THEME_BG_FILES["EllesmereUI"]
local newPath = MEDIA_PATH .. file
-- Swap roles: old front becomes back, new incoming becomes front
bgBack, bgFront = bgFront, bgBack
-- New front layer gets the target texture + tint on top; this SetTexture is also
-- the lazy cleanup of the previous transition's idle layer.
bgFront:SetTexture(newPath)
ApplyBgTintToLayer(bgFront, theme, r, g, b)
bgFront:SetDrawLayer("BACKGROUND", 1)
bgFront:SetAlpha(0)
-- Old layer stays fully solid underneath
bgBack:SetDrawLayer("BACKGROUND", 0)
bgBack:SetAlpha(1)
-- The collapse box is cut from the same art: its pair swaps the same way.
tbox.front, tbox.back = tbox.back, tbox.front
tbox.Layer(tbox.front, 4, 1)
tbox.Layer(tbox.back, 3, 0)
tbox.front.alpha = 0
tbox.Paint(tbox.front, theme, r, g, b)
tbox.Alpha(tbox.back, 1)
-- A collapsed window shows the mini instead: it takes the new art at once.
if EllesmereUI._panelCollapsed then tbox.SkinMini(theme, r, g, b) end
-- Start crossfade (the accent overlay, if either theme has one, rides it)
bgFadeProgress = 0
ApplyThemeAccentOverlay(theme, true)
bgFadeTicker:Show()
-- The sidebar opacity slider's orientation follows the theme.
if EllesmereUI._layoutOpacitySlider then EllesmereUI._layoutOpacitySlider() end
end
-- For tint-only updates (Custom Color picker dragging), update the front layer directly
local function ApplyBgTint(r, g, b)
local theme = ResolveFactionTheme((EllesmereUIDB or {}).activeTheme or EllesmereUI.DEFAULT_THEME)
ApplyBgTintToLayer(bgFront, theme, r, g, b)
tbox.Paint(tbox.front, theme, r, g, b)
if EllesmereUI._panelCollapsed then tbox.SkinMini(theme, r, g, b) end
end
-- Apply initial theme at creation (no crossfade, just set correct texture + tint)
-- Resolve theme color directly -- ELLESMERE_GREEN is the UI accent which may differ
local _initTheme = ResolveFactionTheme((EllesmereUIDB or {}).activeTheme or EllesmereUI.DEFAULT_THEME)
local _initFile = THEME_BG_FILES[_initTheme] or THEME_BG_FILES["EllesmereUI"]
local _initR, _initG, _initB = EllesmereUI.ResolveThemeColor(_initTheme)
bgA:SetTexture(MEDIA_PATH .. _initFile)
ApplyBgTintToLayer(bgA, _initTheme, _initR, _initG, _initB)
tbox.Paint(tbox.front, _initTheme, _initR, _initG, _initB)
ApplyThemeAccentOverlay(_initTheme, false)
EllesmereUI._bgTexture = bgA
EllesmereUI._applyBgTint = ApplyBgTint
EllesmereUI._applyThemeBG = ApplyThemeBG
-----------------------------------------------------------------------
-- Click area (1300x946, centred)
-----------------------------------------------------------------------
clickArea = CreateFrame("Frame", "EllesmereUIClickArea", body)
EllesmereUI._clickArea = clickArea
clickArea:SetSize(CLICK_W, CLICK_H)
clickArea:SetPoint("CENTER", mainFrame, "CENTER", 0, 0)
clickArea:SetFrameLevel(mainFrame:GetFrameLevel() + 1)
clickArea:EnableMouse(true)
clickArea:SetMovable(true)
clickArea:RegisterForDrag("LeftButton")
-- No SetClampedToScreen: the whole window moves as one and drags freely off any edge.
clickArea:SetScript("OnDragStart", function() mainFrame:StartMoving() end)
clickArea:SetScript("OnDragStop", function() mainFrame:StopMovingOrSizing() end)
-- Shift + mouse wheel over any part of the panel without a wheel of its own (the
-- header, the footer, the margins) scales it too; a plain wheel there does nothing.
clickArea:EnableMouseWheel(true)
clickArea:SetScript("OnMouseWheel", function(_, delta) EllesmereUI._ShiftWheelScale(delta) end)
-- Controller cursor: the drag surface is not a stop; its children are.
EllesmereUI.PadHint(clickArea, "nodepass")
-----------------------------------------------------------------------
-- Close button (invisible hit area over background X graphic)
-----------------------------------------------------------------------
local closeBtn = CreateFrame("Button", nil, clickArea)
closeBtn:SetSize(40, 40)
closeBtn:SetPoint("TOPRIGHT", clickArea, "TOPRIGHT", -14, -11)
closeBtn:SetFrameLevel(clickArea:GetFrameLevel() + 20)
closeBtn:SetScript("OnClick", function() EllesmereUI:Hide() end)
tbox.Hover(closeBtn, bgFrame, 0, 0, 0)
-- Controller cursor: its cancel press closes the window through this button.
if EllesmereUI.PadCP() then mainFrame.CloseButton = closeBtn end
-----------------------------------------------------------------------
-- Collapse button (hit area over tbox.front/back, left of the close X)
-- and the mini window it folds the panel to
-----------------------------------------------------------------------
do
local collapseBtn = CreateFrame("Button", nil, clickArea)
collapseBtn:SetSize(40, 40)
collapseBtn:SetPoint("TOPRIGHT", clickArea, "TOPRIGHT", -(tbox.DX + 14), -11)
collapseBtn:SetFrameLevel(clickArea:GetFrameLevel() + 20)
collapseBtn:SetScript("OnClick", function() EllesmereUI._SetPanelCollapsed(true) end)
tbox.Hover(collapseBtn, bgFrame, 0, 0, tbox.DX, EllesmereUI.L("Collapse"))
end
-- Built on the first collapse. Every piece is a rect of art the panel already
-- has loaded, drawn exactly where it sits on the panel, so nothing moves under
-- the cursor: the bar is the panel's own top-right corner (its painted close
-- box included) with its top rows flipped under it as the bottom edge, the
-- expand box lands on the collapse box, and the badge is the painted logo
-- emblem cut to a circle. No events and no OnUpdate.
function tbox.BuildMini()
local MX, MY = 1206, 98 -- canvas point of the mini window's top-left
local W, H = 197, 76
local MASK = MEDIA_PATH .. "portraits\\circle_mask.tga"
local mini = CreateFrame("Frame", nil, mainFrame)
mini:Hide()
mini:SetSize(W, H)
mini:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", MX, -MY)
mini:SetFrameLevel(mainFrame:GetFrameLevel() + 2)
-- Composed as one image before the panel opacity applies, so at reduced
-- opacity the stacked pieces (bar, ring, badge) never show through each other.
mini:SetFlattensRenderLayers(true)
mini:EnableMouse(true)
mini:RegisterForDrag("LeftButton")
mini:SetScript("OnDragStart", function() mainFrame:StartMoving() end)
mini:SetScript("OnDragStop", function()
mainFrame:StopMovingOrSizing()
EllesmereUI._panelNudge = true
end)
EllesmereUI._panelMini = mini
-- Shadow: the panel shadow around the same corner (rows 24-135) and those
-- rows flipped below, symmetric about the bar's middle; the cut end fades.
local shadowFile = MEDIA_PATH .. "backgrounds\\eui-bg.png"
local fadeFrom, fadeTo = CreateColor(1, 1, 1, 0), CreateColor(1, 1, 1, 1)
for half = 0, 1 do
for piece = 0, 1 do
local x0 = (piece == 0) and 1244 or 1314
local x1 = (piece == 0) and 1314 or 1480
local top, bot = 24 / BG_HEIGHT, 136 / BG_HEIGHT
if half == 1 then top, bot = bot, top end
local t = mini:CreateTexture(nil, "BACKGROUND", nil, -8)
t:SetTexture(shadowFile)
t:SetTexCoord(x0 / BG_WIDTH, x1 / BG_WIDTH, top, bot)
t:SetSize(x1 - x0, 112)
t:SetPoint("TOPLEFT", mini, "TOPLEFT", x0 - MX, (half == 0) and 74 or -38)
if piece == 0 then t:SetGradient("HORIZONTAL", fadeFrom, fadeTo) end
end
end
-- Bar: canvas 1244-1403 x 101-157, then rows 101-113 flipped as its bottom edge.
local barTop = mini:CreateTexture(nil, "BACKGROUND", nil, 0)
barTop:SetSize(159, 57)
barTop:SetPoint("TOPLEFT", mini, "TOPLEFT", 1244 - MX, -(101 - MY))
barTop:SetTexCoord(1244 / BG_WIDTH, 1403 / BG_WIDTH, 101 / BG_HEIGHT, 158 / BG_HEIGHT)
local barCap = mini:CreateTexture(nil, "BACKGROUND", nil, 0)
barCap:SetSize(159, 13)
barCap:SetPoint("TOPLEFT", mini, "TOPLEFT", 1244 - MX, -(158 - MY))
barCap:SetTexCoord(1244 / BG_WIDTH, 1403 / BG_WIDTH, 114 / BG_HEIGHT, 101 / BG_HEIGHT)
local barOv = mini:CreateTexture(nil, "BACKGROUND", nil, 1)
-- Expand box and its hit areas (the close X is the painted one in the bar).
local expandBox = tbox.New(mini, mini, MX, MY, tbox.DX, "expand")
tbox.Layer(expandBox, 2, 0)
local expandBtn = CreateFrame("Button", nil, mini)
expandBtn:SetSize(40, 40)
expandBtn:SetPoint("TOPLEFT", mini, "TOPLEFT", (1346 - tbox.DX) - MX, -(115 - MY))
expandBtn:SetFrameLevel(mini:GetFrameLevel() + 5)
expandBtn:SetScript("OnClick", function() EllesmereUI._SetPanelCollapsed(false) end)
tbox.Hover(expandBtn, mini, MX, MY, tbox.DX, EllesmereUI.L("Expand"))
local miniClose = CreateFrame("Button", nil, mini)
miniClose:SetSize(40, 40)
miniClose:SetPoint("TOPLEFT", mini, "TOPLEFT", 1346 - MX, -(115 - MY))
miniClose:SetFrameLevel(mini:GetFrameLevel() + 5)
miniClose:SetScript("OnClick", function() EllesmereUI:Hide() end)
tbox.Hover(miniClose, mini, MX, MY, 0)
-- Badge: a thin ring (a disc 2 units wider behind it) and the emblem square.
local ring = mini:CreateTexture(nil, "ARTWORK", nil, 0)
ring:SetSize(H + 4, H + 4)
ring:SetPoint("CENTER", mini, "TOPLEFT", H / 2, -H / 2)
local ringMask = mini:CreateMaskTexture()
ringMask:SetTexture(MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
ringMask:SetSize((H + 4) * 128 / 104, (H + 4) * 128 / 104) -- mask circle = 104/128
ringMask:SetPoint("CENTER", ring, "CENTER")
ring:AddMaskTexture(ringMask)
local badge = mini:CreateTexture(nil, "ARTWORK", nil, 1)
badge:SetSize(H, H)
badge:SetPoint("TOPLEFT", mini, "TOPLEFT", 0, 0)
local badgeOv = mini:CreateTexture(nil, "ARTWORK", nil, 2)
local badgeMask = mini:CreateMaskTexture()
badgeMask:SetTexture(MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
badgeMask:SetSize(H * 128 / 104, H * 128 / 104)
badgeMask:SetPoint("CENTER", badge, "CENTER")
badge:AddMaskTexture(badgeMask)
badgeOv:AddMaskTexture(badgeMask)
-- A theme's accent overlay (EllesmereUI.THEME_ACCENT_OVERLAYS) cut to the
-- canvas rect x0,y0 - x1,y1 with canvas point (ax, ay) on the mini's
-- TOPLEFT; hidden when the theme has none or the rect misses it.
local function CutOverlay(tex, ov, x0, y0, x1, y1, ax, ay)
if ov then
x0, y0 = math.max(x0, ov.x), math.max(y0, ov.y)
x1, y1 = math.min(x1, ov.x + ov.w), math.min(y1, ov.y + ov.h)
end
if not ov or x1 <= x0 or y1 <= y0 then tex:Hide(); return end
tex:SetTexture(MEDIA_PATH .. ov.file)
tex:SetTexCoord((x0 - ov.x) / ov.w, (x1 - ov.x) / ov.w, (y0 - ov.y) / ov.h, (y1 - ov.y) / ov.h)
tex:SetSize(x1 - x0, y1 - y0)
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", mini, "TOPLEFT", x0 - ax, -(y0 - ay))
tex:SetVertexColor(ELLESMERE_GREEN.r, ELLESMERE_GREEN.g, ELLESMERE_GREEN.b, 1)
tex:Show()
end
RegAccent({ type = "callback", fn = function(r, g, b)
barOv:SetVertexColor(r, g, b, 1)
badgeOv:SetVertexColor(r, g, b, 1)
end })
-- Re-skinned on each collapse (and on a theme change while collapsed).
function tbox.SkinMini(theme, tr, tg, tb)
local file = THEME_BG_FILES[theme] or THEME_BG_FILES["EllesmereUI"]
local spec = EllesmereUI.THEME_CLOSE_BOX[file] or EllesmereUI.THEME_CLOSE_BOX[THEME_BG_FILES["EllesmereUI"]]
local path = MEDIA_PATH .. file
barTop:SetTexture(path)
ApplyBgTintToLayer(barTop, theme, tr, tg, tb)
barCap:SetTexture(path)
ApplyBgTintToLayer(barCap, theme, tr, tg, tb)
local ex, ey = spec.ex, spec.ey
badge:SetTexture(path)
badge:SetTexCoord(ex / BG_WIDTH, (ex + H) / BG_WIDTH, ey / BG_HEIGHT, (ey + H) / BG_HEIGHT)
ApplyBgTintToLayer(badge, theme, tr, tg, tb)
local rr, rg, rb = tbox.Tinted(theme, tr, tg, tb, spec.rr, spec.rg, spec.rb)
ring:SetColorTexture(rr, rg, rb, 1)
local ov = EllesmereUI.THEME_ACCENT_OVERLAYS[theme]
CutOverlay(barOv, ov, 1244, 101, 1403, 158, MX, MY)
CutOverlay(badgeOv, ov, ex, ey, ex + H, ey + H, ex, ey)
tbox.Paint(expandBox, theme, tr, tg, tb)
end
return mini
end
-- Fold the window to the mini window (on) or back. mainFrame stays shown, so
-- the options session goes on: no show/hide callback runs, and previews and
-- editing sessions keep going. Popups that close on an outside click are
-- closed by the click itself; the rest close here, and panel-owned surfaces
-- parented outside the body follow through EllesmereUI:RegisterOnCollapse.
function EllesmereUI._SetPanelCollapsed(on)
local mini = EllesmereUI._panelMini
local hooks = EllesmereUI._onCollapseCallbacks
EllesmereUI.HideWidgetTooltip(true)
if on then
if EllesmereUI._panelCollapsed or not mainFrame:IsShown() then return end
EllesmereUI._panelCollapsed = true
EllesmereUI.CloseSyncPopup()
-- Name/icon popups sit on UIParent with no click-off (the panel's own
-- OnHide closes them for the same reason).
if EllesmereUI._specOvNamePopup then EllesmereUI._specOvNamePopup:Hide() end
local cond = EllesmereUI._CondOv
if cond and cond._namePopup then cond._namePopup:Hide() end
body:Hide()
mini = mini or tbox.BuildMini()
tbox.SkinMini(tbox.Theme())
mini:Show()
for i = 1, #hooks do hooks[i](true) end
elseif EllesmereUI._panelCollapsed then
EllesmereUI._panelCollapsed = nil
mini:Hide()
body:Show()
-- Unfolded by a close (mainFrame's OnHide): the OnHide callbacks take it
-- from here, and a deferred page rebuild waits for the next open.
if not mainFrame:IsShown() then return end
-- Dropped somewhere else: keep the unfolded panel on screen.
if EllesmereUI._panelNudge then EllesmereUI._KeepPanelOnScreen() end
-- Pages are only ever built on screen (RefreshPage defers while folded).
if EllesmereUI._pendingForceRefresh then
EllesmereUI._pendingForceRefresh = nil
EllesmereUI:RefreshPage(true)
end
for i = 1, #hooks do hooks[i](false) end
end
end
-- Shift the window so the panel itself (the click area, not the art's shadow
-- margin) is on screen; a panel taller or wider than the screen keeps its top
-- left corner. Runs once after the mini window was dragged.
function EllesmereUI._KeepPanelOnScreen()
EllesmereUI._panelNudge = nil
local l, r, t, b = clickArea:GetLeft(), clickArea:GetRight(), clickArea:GetTop(), clickArea:GetBottom()
local ml, mb = mainFrame:GetLeft(), mainFrame:GetBottom()
if not (l and r and t and b and ml and mb) then return end
local isv = issecretvalue
if isv and (isv(l) or isv(r) or isv(t) or isv(b) or isv(ml) or isv(mb)) then return end
local s = UIParent:GetEffectiveScale() / mainFrame:GetEffectiveScale()
local sw, sh = UIParent:GetWidth() * s, UIParent:GetHeight() * s
local dx, dy = 0, 0
if r > sw then dx = sw - r end
if l + dx < 0 then dx = -l end
if b < 0 then dy = -b end
if t + dy > sh then dy = sh - t end
if dx ~= 0 or dy ~= 0 then
mainFrame:ClearAllPoints()
mainFrame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", ml + dx, mb + dy)
end
end
-----------------------------------------------------------------------
-- Sidebar
-----------------------------------------------------------------------
sidebar = CreateFrame("Frame", nil, clickArea)
sidebar:SetSize(SIDEBAR_W, CLICK_H)
sidebar:SetPoint("TOPLEFT", clickArea, "TOPLEFT", 0, 0)
sidebar:SetFrameLevel(clickArea:GetFrameLevel() + 2)
EllesmereUI._sidebar = sidebar
-- Nav buttons -- start below the logo area with proper spacing
local NAV_TOP = -114 -- distance from sidebar top to first nav item
local NAV_ROW_H = 40 -- height per nav row (Unlock / Global / Patch Notes / Profiles)
local NAV_ICON_W = 46 -- exact pixel width
local NAV_ICON_H = 31 -- exact pixel height
local NAV_LEFT = 20 -- left padding for icon
local NAV_TXT_GAP = 10 -- gap between icon and label
-- Helper: create a 1px horizontal glow line on a sidebar button (TOP or BOTTOM edge)
local function MakeNavEdgeLine(btn, edge)
local g = btn:CreateTexture(nil, "BORDER")
g:SetHeight(1)
PanelPP.Point(g, edge .. "LEFT", btn, edge .. "LEFT", 0, 0)
PanelPP.Point(g, edge .. "RIGHT", btn, edge .. "RIGHT", 0, 0)
g:SetColorTexture(0.7, 0.7, 0.7, 1)
g:SetGradient("HORIZONTAL", CreateColor(0.7, 0.7, 0.7, 0.5), CreateColor(0.7, 0.7, 0.7, 0))
g:Hide()
return g
end
-- Horizontal gradient glow on a sidebar button, anchored top+bottom so it scales
-- with the row height (group headers and child rows differ).
local function MakeNavGradient(btn, r, g, b, startA)
local tex = btn:CreateTexture(nil, "BACKGROUND")
tex:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
tex:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0)
tex:SetColorTexture(r, g, b, 1)
tex:SetGradient("HORIZONTAL", CreateColor(r, g, b, startA), CreateColor(r, g, b, 0))
tex:Hide()
return tex
end
-- Helper: attach the shared decoration set to a sidebar nav button
-- (active indicator, selection glow, top/bottom edge lines, hover glow, hover indicator)
local function DecorateSidebarButton(btn)
local EG = ELLESMERE_GREEN
btn._indicator = SolidTex(btn, "ARTWORK", EG.r, EG.g, EG.b, 1)
btn._indicator:SetWidth(3)
btn._indicator:SetPoint("TOPLEFT", btn, "TOPLEFT", -1, 0)
btn._indicator:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", -1, 0)
btn._indicator:Hide()
RegAccent({ type="solid", obj=btn._indicator, a=1 })
btn._glow = MakeNavGradient(btn, EG.r, EG.g, EG.b, 0.15)
RegAccent({ type="gradient", obj=btn._glow, startA=0.15 })
btn._glowTop = MakeNavEdgeLine(btn, "TOP")
btn._glowBot = MakeNavEdgeLine(btn, "BOTTOM")
local hR, hG, hB = 0.85, 0.95, 0.90
btn._hoverGlow = MakeNavGradient(btn, hR, hG, hB, 0.03)
btn._hoverIndicator = SolidTex(btn, "ARTWORK", hR, hG, hB, 0.25)
btn._hoverIndicator:SetWidth(3)
btn._hoverIndicator:SetPoint("TOPLEFT", btn, "TOPLEFT", -1, 0)
btn._hoverIndicator:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", -1, 0)
btn._hoverIndicator:Hide()
end
-------------------------------------------------------------------
-- Unlock Mode button (always top, not a module -- just triggers unlock)
-------------------------------------------------------------------
do
local btn = CreateFrame("Button", nil, sidebar)
btn:SetSize(SIDEBAR_W, NAV_ROW_H)
btn:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 0, NAV_TOP)
btn:SetFrameLevel(sidebar:GetFrameLevel() + 1)
DecorateSidebarButton(btn)
-- Glow layer (behind icon): tinted version of the -on texture
local iconGlow = btn:CreateTexture(nil, "ARTWORK", nil, 0)
iconGlow:SetTexture(ICONS_PATH .. "sidebar\\unlockmode-ig-on.png")
iconGlow:SetSize(NAV_ICON_W, NAV_ICON_H)
iconGlow:SetPoint("LEFT", btn, "LEFT", NAV_LEFT, 0)
iconGlow:SetDesaturated(true)
iconGlow:SetVertexColor(ELLESMERE_GREEN.r, ELLESMERE_GREEN.g, ELLESMERE_GREEN.b, 1)
iconGlow:Hide()
btn._iconGlow = iconGlow
RegAccent({ type="vertex", obj=iconGlow })
-- Icon layer (on top of glow): always the white off texture
local icon = btn:CreateTexture(nil, "ARTWORK", nil, 1)
icon:SetTexture(ICONS_PATH .. "sidebar\\unlockmode-ig.png")
icon:SetSize(NAV_ICON_W, NAV_ICON_H)
icon:SetPoint("LEFT", btn, "LEFT", NAV_LEFT, 0)
btn._icon = icon
btn._iconOn = ICONS_PATH .. "sidebar\\unlockmode-ig-on.png"
btn._iconOff = ICONS_PATH .. "sidebar\\unlockmode-ig.png"
local label = MakeFont(btn, 14, nil, TEXT_DIM.r, TEXT_DIM.g, TEXT_DIM.b, TEXT_DIM.a)
label:SetPoint("LEFT", icon, "RIGHT", NAV_TXT_GAP, 0)
label:SetText(EllesmereUI.L("Unlock Mode"))
btn._label = label
-- Always "loaded" appearance
label:SetTextColor(NAV_ENABLED_TEXT.r, NAV_ENABLED_TEXT.g, NAV_ENABLED_TEXT.b, NAV_ENABLED_TEXT.a)
icon:SetDesaturated(false)
icon:SetAlpha(NAV_ENABLED_ICON_A)
local hlTex = SolidTex(btn, "HIGHLIGHT", 1, 1, 1, 0)
hlTex:SetAllPoints()
btn:SetScript("OnEnter", function(self)
hlTex:SetAlpha(0.06)
self._hoverGlow:Show()
self._hoverIndicator:Show()
self._label:SetTextColor(NAV_HOVER_ENABLED_TEXT.r, NAV_HOVER_ENABLED_TEXT.g, NAV_HOVER_ENABLED_TEXT.b, NAV_HOVER_ENABLED_TEXT.a)
end)
btn:SetScript("OnLeave", function(self)
hlTex:SetAlpha(0)
self._hoverGlow:Hide()
self._hoverIndicator:Hide()
self._label:SetTextColor(NAV_ENABLED_TEXT.r, NAV_ENABLED_TEXT.g, NAV_ENABLED_TEXT.b, NAV_ENABLED_TEXT.a)
end)
btn:SetScript("OnClick", function()
if EllesmereUI._openUnlockMode then
EllesmereUI._unlockReturnModule = activeModule
EllesmereUI._unlockReturnPage = activePage
C_Timer.After(0, EllesmereUI._openUnlockMode)
end
end)
EllesmereUI._unlockSidebarBtn = btn
-- One-time tutorial tip: play badge opening the Unlock Mode video guide, retired
-- once clicked. VideoGuides owns all gating (its per-account seen map);
-- nil-guarded for standalone builds.
if EllesmereUI.VideoGuides and EllesmereUI.VideoGuides.AttachTip then
EllesmereUI.VideoGuides.AttachTip(btn, "unlock_mode", {
tooltip = "Video Guide: Unlock Mode",
x = -12,
})
end
end
-------------------------------------------------------------------
-- Global Settings button (always second, not an addon)
-------------------------------------------------------------------
local GLOBAL_KEY = "_EUIGlobal"
do
local btn = CreateFrame("Button", nil, sidebar)
btn:SetSize(SIDEBAR_W, NAV_ROW_H)
btn:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 0, NAV_TOP - NAV_ROW_H)
btn:SetFrameLevel(sidebar:GetFrameLevel() + 1)
DecorateSidebarButton(btn)
-- Glow layer (behind icon): tinted version of the -on texture
local iconGlow = btn:CreateTexture(nil, "ARTWORK", nil, 0)
iconGlow:SetTexture(ICONS_PATH .. "sidebar\\settings-ig-on-2.png")
iconGlow:SetSize(NAV_ICON_W, NAV_ICON_H)
iconGlow:SetPoint("LEFT", btn, "LEFT", NAV_LEFT, 0)
iconGlow:SetDesaturated(true)
iconGlow:SetVertexColor(ELLESMERE_GREEN.r, ELLESMERE_GREEN.g, ELLESMERE_GREEN.b, 1)