-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLibWidgets.lua
More file actions
3505 lines (3218 loc) · 136 KB
/
Copy pathLibWidgets.lua
File metadata and controls
3505 lines (3218 loc) · 136 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
-- LibWidgets -- a small, addon-agnostic UI widget library for 1.12 WoW
-- addons. Currently houses fourteen widgets: NewButton (a flat action button),
-- NewTabButton (a NewButton carrying the lit "selected" look), NewTabStrip (a
-- row of NewTabButtons that measures, wraps and reflows itself),
-- NewIconButton (a small texture-faced button),
-- NewCheckBox (a labelled checkbox), NewColorSwatch (a ColorPickerFrame swatch),
-- NewSlider (a value-carrying OptionsSliderTemplate slider), NewSpinBox (a
-- drag/type/step number control), NewTextBox (a
-- tooltip-backdrop-styled edit box), NewMultiLineEditBox (a scrollable
-- multi-line edit box on this library's own slim slider, sized to its text),
-- NewScrollFrame (a chrome-free content scroller), NewDropButton (a
-- value-picker popup button)
-- NewIconPicker (a searchable icon-browser dialog)
-- and NewListEditor (a bordered FauxScrollFrame-backed row pool with
-- an optional leading tristate/checkbox control, a class/priority-coloured
-- name label, optional trailing per-column widgets, reorder -- arrows + full
-- drag-to-reorder with a ghost row, insertion indicator and cursor-edge
-- auto-scroll -- and an optional add row built from NewButton + NewTextBox).
-- Further widgets are expected to join it under the same library name.
--
-- NewAnchorGrid is a nine-point anchor picker: a bordered 3x3 grid of small
-- buttons whose selected point is highlighted. spec:
-- values -- ordered nine point values, row-major
-- get() -- optional initial value
-- onSelect(v) -- called when a point is picked
-- width/height -- optional outer size (defaults to 100x50)
-- Returns the frame with `.setValue(v)` and `.setSize(width, height)` methods.
--
-- NewTabStrip is a row of tab buttons that lays itself out: each tab is sized
-- to its own label rather than to an equal share, and the row wraps onto as
-- many lines as it needs. spec:
-- tabs -- ordered { { value = <any>, text = <string>, hidden = <bool> }, ... }
-- width -- the width to wrap within (required in practice; default 100)
-- rowHeight -- tab height, default 22
-- gap -- horizontal space between tabs, default 4
-- rowGap -- vertical space between rows, default 4
-- padding -- added to each measured label to get the tab's width, default 16
-- minWidth -- floor for a very short label, default 24
-- fillRatio -- a row at or above this fraction of `width` is stretched to
-- fill it; a sparser one keeps its natural widths and is left-
-- aligned. Default 0.75
-- onSelect(value) -- a tab was clicked
-- onReflow(rows, height) -- the row count or height changed; the consumer
-- re-anchors whatever sits below the strip
-- Returns the frame with `.setTabs(list)`, `.select(value)` (nil deselects
-- every tab), `.getSelected()`, `.getRows()`, `.setWidth(w)` and `.buttons`
-- (index-stable, one per entry in the last `setTabs` list, hidden ones
-- included).
--
-- NewCodeEditBox decorates NewMultiLineEditBox into a syntax-coloured Lua
-- editor: it colours on blur (never while typing, so the caret never lands
-- inside a colour escape), carries its own red error line under the box, and
-- with `spec.default` a two-click-confirm Reset button above it. Its spec:
-- width, height, text, colors, font = {path, size, flags},
-- onChange(code) -- every keystroke, uncommitted
-- onCommit(code) -- focus lost, or a confirmed reset
-- validate(code) -- returns an error string or nil; drives the error line
-- default -- string or function; Reset appears only when set
-- live -- colour per keystroke instead of on blur (see below)
-- tabWidth -- spaces per indent level, or false for hard tabs
-- Tab re-indents the whole buffer. Methods: setText/getText/clearFocus/setSize/
-- indent, plus setValidate/setDefault/setHandlers/setLive/setTabWidth for a
-- consumer that pools and rebinds one instance.
--
-- `live` colours under the caret while typing, which needs the caret saved and
-- restored around every recolour; it is off unless asked for.
--
-- It also carries one non-widget group, at the bottom of this file: a Lua
-- source tokenizer and the syntax-colouring helpers built on it (LuaColorize,
-- LuaEncode/LuaDecode, LuaStripColors, LuaPadWithLinebreaks). They live here
-- because the code edit box that uses them is library code; they are pure
-- string -> string and touch no frame.
--
-- Every caller-specific bit of NewListEditor -- the backing list, how to
-- reorder/remove an entry, how to paint the name/leading control/any
-- trailing columns, and the absolute path to this library's own textures --
-- comes through the `spec` table (documented below), so this file has no
-- knowledge of any particular addon's data model and holds no addon-specific
-- state of its own.
--
-- Registered through LibStub (as "LibWidgets-1.0") so multiple addons
-- vendoring their own copy of this file coexist safely: whichever copy
-- declares the highest MINOR becomes the one shared instance regardless of
-- load order, and every other copy's body no-ops immediately below.
--
-- Vendored as its own Libs\LibWidgets\ folder (own .lua, own textures)
-- rather than a loose file in the addon root. A consuming addon's .toc must
-- list every .lua file this library is made of directly (today just this
-- one) -- there is no single manifest file a consumer can reference once to
-- pull in the whole library, since this client does not process nested
-- <Script>/<Include> directives from a referenced .xml file. manifest.ps1
-- (beside this file) is a packaging-time helper only: it lists this
-- library's shippable files (.lua + textures) so a consumer's own packaging
-- script can include exactly those files without recursively copying this
-- whole folder, which would also capture files that don't belong in a
-- shipped addon (such as version-control metadata now that this folder is a
-- git submodule).
--
-- NewButton(parent, spec) -- a flat, tooltip-backdrop-styled action button (the
-- same look as the list editor's reorder/delete/leading-control buttons). spec:
-- text, width, height (default 22), onClick
-- Returns the button with a `.label` FontString and a `.setText(text)` method for
-- relabeling later (e.g. a button whose face shows a live value).
--
-- NewIconButton(parent, spec) -- the small texture-faced button the list editor's
-- own rows use (reorder/delete), for a caller needing the same control outside a
-- list (e.g. a collapse arrow or a delete affordance on a section header). spec:
-- icon -- texture path (LibWidgets.ICON_DELETE is the list editor's own
-- delete art, published so both read the same)
-- width (default 20), height (default 18), iconSize (default 11)
-- onClick
-- Returns the button with its texture as `.icon`, so a toggle can re-SetTexture it.
--
-- NewCheckBox(parent, spec) -- a standalone labelled checkbox (UICheckButtonTemplate
-- plus a right-hand label). spec:
-- text, width/height (default 22)
-- onClick(checked) -- called on a user toggle with the new boolean state
-- get() -- optional: seeds the initial checked state
-- Returns the CheckButton with a `.label` FontString and a `.setChecked(on)` method
-- that resyncs from external state without firing onClick.
--
-- NewColorSwatch(parent, spec) -- a swatch button that opens the stock
-- ColorPickerFrame (with opacity). spec:
-- get() -> {r,g,b,a} -- current colour (a defaults to 1 if absent)
-- set({r,g,b,a}) -- store a picked colour
-- width/height (default 20), swatchSize (inner fill, default 14)
-- Returns the button with a `.repaint()` method to re-read get() after an
-- external change.
--
-- NewTextBox(parent, spec) -- a single-line edit box with a tooltip-style backdrop
-- (not InputBoxTemplate -- that template's border textures render a black bar at
-- small heights). spec:
-- width (omit to size purely from the caller's own anchors, e.g. a box anchored
-- on both TOPLEFT and RIGHT), height (default 22), text (initial contents)
-- onCommit(text) -- called on Enter (the box then clears focus); Escape clears
-- focus with no commit. Omit for a read-only display box.
-- onChange(text) -- optional: called on every keystroke (live filtering); fires
-- on user edits, not on the initial `text` seed.
-- hint -- optional: greyed placeholder text shown while the box is empty.
--
-- NewMultiLineEditBox(parent, spec) -- a scrollable multi-line edit box (a
-- UIPanelScrollFrameTemplate ScrollFrame wrapping a SetMultiLine EditBox) with
-- the same tooltip-style backdrop, for paste-in/copy-out blobs (import/export).
-- spec:
-- width (default 300), height (default 150), text (initial contents)
-- onChange(text) -- optional: called on every edit
-- Returns the outer frame with methods `.setText(t)`, `.getText()`,
-- `.focusSelectAll()` (focus + highlight everything, so the user can Ctrl-C an
-- export immediately) and `.clearFocus()`, plus the `.edit` EditBox itself.
--
-- NewSlider(parent, spec) -- a horizontal OptionsSliderTemplate slider whose title
-- carries the live value instead of the template's Low/High end labels. spec:
-- name -- unique global frame name (the template needs one to address
-- "<name>Low"/"<name>High"/"<name>Text")
-- min, max, step, width (default 150)
-- onChange(v) -- called on every user drag, and on a committed edit-box entry
-- when `editable` is set
-- format(v) -- optional: -> the full title text (defaults to the number
-- rounded to `decimals` places)
-- decimals -- max decimal places shown in the default title format and the
-- editable box's display (default 2); trailing zeros are
-- trimmed (1 shows as "1", not "1.00"). The same rounding is
-- available standalone as LibWidgets.FormatNumber(v, decimals)
-- for a caller building its own `format`.
-- get() -- optional: seeds the initial value through the same guard
-- `.setValue` uses, so seeding never echoes through onChange
-- editable -- optional: adds a small edit box to the right of the slider
-- bar showing the current value, editable directly (commits on
-- Enter, clamped to min/max, rounded to `decimals`); the bar
-- itself narrows by `inputWidth` + gap to keep the total
-- footprint at `width`
-- inputWidth -- width of that edit box (default 44)
-- Returns the slider with a `.setValue(v)` method: sets the value and repaints the
-- title (and the edit box, if any) without firing onChange, for resyncing the
-- widget from external state.
--
-- NewSpinBox(parent, spec) -- a number control that can be dragged, typed into or
-- stepped: a caption above a filled track, the value centred inside the track as
-- an edit box, and a step button just outside each end of it. Drag for coarse,
-- type for exact, step for fine, in one control the width of the row -- a plain
-- slider can only be dragged, which cannot reach a specific number across a wide
-- range. spec:
-- label -- caption above the track
-- min, max, step, width (default 150)
-- textureDir -- absolute path to this library's textures\ (see the
-- no-self-path note below); the two step buttons are the
-- shared `up` arrow given a quarter turn each
-- onChange(v) -- called on a user drag, a step, and a committed typed value;
-- never on `.setValue` or the `get` seed, and never for an
-- edit that lands on the value already shown
-- fmt(v) -- optional: -> the text shown in the box (defaults to the
-- number rounded to `decimals` places). A format the box
-- cannot parse back makes it read-only in practice, since a
-- typed value that fails tonumber reverts.
-- decimals -- max decimal places in the default box text (default 2)
-- get() -- optional: seeds the initial value through `.setValue`
-- Typing commits on Enter and on focus loss, reverts on Escape, and snaps and
-- clamps to min/max/step -- so the box can never hold a value the slider half of
-- the control could not have produced. Returns the frame with `.setValue(v)`
-- (resync without firing onChange), `.getValue()`, `.setWidth(w)` (the whole
-- control, buttons included -- the track takes what is left), `.edit` (the edit
-- box, for a pooling consumer that must clear focus before rebinding), `.label`
-- and `.stepDown`/`.stepUp`.
--
-- NewScrollFrame(parent, spec) -- a chrome-free vertical content scroller: a plain
-- ScrollFrame (no Blizzard scroll template) with a slim tinted right-edge slider
-- and mouse wheel, for scrolling arbitrary content that can outgrow its frame.
-- spec:
-- wheelStep -- pixels scrolled per wheel notch (default 30)
-- sliderInset -- x-nudge of the slider from the frame's right edge (default 0)
-- The caller anchors the returned ScrollFrame, parents its content into the
-- `.content` scroll child (managing that child's width itself -- reserve a few px
-- on the right for the slider), sets `.content`'s height, then calls `.Update()`
-- so the slider re-fits (again after any later content-height or frame-size
-- change). `.Update(viewH)` takes the viewport height explicitly, which a caller
-- sizing this frame by anchors rather than SetHeight must do -- see the comment
-- on Update. Also exposes `.slider` and `.wheel` (the wheel handler, so a child
-- that captures wheel focus -- e.g. a button -- can forward to it via SetScript).
--
-- NewDropButton(parent, spec) -- a button showing the current value that drops a
-- popup list of options to change it (no cycling). spec:
-- width, height (button size; height defaults to 20)
-- menuWidth (defaults to width), itemHeight (defaults to 14)
-- maxVisibleItems -- popup caps at this many rows (default 8) and scrolls the
-- rest via a slim right-edge slider + mouse wheel; shorter
-- menus size to fit with no slider
-- values -- ordered array of stored values (menu order), or a function
-- returning one: the menu rebuilds on every open (dynamic sets,
-- e.g. profile names)
-- labels -- value -> display label; optional (defaults to the raw value)
-- tips -- value -> tooltip line; optional
-- previews -- value -> true; adds a per-row preview button
-- previewTexture -- texture path for the preview button (optional)
-- onPreview(v) -- called by a row preview without selecting or closing
-- swatches -- optional: value -> texture path. Turns the picker into a
-- preview picker: the button's face and every menu entry draw
-- that texture as a filled green bar behind the label, so a
-- bar-texture choice is judged by how it actually looks rather
-- than by its name. Menu rows grow to `itemHeight` 20 by
-- default so a swatch is legible. (Lookups go through the table
-- on every paint, so a caller recycling one button across
-- fields can hand in a proxy table that forwards to whichever
-- field is currently bound, the same way `labels` does.)
-- onSelect(v) -- called when a menu entry is picked
-- textureDir -- optional: absolute path to this library's textures folder. When
-- given, a down-arrow (grey at rest, green on hover) is drawn on
-- the button's right edge to signal it opens a menu.
-- get() -- optional: when given, the button self-paints from it on build
-- and after each pick via `.setValue`. Omit it for a caller that
-- repaints recycled instances itself each draw (`.setValue(v)`
-- works either way).
-- menuParent, menuStrata -- override the popup's parent/strata. By default the
-- popup is hosted on the button's top-level ancestor frame (the
-- one parented straight to UIParent) at "FULLSCREEN_DIALOG",
-- NOT on the button: parented under the button, a popup dropped
-- from a control inside a ScrollFrame gets clipped where it
-- overflows the scroll region and shares the rows' strata,
-- landing behind the controls below it. It still anchors its
-- position to the button, so it tracks it.
-- The popup is toplevel'd so it orders above sibling same-strata popups on
-- interaction; its high strata already puts it above the host panel. It is
-- deliberately NOT re-levelled on open -- see the open handler for why.
-- The live value is stashed on `.value` for the button's own hover tooltip. At
-- most one NewDropButton popup is ever open at once -- see CloseAllMenus below.
--
-- CloseAllMenus() -- hides whichever NewDropButton popup is currently open, if
-- any, and drops edit focus. Every widget this library builds calls it on
-- interaction (see the comment above its definition for why -- there is no
-- generic focus-lost event to hook instead), so a menu closes and a focused
-- edit box commits the moment anything else in the library is touched. A
-- consuming addon's own panel can call it too (e.g. on OnMouseDown for a
-- blank-area click, or OnHide so a menu left open under a closed panel doesn't
-- pop back up still expanded next time it opens).
--
-- ClearFocus() -- the focus half of CloseAllMenus on its own, for a caller that
-- must leave an open menu alone.
--
-- NewIconPicker(parent, spec) -- a modal icon browser: a live search box over a
-- scrolling grid of every icon the client knows, with a preview of the current
-- pick and Okay/Cancel/Clear. Built once and reused -- `.Open(current)` refills
-- and shows it, `.Close()` hides it. Only columns*visibleRows cell buttons ever
-- exist; they are repainted as the grid scrolls, so a ~5000-icon database costs
-- a fixed number of frames. spec:
-- nameFrame -- REQUIRED: global frame name for the FauxScrollFrame (its
-- scrollbar child is addressed by name); the dialog itself
-- takes "<nameFrame>Dialog"
-- onAccept(path, name) -- the pick, as a full texture path and as the bare
-- uppercase basename; called with ("", nil) for Clear
-- icons() -- optional: the array to browse (paths or basenames), for a
-- caller with its own source. Defaults to the client's macro
-- icon database (LibWidgets.GetIconDatabase)
-- title, searchHint, acceptText, cancelText, clearText -- captions
-- columns (10), visibleRows (7), iconSize (30)
-- dialogParent -- parent frame (default UIParent); pass the owning panel so
-- closing it takes the dialog down too
-- strata (FULLSCREEN_DIALOG), onClose()
--
-- LibWidgets.GetIconDatabase() -- the client's icon list as uppercase basenames
-- (no path, no extension), built once per session. Prefers ClassicAPI's
-- GetMacroIcons/GetMacroItemIcons/GetLoose* enumerators and falls back to
-- vanilla's GetNumMacroIcons/GetMacroIconInfo, which only knows Ability_*/
-- Spell_* and lists no item icons at all.
-- LibWidgets.IconPath(name) -- that basename back to a texture path.
--
-- LibWidgets.BAR_TEXTURES -- the ordered status-bar texture names a bar-texture
-- picker offers ("Flat" and "Blizzard" first, then the bundled set). Pair with
-- LibWidgets.BarTexturePath(dir, name) -> a texture path, where `dir` is the
-- caller's own bars folder ("Interface\AddOns\<addon>\textures\bars\"); "Flat"
-- (a solid fill, no grain) and "Blizzard" resolve to stock client art and ignore
-- `dir`. Feed the list to
-- NewDropButton's `values` and a name->path map built from it to `swatches` for a
-- previewing texture picker. Same reasoning as `textureDir`: this file cannot
-- discover its own path at runtime, so the art location is the caller's to supply.
--
-- NewListEditor(parent, spec) -- spec fields:
-- nameFrame -- unique string naming the internal ScrollFrame (1.12's
-- FauxScrollFrameTemplate needs an addressable global name
-- for its scrollbar child, "<nameFrame>ScrollBar")
-- textureDir -- absolute path to this library's own textures folder
-- (e.g. "Interface\AddOns\<addon>\Libs\LibWidgets\textures\").
-- WoW texture paths are always absolute and this file has
-- no way to discover its own path at runtime, so each
-- caller supplies it like any other spec field.
-- x, y -- TOPLEFT offset from `parent`
-- rightInset -- RIGHT inset from `parent` (default 16)
-- rowHeight, visibleRows -- when visibleRows >= #list() the scrollbar just
-- stays inert, so a "fixed, never scrolls" list (e.g. one
-- row per class) needs no special casing here.
-- list() -> the live ordered array, read fresh each refresh
-- reorder(fromIndex, before) -- before is a boundary in 1..n+1: the entry
-- ends up just before whatever currently sits at original
-- index `before`. Used by both the arrow buttons and
-- drag-drop.
-- remove(index) -- optional; omit to hide the delete button
-- add = { onAdd(text) } -- optional; builds an edit box + Add button
-- below the list (children of the returned `frame`, so
-- hiding it hides them too)
-- leadingControl -- optional:
-- { kind = "tristate", states = { {key=,color={r,g,b},tooltip=}, ... },
-- get(entry) -> key, cycle(entry) }
-- or
-- { kind = "checkbox", get(entry) -> bool, set(entry, bool) }
-- nameGet(entry) -> text
-- nameColor(entry, index) -> r, g, b -- optional
-- columns = { { width, build(row) -> widget, update(widget, entry, index, count) }, ... }
-- -- optional trailing per-row widgets; not used by any
-- current caller, but the hook for future per-row data.
--
-- Returns { height = <total pixel height used below (x,y)>, refresh = fn,
-- frame = <the list's outer frame> }.
local MAJOR, MINOR = "LibWidgets-1.0", 20
-- Bind the global only on the winning copy. NewLibrary returns nil for a copy
-- that loses the version race; assigning that nil straight to the global would
-- wipe out the winner's binding (an older/equal copy loading last nulls it),
-- so keep the return in a local and publish only when we actually won.
-- Registration diagnostics, captured before the call and published below for a
-- consumer's own version report: what was already registered under this major,
-- and how this MINOR survives each of the two pattern functions LibStub
-- implementations use to parse it. Worth keeping because the failure they
-- describe is silent -- a copy that loses the race and doesn't load presents as
-- a library inexplicably missing a function, never as a version problem.
local preMinor = LibStub.minors and LibStub.minors[MAJOR]
local parseMatch = string.match and tonumber(string.match(MINOR, "%d+"))
local _, _, findCapture = string.find(MINOR, "(%d+)")
local lib, displaced = LibStub:NewLibrary(MAJOR, MINOR)
local overridden = false -- LIBWIDGETS_DEV forced this copy in
local repaired = false -- LibStub's verdict contradicted its own bookkeeping
if not lib then
local existing, recordedMinor = LibStub:GetLibrary(MAJOR, true)
if existing then
-- LibStub's verdict is not trustworthy on every client. One in the wild
-- assigns the minor a constant before comparing, discarding what it was
-- handed: every major records that same constant, every registration
-- after the first is refused, and `minors` says nothing about what is
-- actually loaded. Version arbitration therefore can't be delegated to
-- LibStub -- each copy publishes its own version as `.MINOR` on the
-- shared table, and that is the number compared here. A copy predating
-- that field publishes nothing and is treated as older than anything,
-- which is correct: it is.
local liveMinor = existing.MINOR or recordedMinor or 0
if liveMinor < MINOR then
-- Strictly newer than what's loaded: take over. This is the normal
-- upgrade path, not a workaround, and it is safe to ship precisely
-- because it never displaces an equal or newer copy.
lib, displaced, repaired = existing, liveMinor, true
LibStub.minors[MAJOR] = MINOR
elseif LIBWIDGETS_DEV then
-- Dev override: take over even from an equal or newer copy. Needed
-- because addons sharing one submodule normally sit at *equal*
-- MINOR, where nothing above applies and edits to this checkout
-- appear to do nothing -- everything still working, from someone
-- else's copy. Don't ship with it set.
lib, displaced, overridden = existing, liveMinor, true
LibStub.minors[MAJOR] = MINOR
end
end
end
if not lib then return end
LibWidgets = lib
-- Published so a consumer can report which copy actually won (a mismatch is
-- otherwise invisible until a missing function blows up mid-call).
-- DEV_OVERRIDE distinguishes the two ways this copy can end up live: won the
-- version race on its own, or only because LIBWIDGETS_DEV forced it. Without
-- that flag recorded, the two are indistinguishable after the fact -- the
-- override sets the registered minor to this copy's own, so the numbers look
-- identical either way.
LibWidgets.MINOR = MINOR
LibWidgets.DEV_OVERRIDE = overridden
LibWidgets.REPAIRED = repaired
-- The minor that was registered before this copy took over (nil = none).
LibWidgets.DISPLACED_MINOR = displaced
LibWidgets.PRE_MINOR = preMinor
LibWidgets.PARSE_MATCH = parseMatch
LibWidgets.PARSE_FIND = tonumber(findCapture)
local BTN_W = 20
local BTN_GAP = 2
local COL_GAP = 6
local STATE_W = 20
-- Rounds to `decimals` places and trims trailing zeros (and a bare trailing
-- "." if decimals rounded away entirely), so an integer value reads as "1"
-- rather than "1.00" while a fractional one still shows up to `decimals`
-- places -- NewSlider's default title/edit-box formatting.
local function formatNumber(v, decimals)
local s = string.format("%." .. (decimals or 2) .. "f", v)
if string.find(s, "%.") then
s = string.gsub(s, "0+$", "")
s = string.gsub(s, "%.$", "")
end
return s
end
LibWidgets.FormatNumber = formatNumber
local WIDGET_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 9,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
}
local ICON_DELETE = "Interface\\Buttons\\UI-GroupLoot-Pass-Up"
local MOVE_OK = { 0.2, 0.9, 0.2 }
local MOVE_NONE = { 0.5, 0.5, 0.5 }
-- Only one NewDropButton popup is ever open at a time. 1.12 has no generic
-- focus-lost event for a plain Button/Slider/CheckButton (only EditBox has
-- OnEditFocusGained/Lost), so there is no reliable way to detect "some other
-- control just gained focus" from the outside. Instead every interactive
-- widget this library builds calls CloseAllMenus() as the first thing it
-- does on interaction (a click, a drag-start, an edit box gaining focus), so
-- touching *anything* else in the library always closes a still-open menu --
-- this is an explicit, not passive, close rather than a screen-covering
-- click-catcher, so it never costs the "click a different drop button"
-- case an extra click the way a catcher would. The one gap this doesn't
-- cover is a click that lands on nothing interactive at all (bare panel
-- background, or outside the addon's own frames entirely); a consuming
-- addon can close that gap too by wiring its own panel's OnMouseDown to
-- LibWidgets.CloseAllMenus().
--
-- Edit focus rides the same signal, for the same reason. Only an EditBox is
-- told it lost focus, so a box the user clicked away from keeps focus -- and
-- with it whatever it commits on blur -- indefinitely. CloseAllMenus therefore
-- drops it too: "the user touched something else" is one event here, and every
-- call site that wants a menu closed wants a stale caret gone as well.
local activeMenu = nil
local focusedEdit = nil
-- Drops edit focus alone, for a caller that must not disturb an open menu.
-- Clearing focus on a box that no longer has it does nothing, so a stale
-- `focusedEdit` (a consumer replacing an OnEditFocusLost handler, say) costs
-- nothing beyond a wasted call.
function LibWidgets.ClearFocus()
local e = focusedEdit
focusedEdit = nil
if e then e:ClearFocus() end
end
function LibWidgets.CloseAllMenus()
if activeMenu then activeMenu:Hide() end
activeMenu = nil
LibWidgets.ClearFocus()
end
-- What every OnEditFocusGained in this file calls. The recorded box is dropped
-- *before* the menus close, not cleared: the engine has already taken focus off
-- whoever held it, and clearing a recorded box that is the one now gaining
-- focus would fire its own focus-lost handler underneath it -- an inline rename
-- box reopened on a second row closes itself that way.
local function takeFocus(e)
focusedEdit = nil
LibWidgets.CloseAllMenus()
focusedEdit = e
end
-- Flat, tooltip-backdrop-styled button base shared by the reorder/delete/
-- leading-control buttons.
local function styleFlatButton(b)
b:SetBackdrop(WIDGET_BACKDROP)
b:SetBackdropColor(0, 0, 0, 0.7)
b:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
b:SetScript("OnEnter", function() this:SetBackdropBorderColor(0.9, 0.8, 0.2, 1) end)
b:SetScript("OnLeave", function() this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) end)
end
-- Reorder/delete icon button. Overrides styleFlatButton's hover so a disabled
-- button (row 1's "up", the last row's "down") doesn't brighten on hover.
local function iconButton(parent, icon, onClick, width, height, iconSize)
local b = CreateFrame("Button", nil, parent)
b:SetWidth(width or BTN_W); b:SetHeight(height or 18)
styleFlatButton(b)
local t = b:CreateTexture(nil, "ARTWORK")
t:SetWidth(iconSize or 11); t:SetHeight(iconSize or 11)
t:SetPoint("CENTER", 0, 0)
t:SetTexture(icon)
b.icon = t
b:SetScript("OnEnter", function() if this:IsEnabled() == 1 then this:SetBackdropBorderColor(0.9, 0.8, 0.2, 1) end end)
b:SetScript("OnLeave", function() this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) end)
b:SetScript("OnMouseDown", function() this.icon:SetPoint("CENTER", 1, -1) end)
b:SetScript("OnMouseUp", function() this.icon:SetPoint("CENTER", 0, 0) end)
b:SetScript("OnClick", function() LibWidgets.CloseAllMenus(); onClick() end)
return b
end
-- The same small icon button the list editor's own rows use, as a public
-- widget; see the header comment for spec. `.icon` is the texture, so a caller
-- repainting a toggle (an expand/collapse arrow) just re-SetTextures it.
function LibWidgets.NewIconButton(parent, spec)
spec = spec or {}
return iconButton(parent, spec.icon, spec.onClick or function() end,
spec.width, spec.height, spec.iconSize)
end
-- The delete-row art the list editor uses, published so a consumer's own
-- delete affordance outside a list reads as the same control.
LibWidgets.ICON_DELETE = ICON_DELETE
-- Leading tristate chip: a colour-tinted circle swatch that cycles through
-- leadingControl.states on click. iconPath is the caller's spec.textureDir-
-- resolving helper (see LibWidgets.NewListEditor), passed in rather than closed over
-- since this factory is shared across every instance.
local function buildTristate(row, lc, iconPath)
local b = CreateFrame("Button", nil, row)
b:SetWidth(STATE_W); b:SetHeight(18)
styleFlatButton(b)
local sw = b:CreateTexture(nil, "ARTWORK")
sw:SetWidth(12); sw:SetHeight(12)
sw:SetPoint("CENTER", 0, 0)
sw:SetTexture(iconPath("circle"))
b:SetScript("OnClick", function()
LibWidgets.CloseAllMenus()
if row.entry ~= nil then lc.cycle(row.entry) end
end)
b:SetScript("OnEnter", function()
this:SetBackdropBorderColor(0.9, 0.8, 0.2, 1)
if b.tip then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine(b.tip)
GameTooltip:AddLine("Click to change", 0.5, 0.5, 0.5)
GameTooltip:Show()
end
end)
b:SetScript("OnLeave", function() this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8); GameTooltip:Hide() end)
b.paint = function(entry)
local key = lc.get(entry)
for i = 1, table.getn(lc.states) do
local st = lc.states[i]
if st.key == key then
sw:SetVertexColor(st.color[1], st.color[2], st.color[3])
b.tip = st.tooltip
end
end
end
return b
end
-- Leading checkbox: a plain enable/disable toggle.
local function buildCheckbox(row, lc)
local b = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
b:SetWidth(STATE_W); b:SetHeight(18)
b:SetScript("OnClick", function()
LibWidgets.CloseAllMenus()
if row.entry ~= nil then lc.set(row.entry, this:GetChecked() and true or false) end
end)
b.paint = function(entry) b:SetChecked(lc.get(entry) and true or false) end
return b
end
-- A flat action button in the shared style; see the header comment for spec.
function LibWidgets.NewButton(parent, spec)
spec = spec or {}
local b = CreateFrame("Button", nil, parent)
b:SetWidth(spec.width or 80); b:SetHeight(spec.height or 22)
styleFlatButton(b)
local fs = b:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
fs:SetPoint("CENTER", 0, 0)
fs:SetText(spec.text or "")
b.label = fs
function b.setText(text) fs:SetText(text or "") end
b:SetScript("OnMouseDown", function()
LibWidgets.CloseAllMenus()
this.label:SetPoint("CENTER", 1, -1)
end)
b:SetScript("OnMouseUp", function() this.label:SetPoint("CENTER", 0, 0) end)
if spec.onClick then b:SetScript("OnClick", spec.onClick) end
return b
end
-- A NewButton carrying the lit "selected" look a tab needs, plus the `value`
-- identifying it to its strip. The selected tab ignores hover, so the active
-- one stays lit rather than dimming when the pointer crosses it.
function LibWidgets.NewTabButton(parent, spec)
spec = spec or {}
local b = LibWidgets.NewButton(parent, {
text = spec.text, onClick = spec.onClick,
width = spec.width, height = spec.height or 22,
})
b.value = spec.value
function b.setSelected(on)
b.selected = on and true or false
if b.selected then
b:SetBackdropColor(0.22, 0.20, 0.05, 0.95)
b:SetBackdropBorderColor(0.9, 0.8, 0.2, 1)
b.label:SetTextColor(1, 1, 1)
else
b:SetBackdropColor(0, 0, 0, 0.7)
b:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
b.label:SetTextColor(0.7, 0.7, 0.7)
end
end
b:SetScript("OnEnter", function() if not this.selected then this:SetBackdropBorderColor(0.9, 0.8, 0.2, 1) end end)
b:SetScript("OnLeave", function() if not this.selected then this:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) end end)
b.setSelected(false)
return b
end
-- A self-laying-out row of tab buttons; see the header comment for spec.
--
-- The layout is AceGUI's TabGroup:BuildTabs: measure each tab from its own
-- text, wrap greedily, pull a lone last tab up beside its neighbours, then
-- stretch a row to fill. Dividing the width equally instead is the obvious
-- alternative and it does not survive a strip growing past a handful of tabs --
-- every tab shrinks to the narrowest one's needs, and a label with no width set
-- does not clip on this client, it overflows into its neighbours.
function LibWidgets.NewTabStrip(parent, spec)
spec = spec or {}
local frame = CreateFrame("Frame", nil, parent)
local gap = spec.gap or 4
local rowGap = spec.rowGap or 4
local rowHeight = spec.rowHeight or 22
local padding = spec.padding or 16
local minWidth = spec.minWidth or 24
local fillRatio = spec.fillRatio or 0.75
local width = spec.width or 100
frame:SetWidth(width)
frame:SetHeight(rowHeight)
local buttons, tabs = {}, {}
local rows, selected = 0, nil
frame.buttons = buttons
local function layout()
local shown, w = {}, {}
for i = 1, table.getn(tabs) do
if tabs[i].hidden then
buttons[i]:Hide()
else
buttons[i]:Show()
table.insert(shown, buttons[i])
end
end
local n = table.getn(shown)
for i = 1, n do
local sw = (shown[i].label:GetStringWidth() or 0) + padding
if sw < minWidth then sw = minWidth end
w[i] = sw
end
-- Greedy wrap. A row never breaks before its own first tab, so one too
-- wide for the strip gets a row to itself instead of disappearing.
local rowEnd, rowWidth = {}, {}
local used, r = 0, 1
for i = 1, n do
if used ~= 0 and used + gap + w[i] > width then
rowWidth[r] = used
rowEnd[r] = i - 1
r = r + 1
used = w[i]
else
used = used + (used == 0 and 0 or gap) + w[i]
end
end
rowWidth[r] = used
rowEnd[r] = n
local numRows = n > 0 and r or 0
-- A single tab alone on the last row reads as a mistake rather than as a
-- second row, so pull one down to keep it company when the row above can
-- spare it and the last row has room. (Ace's own second guard here is
-- redundant given the first; this is the intent, not a transcription.)
if numRows > 1 and rowEnd[numRows] - rowEnd[numRows - 1] == 1 then
local prevStart = numRows > 2 and rowEnd[numRows - 2] or 0
local prevCount = rowEnd[numRows - 1] - prevStart
local moving = w[rowEnd[numRows - 1]]
if prevCount > 2 and rowWidth[numRows] + gap + moving <= width then
rowEnd[numRows - 1] = rowEnd[numRows - 1] - 1
rowWidth[numRows] = rowWidth[numRows] + gap + moving
rowWidth[numRows - 1] = rowWidth[numRows - 1] - gap - moving
end
end
local first = 1
for row = 1, numRows do
local last = rowEnd[row]
local count = last - first + 1
-- Stretching a nearly-full row to the edge tidies it; stretching a
-- sparse one balloons two tabs across the whole strip, which is worse
-- than a ragged right edge. Ace applies this test only to a lone row;
-- per row is the same rule read one level down, and it is what keeps a
-- short second row looking like tabs.
-- Never negative: a row can hold one tab wider than the whole strip
-- (it cannot break before its first), and sharing that overrun out as
-- a squeeze would push every label back outside its own button --
-- the failure measuring the labels exists to avoid. Let it overhang.
local extra = 0
if rowWidth[row] >= width * fillRatio and width > rowWidth[row] then
extra = math.floor((width - rowWidth[row]) / count)
end
local x = 0
for i = first, last do
local b = shown[i]
b:SetWidth(w[i] + extra)
b:SetHeight(rowHeight)
b:ClearAllPoints()
b:SetPoint("TOPLEFT", frame, "TOPLEFT", x, -(row - 1) * (rowHeight + rowGap))
x = x + w[i] + extra + gap
end
first = last + 1
end
rows = numRows
local h = numRows > 0 and (numRows * rowHeight + (numRows - 1) * rowGap) or 0
frame:SetHeight(h > 0 and h or 1)
if spec.onReflow then spec.onReflow(numRows, h) end
end
-- Buttons are pooled by index and rebound rather than rebuilt: frames cannot
-- be destroyed on this client, so a strip rebuilt per selection change would
-- leak one button per tab every time.
function frame.setTabs(list)
tabs = list or {}
for i = 1, table.getn(tabs) do
local b = buttons[i]
if not b then
b = LibWidgets.NewTabButton(frame, {
height = rowHeight,
onClick = function()
if spec.onSelect then spec.onSelect(b.value) end
end,
})
buttons[i] = b
end
b.setText(tabs[i].text or "")
b.value = tabs[i].value
end
for i = table.getn(tabs) + 1, table.getn(buttons) do
buttons[i]:Hide()
end
layout()
frame.select(selected)
end
-- nil deselects every tab, which is a real state: a panel showing something
-- that belongs to no tab has no tab to light.
function frame.select(value)
selected = value
for i = 1, table.getn(buttons) do
buttons[i].setSelected(value ~= nil and buttons[i].value == value)
end
end
function frame.getSelected() return selected end
function frame.getRows() return rows end
function frame.setWidth(w)
width = w
frame:SetWidth(w)
layout()
end
if spec.tabs then frame.setTabs(spec.tabs) end
return frame
end
-- A standalone labelled checkbox; see the header comment for spec.
function LibWidgets.NewCheckBox(parent, spec)
spec = spec or {}
local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
cb:SetWidth(spec.width or 22); cb:SetHeight(spec.height or 22)
local fs = cb:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
fs:SetPoint("LEFT", cb, "RIGHT", 2, 0)
fs:SetText(spec.text or "")
cb.label = fs
cb:SetScript("OnClick", function()
LibWidgets.CloseAllMenus()
if spec.onClick then spec.onClick(this:GetChecked() and true or false) end
end)
-- Resync from external state without echoing back through onClick (OnClick
-- fires only on a user click, not SetChecked).
function cb.setChecked(on) cb:SetChecked(on and true or false) end
if spec.get then cb:SetChecked(spec.get() and true or false) end
return cb
end
-- A compact nine-point anchor picker. The outer frame owns the border and the
-- buttons are created once; repainting only changes their points, colour and
-- selected state, so a consumer can safely pool the whole widget.
function LibWidgets.NewAnchorGrid(parent, spec)
spec = spec or {}
local frame = CreateFrame("Frame", nil, parent)
frame:SetWidth(spec.width or 100)
frame:SetHeight(spec.height or 50)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = true, tileEdge = true, edgeSize = 1,
})
frame:SetBackdropColor(0.2, 0.2, 0.2, 0.5)
frame:SetBackdropBorderColor(1, 1, 1, 0.6)
local buttons = {}
local values = spec.values or {}
local bindValue = spec.get
local bindSelect = spec.onSelect
for i = 1, 9 do
local index = i
local b = CreateFrame("Button", nil, frame)
b:SetWidth(10); b:SetHeight(10)
local t = b:CreateTexture(nil, "ARTWORK")
t:SetAllPoints(b)
b.texture = t
b:SetScript("OnClick", function()
LibWidgets.CloseAllMenus()
local value = values[index]
if frame.setValue then frame.setValue(value) end
if bindSelect then bindSelect(value) end
end)
buttons[i] = b
end
local function layout()
local width, height = frame:GetWidth(), frame:GetHeight()
for i = 1, 9 do
local b = buttons[i]
b:ClearAllPoints()
b:SetPoint("CENTER", frame, values[i])
end
end
local function paint(value)
for i = 1, 9 do
local selected = values[i] == value
if selected then
buttons[i].texture:SetTexture(0.95, 0.75, 0.15, 1)
else
buttons[i].texture:SetTexture(0.5, 0.5, 0.5, 0.9)
end
end
frame.value = value
end
function frame.setValue(value) paint(value) end
function frame.setBindings(newValues, get, onSelect)
values = newValues or values
bindValue, bindSelect = get, onSelect
end
function frame.setSize(width, height)
frame:SetWidth(width); frame:SetHeight(height); layout()
end
frame.buttons = buttons
layout()
paint(bindValue and bindValue())
return frame
end
-- A colour swatch opening the stock ColorPickerFrame; see the header comment
-- for spec. OpacitySliderFrame reports 1-alpha, hence the inversions.
function LibWidgets.NewColorSwatch(parent, spec)
spec = spec or {}
local get, set = spec.get, spec.set
local sz = spec.swatchSize or 14
local b = CreateFrame("Button", nil, parent)
b:SetWidth(spec.width or 20); b:SetHeight(spec.height or 20)
b:SetBackdrop(WIDGET_BACKDROP)
b:SetBackdropColor(0, 0, 0, 1)
b:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
local tex = b:CreateTexture(nil, "OVERLAY")
tex:SetPoint("CENTER", 0, 0); tex:SetWidth(sz); tex:SetHeight(sz)
local function paint()
local c = get() or { 1, 1, 1, 1 }
tex:SetTexture(c[1] or 1, c[2] or 1, c[3] or 1, c[4] or 1)
end
paint()
b.repaint = paint
b:SetScript("OnClick", function()
LibWidgets.CloseAllMenus()
local c = get() or { 1, 1, 1, 1 }
local cr, cg, cbl, ca = c[1] or 1, c[2] or 1, c[3] or 1, c[4] or 1
ColorPickerFrame.func = function()
local r, g, bl = ColorPickerFrame:GetColorRGB()
local a = OpacitySliderFrame and (1 - OpacitySliderFrame:GetValue()) or 1
set({ r, g, bl, a }); paint()
end
ColorPickerFrame.opacityFunc = ColorPickerFrame.func
ColorPickerFrame.cancelFunc = function() set({ cr, cg, cbl, ca }); paint() end
ColorPickerFrame.opacity = 1 - ca
ColorPickerFrame.hasOpacity = 1
ColorPickerFrame:SetColorRGB(cr, cg, cbl)
ColorPickerFrame:SetFrameStrata("DIALOG")
ShowUIPanel(ColorPickerFrame)
end)
return b
end
-- A tooltip-backdrop-styled edit box; see the header comment for spec.
function LibWidgets.NewTextBox(parent, spec)
spec = spec or {}
local e = CreateFrame("EditBox", nil, parent)
if spec.width then e:SetWidth(spec.width) end
e:SetHeight(spec.height or 22)
e:SetAutoFocus(false)
e:SetFontObject(GameFontHighlightSmall)
e:SetTextInsets(5, 5, 2, 2)
e:SetBackdrop(WIDGET_BACKDROP)
e:SetBackdropColor(0, 0, 0, 0.7)
e:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
-- Greyed placeholder shown only while the box is empty (1.12 has no native
-- placeholder/SearchBoxTemplate to borrow one from).
local hint
if spec.hint then
hint = e:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
hint:SetPoint("LEFT", 5, 0)
hint:SetText(spec.hint)
end
local function updateHint()
if hint then
if e:GetText() == "" then hint:Show() else hint:Hide() end
end
end
-- Seed before wiring OnTextChanged so the initial value doesn't echo through
-- spec.onChange (matches NewSlider's seed-doesn't-fire-onChange contract).
if spec.text then e:SetText(spec.text) end
updateHint()
if spec.onChange or hint then
e:SetScript("OnTextChanged", function()
updateHint()
if spec.onChange then spec.onChange(this:GetText()) end
end)
end
e:SetScript("OnEditFocusGained", function() takeFocus(this) end)
e:SetScript("OnEnterPressed", function()
if spec.onCommit then spec.onCommit(this:GetText()) end
this:ClearFocus()
end)
e:SetScript("OnEscapePressed", function() this:ClearFocus() end)
return e
end
-- A scrollable multi-line edit box; see the header comment for spec.
--
-- Scrolled by this library's own NewScrollFrame (the slim tinted slider every
-- other scroller here uses) rather than UIPanelScrollFrameTemplate's chunky
-- Blizzard scrollbar, so it reads as part of the same widget set.
--
-- That swap brings a real fix with it. A multi-line EditBox does not size
-- itself to its text on this client, which is why the old version pinned the
-- child at a flat 2000px and let the template clip it -- leaving the scroll
-- range permanently wrong. The content height is instead *measured*, with a
-- zero-alpha FontString carrying the same font and wrap width as the edit box,
-- so the slider's range and thumb match the text that is actually there.
local SCROLL_PAD = 5 -- inset from the box's border to the scroll viewport
local SLIDER_GUTTER = 10 -- room kept clear on the right for the slider
function LibWidgets.NewMultiLineEditBox(parent, spec)
spec = spec or {}
local w = spec.width or 300
local h = spec.height or 150
local box = CreateFrame("Frame", nil, parent)
box:SetWidth(w); box:SetHeight(h)
box:SetBackdrop(WIDGET_BACKDROP)
box:SetBackdropColor(0, 0, 0, 0.7)
box:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
local edit, measure
local scroll = LibWidgets.NewScrollFrame(box, {
wheelStep = 20,
child = function(sf)