/
uielement.lua
3133 lines (2881 loc) · 106 KB
/
uielement.lua
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
-- Toribash UI manager
-- Created by sir @ Nabi Studios
local w, h = get_window_size()
---Window width that UIElement class currently operates with
_G.WIN_W = w
---Window height that UIElement class currently operates with
_G.WIN_H = h
---Current screen ratio
_G.SCREEN_RATIO = WIN_W / WIN_H
local safe_x, safe_y, safe_w, safe_h = get_window_safe_size()
---Safe screen area X offset
_G.SAFE_X = math.max(safe_x, _G.WIN_W - safe_w - safe_x)
---Safe screen area Y offset
_G.SAFE_Y = math.max(safe_y, _G.WIN_H - safe_h - safe_y)
add_hook("resolution_changed", "uiResolutionUpdater", function()
WIN_W, WIN_H = get_window_size()
SCREEN_RATIO = WIN_W / WIN_H
local safe_x, safe_y, safe_w, safe_h = get_window_safe_size()
SAFE_X = math.max(safe_x, WIN_W - safe_w - safe_x)
SAFE_Y = math.max(safe_y, WIN_H - safe_h - safe_y)
end)
---Current cursor X coordinate
_G.MOUSE_X = 0
---Current cursor Y coordinate
_G.MOUSE_Y = 0
---@alias FontId
---| 0 # FONTS.BIG | Badaboom big
---| 1 # FONTS.SMALL | Arial small (chat default)
---| 2 # FONTS.MEDIUM | Badaboom medium
---| 3 # Bedrock medium
---| 4 # Arial Bold medium
---| 5 # Kanji supported small
---| 6 # Kanji supported medium
---| 7 # SimHei small
---| 8 # SimHei medium
---| 9 # FONTS.BIGGER | Badaboom giant
---| 10 # Badaboom big @2
---| 11 # Arial small @2
---| 12 # Badaboom medium @2
---| 13 # Bedrock medium @2
---| 14 # Arial Bold medium @2
---| 15 # Kanji supported small @2
---| 16 # Kanji supported medium @2
---| 17 # SimHei small @2
---| 18 # SimHei medium @2
---| 19 # Badaboom giant @2
---@alias UIElementShape
---| 1 # SQUARE
---| 2 # ROUNDED
_G.SQUARE = 1
_G.ROUNDED = 2
---@alias UIElementTextAlign
---| 0 # LEFT | Top Left
---| 1 # CENTER | Top Center
---| 2 # RIGHT | Top Right
---| 3 # LEFTBOT | Bottom Left
---| 4 # CENTERBOT | Bottom Center
---| 5 # RIGHTBOT | Bottom Right
---| 6 # LEFTMID | Middle Left
---| 7 # CENTERMID | Middle Center
---| 8 # RIGHTMID | Middle Right
_G.LEFT = 0
_G.CENTER = 1
_G.RIGHT = 2
_G.LEFTBOT = 3
_G.CENTERBOT = 4
_G.RIGHTBOT = 5
_G.LEFTMID = 6
_G.CENTERMID = 7
_G.RIGHTMID = 8
---@alias UIElementBtnState
---| 0 # BTN_NONE | Default UIElement button state
---| 1 # BTN_HVR | Hover state
---| 2 # BTN_FOCUS | Focused state - only used with keyboard controls
---| 3 # BTN_DN | Pressed state
_G.BTN_NONE = 0
_G.BTN_HVR = 1
_G.BTN_FOCUS = 2
_G.BTN_DN = 3
---@alias UIElementScrollMode
---| 1 # SCROLL_VERTICAL
---| 2 # SCROLL_HORIZONTAL
_G.SCROLL_VERTICAL = 1
_G.SCROLL_HORIZONTAL = 2
---@alias SortOrder
---| true # SORT_DESCENDING
---| false # SORT_ASCENDING
_G.SORT_DESCENDING = true
_G.SORT_ASCENDING = false
---@alias Color number[]
_G.UICOLORWHITE = { 1, 1, 1, 1 }
_G.UICOLORBLACK = { 0, 0, 0, 1 }
_G.UICOLORRED = { 1, 0, 0, 1 }
_G.UICOLORGREEN = { 0, 1, 0, 1 }
_G.UICOLORBLUE = { 0, 0, 1, 1 }
_G.UICOLORTORI = { 0.58, 0, 0, 1 }
_G.DEFTEXTCOLOR = _G.DEFTEXTCOLOR or { 1, 1, 1, 1 }
_G.DEFSHADOWCOLOR = _G.DEFSHADOWCOLOR or { 0, 0, 0, 0.6 }
---@class Vector2Base
---@field x number
---@field y number
---@class Vector3Base : Vector2Base
---@field z number
---@class Rect : Vector2Base
---@field w number
---@field h number
---@type UIElement[]
_G.UIElementManager = _G.UIElementManager or {}
---@type UIElement[][]
_G.UIVisualManager = _G.UIVisualManager or {}
---@type UIElement[]
_G.UIViewportManager = _G.UIViewportManager or {}
---@type UIElement[]
_G.UIMouseHandler = _G.UIMouseHandler or {}
---@type UIElement[]
_G.UIKeyboardHandler = _G.UIKeyboardHandler or {}
---@type UIElement[]
_G.UIScrollbarHandler = _G.UIScrollbarHandler or {}
---@type integer[]
_G.UIElementTextureCache = _G.UIElementTextureCache or {}
_G.UIElementTextureIndex = _G.UIElementTextureIndex or 0
-- Default texture that will be used for fallback by `UIElement:updateTexture()`
_G.UIElementDefaultTexture = "../textures/menu/logos/toribash.tga"
if (not UIElement) then
---@class UIElementSize
---@field w number
---@field h number
---Options to use to spawn the new UIElement object.\
---*Majority of these are the same as UIElement class fields.*
---@class UIElementOptions
---@field globalid integer
---@field parent UIElement Specifying a parent will set its globalid and some other settings automatically
---@field pos number[] Object's target position (relative to parent, if applicable). Negative values imply offset from the opposite direction.
---@field size number[]
---@field shift number[] Object's padding (horizontal and vertical). *Only used when spawning an object with UIElement:addChild()*.
---@field interactive boolean
---@field bgColor Color
---@field hoverColor Color
---@field pressedColor Color
---@field inactiveColor Color
---@field uiColor Color
---@field uiShadowColor Color
---@field viewport boolean
---@field bgImage string|string[] Image path for the object. Alternatively, can be an array with two elements for main texture and fallback option in case main texture file is missing.
---@field disableUnload boolean
---@field imagePatterned boolean
---@field imageAtlas boolean
---@field imageColor Color
---@field atlas Rect
---@field imageHoverColor Color
---@field imagePressedColor Color
---@field imageInactiveColor Color
---@field textfield boolean Whether the object will be used as a text field
---@field textfieldstr string|string[]
---@field textfieldsingleline boolean
---@field textfieldkeepfocusonhide boolean
---@field inputType KeyboardInputType On-screen keyboard input type
---@field autoCompletion boolean On-screen keyboard autocompletion
---@field returnKeyType KeyboardReturnType On-screen keyboard return key type
---@field isNumeric boolean Whether the textfield object should only accept numeric values
---@field allowNegative boolean Whether the numeric only textfield should accept negative values
---@field allowDecimal boolean Whether the numeric only textfield should accept decimal values
---@field customRegex string Custom regex string to match textfield input against
---@field toggle boolean Whether the object will be used as a toggle
---@field innerShadow number|number[]
---@field shadowColor Color|Color[]
---@field shadowOffset number
---@field shapeType UIElementShape
---@field rounded number|number[] Rounding size to use for an object with `ROUNDED` shapeType
---@field scrollEnabled boolean
---@field keyboard boolean True if we want to spawn the object with default keyboard handlers
---@field permanentListener boolean
---@field hoverSound number
---@field upSound number
---@field downSound number
---@field clickThrough boolean
---@field hoverThrough boolean
---@field bgGradient Color[] List of two colors to generate gradient with
---@field bgGradientMode PlayerBody Toribash bodypart id to base gradient UV on
-- **Toribash GUI elements manager class**
--
-- **Version 5.65:**
-- * Added `UIElement.onShow` callback function to execute at the end of `UIElement.show()` call
-- * `UIElement.killAction` and `UIElement.onShow` are now wrapped into pcall() to ensure default behavior proceeds uninterrupted in case of error
-- * Marked `UIElement.killAction` and `UIElement.onShow` as nullable
--
-- **Version 5.62:**
-- * `UIElement.bgImageDefault` boolean value to tell which texture was loaded during `UIElement.updateImage()` call
--
-- **Version 5.61:**
-- * On-screen keyboard customization support for text fields
--
-- **Version 5.60:**
-- * Rewritten all keyboard handlers to make better use of SDL text input events
-- * `UIElement.keyboardHooks()` to initialize generic text field handlers on start
-- * `UIElement.mouseHooks()` is now an abstract class function
-- * `print` and `print_r` functions for easier debug
-- * Gradient generation support for generated UIElements
--
-- **Version 1.6:**
-- * `hoverThrough` support
-- * `UIElement.clock` value to store last graphics update time tick
-- * Use UITween class for framerate independent animations
--
-- **Version 1.5:**
-- * `imageHoverColor` and `imagePressedColor` support
-- * `UIElement:qsort()`, `UIElement:runCmd()` marked as deprecated
-- * New `table.qsort()`, `table.reverse()`, `table.clone()`, `table.compare()`, `table.empty()`, `table.unpack_all()` functions to replace legacy names
-- * New `string.escape()` to replace legacy strEsc() function
-- * Guid() is now `generate_uid()` to prevent confusion with a potential class name
-- * debugEcho() is now `print_r(mixed data, boolean returnString)`
--
-- **Version 1.4:**
-- * `UIElement:mouseHooks()` is now initialized when this script is loaded to ensure it isn't required in every script that requires UIElements
-- * Moved scrollable list update on mouse bar scroll from mouse_move hook to pre_draw for better performance
-- * Different top/bottom rounding support and `roundedInternal` UIElement field
-- * Added EmmyLua annotations for some methods
---@class UIElement
---@field lightUIMode boolean Disables animations and some unimportant effects to improve GUI performance on lower end machines, this is based on `uilight` option
---@field globalid integer Global ID to use for UIElement internal update / display loops
---@field parent UIElement Parent element
---@field child UIElement[] Table containing the list of all children of an object
---@field pos Vector2Base Object's **absolute** position
---@field shift? Vector2Base Object position **relative to its parent**
---@field size UIElementSize Object size
---@field uiColor Color Default text color to be used for uiText() calls
---@field uiShadowColor Color Default text shadow color to be used for uiText() calls
---@field viewport boolean True for UIElement objects that act as a 3D viewport holder
---@field bgColor Color Object's background color
---@field hoverColor Color Object's background color when in hover state. *Only used when object is interactive*.
---@field pressedColor Color Object's background color when in pressed state. *Only used when object is interactive*.
---@field inactiveColor Color Object's background color when in disabled state. *Only used when object is interactive*.
---@field animateColor Color Object's current background color when in normal or hover state. *Only used when object is interactive and UI animations are enabled*.
---@field interactive boolean Whether the object is interactive
---@field bgImage integer Object's image ID obtained from load_texture() call
---@field bgImageDefault boolean Whether the loaded texture is a default fallback
---@field disableUnload boolean True if object's image should not get unloaded when object is destroyed. **Only use this if you know what you're doing**.
---@field drawMode integer Draw mode for normal (quad) objects
---@field atlas Rect Atlas settings for patterned and atlas objects
---@field imageColor Color Color modifier that should be applied to object's image. Default is `{ 1, 1, 1, 1 }`.
---@field imageHoverColor Color Target image color modifier when UIElement is in hover state. *Only used when object is interactive*.
---@field imagePressedColor Color Image color modifier when UIElement is in pressed state. *Only used when object is interactive*.
---@field imageAnimateColor Color Current image color modifier when in normal or hover state. *Only used when object is interactive and UI animations are enabled*.
---@field imageInactiveColor Color Image color modifier when UIElement is in inactive state. *Only used when object is interactive*.
---@field keyboard boolean True for objects that currently handle keyboard events
---@field textfield boolean Internal value to modify behavior for elements that are going to be used as text fields
---@field textfieldstr string[] Text field data. Stored as a table to be able to access data by its reference. **Access UIElement.textfieldstr[1] for the actual string data of a text field**. *Only used for textfield objects*.
---@field textfieldindex number Current input index (cursor position) for the text field. *Only used for textfield objects*.
---@field textfieldsingleline boolean Whether the text field should accept multiline input. *Only used for textfield objects*.
---@field textfieldkeepfocusonhide boolean Whether text field should keep or lose focus when hide() is called on it. Default is `false`.
---@field inputType KeyboardInputType On-screen keyboard input type
---@field autoCompletion boolean On-screen keyboard autocompletion
---@field returnKeyType KeyboardReturnType On-screen keyboard return key type
---@field toggle boolean Internal value to modify behavior for elements that are going to be used as toggles
---@field innerShadow number[] Table containing top and bottom inner shadow size
---@field shadowColor Color[] Table containing top and bottom inner shadow colors
---@field shadowOffset number Custom text shadow offset value to use for uiText() rendering
---@field shapeType UIElementShape Object's shape type. Can be either `SQUARE` (1) or `ROUNDED` (2).
---@field roundedInternal number[] Values that the object will use for rounding edges (top and bottom)
---@field rounded number Max value out of UIElement.roundedInternal values
---@field isactive boolean Internal value to tell if an interactive object is currently active
---@field scrollEnabled boolean If true, an interactive object will also handle mouse scroll events in its `UIElement.btnDown()` callback
---@field hoverState UIElementBtnState Current mouse hover state of an object
---@field pressedPos Vector2Base Internal table containing relative cursor position at the moment of `UIElement.btnDown()` call on an active scroll bar
---@field permanentListener boolean True if we want an object with keyboard handlers to react to all keyboard events, even when not in focus. Permanent keyboard listeners will also not exit keyboard loop early.
---@field hoverSound integer Sound ID to play when object enters `BTN_HVR` mouse hover state
---@field hoverClock number Time for the BTN_HVR state enter
---@field upSound integer Sound ID to play when object exits `BTN_DN` mouse hover state
---@field downSound integer Sound ID to play when object enters `BTN_DN` mouse hover state
---@field clickThrough boolean If true, successful click on an object will not exit mouse loop early
---@field hoverThrough boolean If true, hovering over an object will not exit mouse loop early
---@field displayed boolean Read-only value to tell if the object is currently being displayed
---@field destroyed boolean Read-only value to indicate the object has been destroyed. Use this to check whether the UIElement still exists when a UIElement:kill() function may have been called on its reference elsewhere.
---@field killAction function? Additional callback to be executed when object is being destroyed
---@field onShow function? Additional callback to executed at the end of object's `UIElement.show()` call
---@field scrollBar UIElement Reference to scrollable list holder's scroll bar
---@field __positionDirty boolean Read-only value to tell the UIElement internal loops to refresh element position
---@field scrollableListTouchScrollActive boolean Read-only value used for scrollable list view elements on touch devices
---@field prevInput UIElement Previous input element, set with `UIElement.addTabSwitchPrev()`
---@field nextInput UIElement Next input element, set with `UIElement.addTabSwitch()`
UIElement = {
ver = 5.65,
animationDuration = 0.1,
longPressDuration = 0.25,
lightUIMode = get_option("uilight") == 1
}
UIElement.__index = UIElement
-- Whether UIElement.mouseHooks() has already been called to spawn mouse hooks
---@type boolean
UIElement.__mouseHooks = nil
---Whether UIElement.keyboardHooks() has been called to initialize keyboard hooks
---@type boolean
UIElement.__keyboardHooks = nil
---Whether UIElement.drawHooks() has been called to initialize helper drawing hook
---@type boolean
UIElement.__drawHooks = nil
---Last rendering cycle timestamp
---@type number
UIElement.clock = os.clock_real()
---@class Vector2 : Vector2Base
Vector2 = {}
Vector2.__index = Vector2
end
---Initializes a **Vector2** object
---@param x number?
---@param y number?
---@return Vector2
function Vector2.New(x, y)
local vector = { x = x or 0, y = y or 0 }
setmetatable(vector, Vector2)
return vector
end
---Returns vector magnitude
---@return number
function Vector2:magnitude()
return math.sqrt(self.x * self.x + self.y * self.y)
end
---Returns a normalized version of a vector
---@return Vector2
function Vector2:normalize()
local mag = self:magnitude()
if (mag == 0) then
return Vector2.New(self.x, self.y)
end
return Vector2.New(self.x / mag, self.y / mag)
end
---Returns a vector with the clamped magnitude
---@param max number
---@return Vector2
function Vector2:clampMagnitude(max)
if (max <= 0) then return Vector2.New() end
local mag = self:magnitude()
if (max <= mag) then
return Vector2.New(self.x, self.y)
end
local f = math.min(mag, max) / mag
return Vector2.New(self.x * f, self.y * f)
end
---Returns a vector that represents current vector multiplied by a given value
---@param n number
function Vector2:multiply(n)
return Vector2.New(self.x * n, self.y * n)
end
---Returns a vector produced by adding given vector to current one
---@param other Vector2|Vector2Base
---@return Vector2
function Vector2:add(other)
return Vector2.New(self.x + other.x, self.y + other.y)
end
---Callback function triggered on text input event while UIElement is active and focused
---@param input string
UIElement.textInput = function(input) end
---Custom callback function triggered on text input event while UIElement is active and focused
UIElement.textInputCustom = function() end
-- Callback function triggered on any keyboard key down event while UIElement is active
---@param key ?number Pressed key's keycode
---@return number|nil
UIElement.keyDown = function(key) end
-- Callback function triggered on any keyboard key up event while UIElement is active
---@param key ?number Pressed key's keycode
---@return number|nil
UIElement.keyUp = function(key) end
-- Custom callback function triggered on any keyboard key down event while UIElement is active
---@param key ?number Pressed key's keycode
---@return number|nil
UIElement.keyDownCustom = function(key) end
-- Custom callback function triggered on any keyboard key up event while UIElement is active
---@param key ?number Pressed key's keycode
---@return number|nil
UIElement.keyUpCustom = function(key) end
-- Callback function triggered on mouse button down event when cursor is within object transform
---@param buttonId ?number Mouse button ID associated with the event
---@param x ?number Mouse cursor X position associated with the event
---@param y ?number Mouse cursor Y position associated with the event
UIElement.btnDown = function(buttonId, x, y) end
-- Callback function triggered on mouse button up event when cursor is within object transform
---@param buttonId ?number Mouse button ID associated with the event
---@param x ?number Mouse cursor X position associated with the event
---@param y ?number Mouse cursor Y position associated with the event
UIElement.btnUp = function(buttonId, x, y) end
-- Callback function triggered on mouse move event when cursor is within object transform
---@param x ?number Mouse cursor X position associated with the event
---@param y ?number Mouse cursor Y position associated with the event
UIElement.btnHover = function(x, y) end
-- Callback function triggered on right mouse button up event when cursor is within object transform\
-- We use a separate event because normally right mouse clicks do not produce events so behavior won't be the same
---@param buttonId ?number Mouse button ID associated with the event
---@param x ?number Mouse cursor X position associated with the event
---@param y ?number Mouse cursor Y position associated with the event
UIElement.btnRightUp = function(buttonId, x, y) end
-- Callback function triggered on mouse button up event when object received button down event but
-- cursor has since moved outside object transform
---@param buttonId ?number Mouse button ID associated with the event
---@param x ?number Mouse cursor X position associated with the event
---@param y ?number Mouse cursor Y position associated with the event
UIElement.btnUpOutside = function(buttonId, x, y) end
-- Spawn a new UI Element
---@param _self UIElement
---@param o UIElementOptions Options to use for spawning the new object
---@return UIElement
---@overload fun(o: UIElementOptions): UIElement
function UIElement.new(_self, o)
if (o == nil) then
if (_self ~= nil) then
---@diagnostic disable-next-line: cast-local-type
o = _self
else
error("Invalid argument #1 provided to UIElement.new(o: UIElementOptions)")
end
end
---@type UIElement
local elem = { globalid = 1000,
child = {},
pos = {},
shift = {},
bgColor = { 1, 1, 1, 0 },
innerShadow = { 0, 0 },
__positionDirty = true
}
setmetatable(elem, UIElement)
if (o.parent) then
elem.globalid = o.parent.globalid
elem.parent = o.parent
elem.uiColor = o.parent.uiColor
elem.uiShadowColor = o.parent.uiShadowColor
table.insert(elem.parent.child, elem)
if (o.parent.viewport) then
elem.pos.x = o.pos[1]
elem.pos.y = o.pos[2]
elem.pos.z = o.pos[3]
else
elem.shift.x = o.pos[1]
elem.shift.y = o.pos[2]
elem.size = { w = o.size[1], h = o.size[2] }
end
else
elem.pos.x = o.pos[1]
elem.pos.y = o.pos[2]
elem.size = { w = o.size[1], h = o.size[2] }
end
if (o.globalid) then
elem.globalid = o.globalid
end
if (o.uiColor) then
elem.uiColor = o.uiColor
end
if (o.uiShadowColor) then
elem.uiShadowColor = o.uiShadowColor
end
if (o.shadowOffset) then
elem.shadowOffset = o.shadowOffset
end
if (o.viewport) then
elem.viewport = o.viewport
end
if (o.bgGradient) then
elem:updateImageGradient(o.bgGradient[1], o.bgGradient[2], o.bgGradientMode)
elseif (o.bgColor) then
elem.bgColor = o.bgColor
end
if (o.bgImage or elem.bgImage) then
elem.disableUnload = o.disableUnload
elem.drawMode = o.imagePatterned and 1 or 0
elem.drawMode = o.imageAtlas and 2 or elem.drawMode
elem.imageColor = o.imageColor or { 1, 1, 1, 1 }
elem.atlas = o.atlas or {}
elem.atlas.x = elem.atlas.x or 0
elem.atlas.y = elem.atlas.y or 0
elem.atlas.w = elem.atlas.w or elem.size.w
elem.atlas.h = elem.atlas.h or elem.size.h
if (elem.bgImage == nil) then
if (type(o.bgImage) == "table") then
elem:updateImage(o.bgImage[1], o.bgImage[2])
else
---@diagnostic disable-next-line: param-type-mismatch
elem:updateImage(o.bgImage)
end
end
end
-- Textfield value is a table to allow proper initiation / use after obj is created
if (o.textfield) then
elem.textfield = o.textfield
---@diagnostic disable-next-line: assign-type-mismatch
elem.textfieldstr = o.textfieldstr and (type(o.textfieldstr) == "table" and o.textfieldstr or { o.textfieldstr .. '' }) or { "" }
elem.inputType = o.inputType or KEYBOARD_INPUT.ASCII
elem.autoCompletion = o.autoCompletion == nil and true or o.autoCompletion
elem.returnKeyType = o.returnKeyType or KEYBOARD_RETURN.DEFAULT
elem.textfieldindex = utf8.len(elem.textfieldstr[1]) or 0
elem.textfieldsingleline = o.textfieldsingleline
elem.textfieldkeepfocusonhide = o.textfieldkeepfocusonhide
---@diagnostic disable-next-line: duplicate-set-field
elem.textInput = function(input) elem:textfieldInput(input, o.isNumeric, o.allowNegative, o.allowDecimal, o.customRegex) end
---@diagnostic disable-next-line: duplicate-set-field
elem.keyDown = function(key)
if (elem:textfieldKeyDown(key) and elem.textInputCustom) then
-- We have updated textfield input and have a custom text input function defined
-- Fire a textInputCustom() call for seamless behavior across all input field actions
elem.textInputCustom()
end
end
---@diagnostic disable-next-line: duplicate-set-field
elem.keyUp = function(key) elem:textfieldKeyUp(key) end
table.insert(UIKeyboardHandler, elem)
end
if (o.toggle) then
elem.toggle = o.toggle
---@diagnostic disable-next-line: duplicate-set-field
elem.keyUp = function(key) elem:textfieldKeyUp(key) end
table.insert(UIKeyboardHandler, elem)
end
if (o.innerShadow and o.shadowColor) then
elem.shadowColor = {}
if (type(o.shadowColor[1]) == "table") then
elem.shadowColor = o.shadowColor
else
elem.shadowColor = { o.shadowColor, o.shadowColor }
end
---@diagnostic disable-next-line: assign-type-mismatch
elem.innerShadow = type(o.innerShadow) == "table" and o.innerShadow or { o.innerShadow, o.innerShadow }
end
if (o.shapeType == ROUNDED and o.rounded) then
elem.setRounded(elem, o.rounded)
-- Light UI mode - don't add rounded corners if it's just for cosmetics
if (not UIElement.lightUIMode or elem.rounded > elem.size.w / 4) then
elem.shapeType = o.shapeType
end
end
if (o.interactive) then
elem.interactive = o.interactive
elem.isactive = true
elem.scrollEnabled = o.scrollEnabled or false
elem.hoverColor = o.hoverColor or nil
elem.pressedColor = o.pressedColor or nil
elem.inactiveColor = o.inactiveColor or o.bgColor
elem.animateColor = table.clone(elem.bgColor)
if (elem.bgImage) then
elem.imageHoverColor = o.imageHoverColor or nil
elem.imagePressedColor = o.imagePressedColor or nil
elem.imageInactiveColor = o.imageInactiveColor or nil
elem.imageAnimateColor = table.clone(elem.imageColor)
end
elem.hoverState = BTN_NONE
elem.hoverClock = UIElement.clock
elem.clickThrough = o.clickThrough
elem.hoverThrough = o.hoverThrough
elem.hoverSound = o.hoverSound
elem.upSound = o.upSound
elem.downSound = o.downSound
elem.pressedPos = { x = 0, y = 0 }
table.insert(UIMouseHandler, elem)
end
if (o.keyboard) then
elem.permanentListener = o.permanentListener
table.insert(UIKeyboardHandler, elem)
end
---Only add root elements to UIElementManager to decrease table traversal speed
if (elem.parent == nil) then
table.insert(UIElementManager, elem)
end
-- Display is enabled by default, comment this out to disable
if (elem.viewport or (elem.parent and elem.parent.viewport)) then
--table.insert(UIViewportManager, elem)
else
UIVisualManager[elem.globalid] = UIVisualManager[elem.globalid] or { }
table.insert(UIVisualManager[elem.globalid], elem)
end
-- Force update global x/y pos when spawning element
elem:updatePos()
elem.displayed = true
return elem
end
-- Spawns a new UIElement and sets the calling object as its parent
---@param o UIElementOptions
---@param copyShape? boolean Whether to copy `shapeType` and `rounded` values to the new object
---@return UIElement
function UIElement:addChild(o, copyShape)
if (o.shift) then
o.pos = { o.shift[1], o.shift[2] }
o.size = { self.size.w - o.shift[1] * 2, self.size.h - o.shift[2] * 2 }
else
o.pos = o.pos and o.pos or { 0, 0 }
o.size = o.size and o.size or { self.size.w, self.size.h }
end
if (copyShape) then
o.shapeType = o.shapeType and o.shapeType or self.shapeType
o.rounded = o.rounded and o.rounded or table.clone(self.roundedInternal)
end
o.parent = self
return UIElement.new(o)
end
-- Specifies rounding value to be used for UIElements with ROUNDED shape type
---@param rounded number|number[]
function UIElement:setRounded(rounded)
if (type(rounded) ~= "table") then
self.roundedInternal = { tonumber(rounded) or 0, tonumber(rounded) or 0 }
else
self.roundedInternal = { rounded[1], rounded[#rounded] }
end
local minSize = math.min(table.unpack_all(self.size))
local minRounded = self.roundedInternal[1] + self.roundedInternal[2] > minSize and minSize / 2 or math.max(unpack(self.roundedInternal))
self.rounded = 0
for i, v in pairs(self.roundedInternal) do
if (v > minRounded) then
self.roundedInternal[i] = minRounded
end
self.rounded = math.max(self.rounded, self.roundedInternal[i])
end
---With GLES we have a perfect disk shader that allows us to draw circles more efficiently
---Set slices to 0 to use it instead of rendering disks made out of vertices
self.diskSlices = is_mobile() and 0 or math.min(self.rounded * 5, 50)
end
-- Adds mouse handlers to use for an interactive UIElement object
---@param btnDown? function Button down callback function
---@param btnUp? function Button up callback function
---@param btnHover? function Mouse hover callback function
---@param btnRightUp? function Right mouse button up callback function
---@param btnUpOutside? function Outside button up callback function
function UIElement:addMouseHandlers(btnDown, btnUp, btnHover, btnRightUp, btnUpOutside)
if (btnDown) then
self.btnDown = btnDown
end
if (btnUp) then
self.btnUp = btnUp
end
if (btnHover) then
self.btnHover = btnHover
end
if (btnRightUp) then
self.btnRightUp = btnRightUp
end
if (btnUpOutside) then
self.btnUpOutside = btnUpOutside
end
end
---Shorthand function to add mouse button down handler
---@param func function
function UIElement:addMouseDownHandler(func)
self:addMouseHandlers(func)
end
---Shorthand function to add mouse button up handler
---@param func function
function UIElement:addMouseUpHandler(func)
self:addMouseHandlers(nil, func)
end
---Shorthand function to add mouse move handler
---@param func function
function UIElement:addMouseMoveHandler(func)
self:addMouseHandlers(nil, nil, func)
end
---Shorthand function to add mouse right button up handler
---@param func function
function UIElement:addMouseUpRightHandler(func)
self:addMouseHandlers(nil, nil, nil, func)
end
---Shorthand function to add mouse button up handler when we're while no longer hovering over the object
---@param func function
function UIElement:addMouseUpOutsideHandler(func)
self:addMouseHandlers(nil ,nil, nil, nil, func)
end
-- Adds keyboard handlers to use for an interactive UIElement object
---@param keyDown? function Keyboard key down callback function
---@param keyUp? function Keyboard key up callback function
function UIElement:addKeyboardHandlers(keyDown, keyUp)
if (keyDown) then
self.keyDownCustom = keyDown
end
if (keyUp) then
self.keyUpCustom = keyUp
end
end
---Adds input handler to use for a textfield object
---@param func? function
function UIElement:addInputCallback(func)
if (func) then
self.textInputCustom = func
end
end
-- Adds enter key handler for an interactive UIElement object
---@param func function
function UIElement:addEnterAction(func)
self.enteraction = func
end
-- Removes currently set enter key handler
function UIElement:removeEnterAction()
self.enteraction = nil
end
-- Adds tab key handler for an interactive UIElement object
---@param func function
function UIElement:addTabAction(func)
self.tabaction = func
end
-- Removes currently set tab key handler
function UIElement:removeTabAction()
self.tabaction = nil
end
---Adds a function to be executed when a focusable element is tabbed out of
---@param func function
function UIElement:addOnLoseTabFocus(func)
self.onLoseFocus = func
end
---Removes a function that is executed when a focusable element is tabbed out of
function UIElement:removeOnLoseTabFocus()
self.onLoseFocus = nil
end
---Adds a function to be executed when a focusable element is tabbed into
---@param func function
function UIElement:addOnReceiveTabFocus(func)
self.onReceiveFocus = func
end
---Removes a function that is executed when a focusable element is tabbed into
function UIElement:removeOnReceiveTabFocus()
self.onReceiveFocus = nil
end
---Sets the previous element to switch focus to when pressing `SHIFT` + `TAB`
---@param element UIElement
---@param btnDownArg ?table Any additional arguments to be used by element's `btnDown()` event
function UIElement:addTabSwitchPrev(element, btnDownArg)
self:addTabSwitch(element, btnDownArg, true)
end
---Sets the element to switch focus to when pressing `TAB`
---@param element UIElement
---@param btnDownArg ?table Any additional arguments to be used by element's `btnDown()` event
---@param prev ?boolean
function UIElement:addTabSwitch(element, btnDownArg, prev)
local btnDownArg = btnDownArg or {}
local action = prev and "tabswitchprevaction" or "tabswitchaction"
local targetName = prev and "prevInput" or "nextInput"
self[targetName] = element
self[action] = function()
if (self.onLoseFocus) then
self.onLoseFocus()
end
for _, v in pairs(UIKeyboardHandler) do
v.keyboard = false
end
element.hoverState = BTN_HVR
element.btnDown(unpack(btnDownArg))
if (element.textfield) then
element.keyboard = true
disable_camera_movement()
end
if (element.onReceiveFocus) then
element.onReceiveFocus()
end
end
end
---Clears the currently set `TAB` switch action
function UIElement:removeTabSwitch()
self.tabswitchaction = nil
end
---Clears the currently set `SHIFT` + `TAB` switch action
function UIElement:removeTabSwitchPrev()
self.tabswitchprevaction = nil
end
---Internal function to reload scrollable list elements. \
---*You likely don't need to call it manually.*
---@param listHolder UIElement
---@param listElements UIElement[]
---@param toReload UIElement
---@param enabled UIElement[]
---@param orientation UIElementScrollMode
function UIElement:reloadListElements(listHolder, listElements, toReload, enabled, orientation)
local listElementSize, shiftVal
if (orientation == SCROLL_VERTICAL) then
listElementSize = listElements[1].size.h
shiftVal = listHolder.shift.y + self.size.h
else
listElementSize = listElements[1].size.w
shiftVal = listHolder.shift.x + self.size.w
end
local checkPos = math.abs(math.ceil(-shiftVal / listElementSize))
for i = #enabled, 1, -1 do
enabled[i]:hide(true)
table.remove(enabled)
end
if (checkPos > 0 and checkPos * listElementSize + shiftVal > 0) then
if (listElements[checkPos]) then
listElements[checkPos]:show(true)
table.insert(enabled, listElements[checkPos])
end
end
while ((shiftVal + checkPos * listElementSize >= 0) and ((orientation == SCROLL_VERTICAL and listHolder.shift.y or listHolder.shift.x) + checkPos * listElementSize <= 0) and (checkPos < #listElements)) do
listElements[checkPos + 1]:show(true)
table.insert(enabled, listElements[checkPos + 1])
checkPos = checkPos + 1
end
toReload:reload()
end
---Shorthand function to initialize a horizontal scrollable list with a scroll bar
---@param listHolder UIElement
---@param listElements UIElement[]
---@param toReload UIElement
---@param posShift ?number[]
---@param scrollSpeed ?number
---@param scrollIgnoreOverride ?boolean
function UIElement:makeHorizontalScrollBar(listHolder, listElements, toReload, posShift, scrollSpeed, scrollIgnoreOverride)
self:makeScrollBar(listHolder, listElements, toReload, posShift, scrollSpeed, scrollIgnoreOverride, SCROLL_HORIZONTAL)
end
---Function to initialize a scrollable list with a scroll bar. \
---**Important: all list elements must have the same height / width depending on list orientation.**
---@param listHolder UIElement Holder element for all list elements
---@param listElements UIElement[] List of elements that should be scrollable
---@param toReload UIElement Holder element that should be rendered on top of scrollable elements
---@param posShift ?number[]
---@param scrollSpeed ?number
---@param scrollIgnoreOverride ?boolean
---@param orientation ?UIElementScrollMode
function UIElement:makeScrollBar(listHolder, listElements, toReload, posShift, scrollSpeed, scrollIgnoreOverride, orientation)
local scrollSpeed = scrollSpeed or 1
local posShift = posShift or { 0 }
self.orientation = orientation or SCROLL_VERTICAL
local enabled = {}
if (self.orientation == SCROLL_VERTICAL) then
listHolder:moveTo(0, listHolder.shift.y == 0 and -listHolder.size.h or listHolder.shift.y)
else
listHolder:moveTo(listHolder.shift.x == 0 and -listHolder.size.w or listHolder.shift.x)
end
self.pressedPos = { x = 0, y = 0 }
self.listReload = function() listHolder.parent:reloadListElements(listHolder, listElements, toReload, enabled, self.orientation) end
---@diagnostic disable-next-line: undefined-field
self.scrollReload = function() if (self.holder) then self.holder:reload() end self:reload() end
self:barScroll(listElements, listHolder, toReload, posShift[1], enabled, true)
UIElement.updatePos(listHolder)
local targetPos = nil
self:addMouseHandlers(
function(s, x, y)
if (is_mobile() and s < 4) then
disable_mouse_camera_movement()
end
local scrollIgnore = UIScrollbarIgnore
if (scrollIgnoreOverride and scrollIgnore) then
UIScrollbarIgnore = false
end
local scrollSuccessful = false
if (s < 4) then
self.pressedPos = self:getLocalPos(x,y)
self.hoverState = BTN_DN
elseif (not UIScrollbarIgnore and ((#UIScrollbarHandler == 1 and listHolder.scrollBar ~= self) or
(MOUSE_X > listHolder.parent.pos.x and MOUSE_X < listHolder.parent.pos.x + listHolder.parent.size.w and MOUSE_Y > listHolder.parent.pos.y and MOUSE_Y < listHolder.parent.pos.y + listHolder.parent.size.h))) then
self:mouseScroll(listElements, listHolder, toReload, y * scrollSpeed, enabled)
posShift[1] = self.orientation == SCROLL_VERTICAL and self.shift.y or self.shift.x
scrollSuccessful = true
end
if (scrollIgnore and not UIScrollbarIgnore) then
UIScrollbarIgnore = true
end
return scrollSuccessful
end, function()
self.scrollReload()
if (is_mobile()) then
enable_mouse_camera_movement()
end
end,
function(x, y)
if (self.hoverState == BTN_DN) then
if (self.orientation == SCROLL_VERTICAL) then
targetPos = self:getLocalPos(x,y).y - self.pressedPos.y + self.shift.y
posShift[1] = self.shift.y
else
targetPos = self:getLocalPos(x,y).x - self.pressedPos.x + self.shift.x
posShift[1] = self.shift.x
end
end
end, nil, function()
self.scrollReload()
if (is_mobile()) then
enable_mouse_camera_movement()
end
end)
if (not self.isScrollBar) then
self.isScrollBar = true
table.insert(UIScrollbarHandler, self)
end
local barScroller = self:addChild({})
barScroller.uid = generate_uid()
barScroller.killAction = function()
if (listHolder.parent.scrollableListTouchScrollActive) then
listHolder.parent.btnUp()
end
remove_hooks("barScroller" .. barScroller.uid)
remove_hooks("touchScroller" .. barScroller.uid)
end
add_hook("pre_draw", "barScroller" .. barScroller.uid, function()
if (targetPos ~= nil) then
self:barScroll(listElements, listHolder, toReload, targetPos, enabled)
targetPos = nil
end
end)
local lastListHolderVal = -1
local deltaChange = 0
local lastClock = UIElement.clock
listHolder.parent:addMouseHandlers(function(_, x, y)
disable_mouse_camera_movement()
listHolder.parent.scrollableListTouchScrollActive = true
lastListHolderVal = (self.orientation == SCROLL_VERTICAL) and y or x
end, function()
listHolder.parent.scrollableListTouchScrollActive = false
lastListHolderVal = -1
enable_mouse_camera_movement()
end, function(x, y)
if (listHolder.parent.scrollableListTouchScrollActive) then
lastClock = UIElement.clock
local targetValue = (self.orientation == SCROLL_VERTICAL) and y or x
deltaChange = lastListHolderVal - targetValue
lastListHolderVal = targetValue
end
end, nil, function()
listHolder.parent.scrollableListTouchScrollActive = false
lastListHolderVal = -1