-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAtomUI.lua
More file actions
8256 lines (7465 loc) · 372 KB
/
Copy pathAtomUI.lua
File metadata and controls
8256 lines (7465 loc) · 372 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
local safe_clone = cloneref or function(service) return service end
local tween_service = safe_clone(game:GetService("TweenService"))
local input_service = safe_clone(game:GetService("UserInputService"))
local run_service = safe_clone(game:GetService("RunService"))
local players = safe_clone(game:GetService("Players"))
local text_service = safe_clone(game:GetService("TextService"))
local core_gui = safe_clone(game:GetService("CoreGui"))
local gui_service = safe_clone(game:GetService("GuiService"))
local http_service = safe_clone(game:GetService("HttpService"))
local lighting = safe_clone(game:GetService("Lighting"))
--#endregion═════════════════════════════════════════════════════════════════════
--#region ══╗ Icons System ╔═════════════════════════════════════════════════════
local icons_module = nil
local icons_cache = {}
local function load_icons_module()
if icons_module then return icons_module end
local ok, result = pcall(function()
return loadstring(game:HttpGetAsync("https://raw.githubusercontent.com/Footagesus/Icons/main/Main-v2.lua"))()
end)
if ok and result then
icons_module = result
pcall(function() icons_module.SetIconsType("lucide") end)
end
return icons_module
end
local function get_icon(name, fallback)
if not name or name == "" then
return fallback or "rbxassetid://94219370057308" -- default tab icon
end
if name:match("^rbxassetid://") or name:match("^rbxasset://") or name:match("^http") then
return name
end
if icons_cache[name] then
return icons_cache[name]
end
local mod = load_icons_module()
if mod and type(mod.GetIcon) == "function" then
local ok, icon_id = pcall(mod.GetIcon, name)
if ok and icon_id and icon_id ~= "" then
icons_cache[name] = icon_id
return icon_id
end
end
return fallback or "rbxassetid://94219370057308" -- default tab icon (safe fallback)
end
local default_icons = {
section = "rbxassetid://98092584632154",
tab = "rbxassetid://94219370057308",
group = "rbxassetid://10723427199",
search = "rbxassetid://10734943674",
settings = "rbxassetid://6031280882",
expand = "rbxassetid://111626678408582",
resize = "rbxassetid://111626678408582",
close = "rbxassetid://10747384394",
dropdown_arrow = "rbxassetid://111626678408582",
}
local atomic_logo = "rbxassetid://113081944862488"
--#endregion═════════════════════════════════════════════════════════════════════
--#region ══╗ Core ╔═════════════════════════════════════════════════════════════
local atom_ui = {}
atom_ui.__index = atom_ui
local local_player = players.LocalPlayer
local player_mouse = local_player:GetMouse()
local is_mobile = input_service.TouchEnabled and not input_service.KeyboardEnabled
local scale_factor = is_mobile and 0.7 or 1
local default_font_enum = Enum.Font.GothamSemibold
local default_font_family = "rbxasset://fonts/families/GothamSSm.json"
local function resolve_font_enum(weight)
if weight == Enum.FontWeight.Bold then
return Enum.Font.GothamBold
end
if weight == Enum.FontWeight.Medium then
return Enum.Font.GothamMedium
end
return default_font_enum
end
local font_face_supported = false
local function make_font_descriptor(family, weight, style, enumFont)
return {
__atom_font = true,
Family = family or default_font_family,
Weight = weight or Enum.FontWeight.SemiBold,
Style = style or Enum.FontStyle.Normal,
EnumFont = enumFont or resolve_font_enum(weight)
}
end
local function is_font_descriptor(value)
return type(value) == "table" and value.__atom_font == true
end
local function get_fallback_font(value, fallbackEnum)
if is_font_descriptor(value) then
return value.EnumFont or fallbackEnum or default_font_enum
end
return fallbackEnum or default_font_enum
end
local function apply_font(instance, value, fallbackEnum)
if not instance or not (instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox")) then
return false
end
pcall(function()
instance.Font = get_fallback_font(value, fallbackEnum)
end)
return false
end
local Font = {
new = function(family, weight, style)
family = family or default_font_family
weight = weight or Enum.FontWeight.SemiBold
style = style or Enum.FontStyle.Normal
return make_font_descriptor(family, weight, style)
end,
fromEnum = function(enumFont)
enumFont = enumFont or default_font_enum
return make_font_descriptor(default_font_family, Enum.FontWeight.SemiBold, Enum.FontStyle.Normal, enumFont)
end
}
local function create(className, properties)
local instance = Instance.new(className)
for property, value in pairs(properties) do
if property ~= "Parent" then
if property == "FontFace" then
apply_font(instance, value)
else
pcall(function()
instance[property] = value
end)
end
end
end
if properties.Parent then
instance.Parent = properties.Parent
end
return instance
end
local function tween_to(instance, properties, duration, easingStyle, easingDirection)
if not instance or typeof(instance) ~= "Instance" then
return nil
end
local ok, tween_info = pcall(function()
return TweenInfo.new(duration or 0.22, easingStyle or Enum.EasingStyle.Quint, easingDirection or Enum.EasingDirection.Out)
end)
if not ok then
return nil
end
local ok2, tween_obj = pcall(function()
return tween_service:Create(instance, tween_info, properties)
end)
if not ok2 or not tween_obj then
return nil
end
pcall(function() tween_obj:Play() end)
return tween_obj
end
local function disconnect_signal(conn)
if conn and typeof(conn) == "RBXScriptConnection" then
pcall(function()
if conn.Connected then
conn:Disconnect()
end
end)
elseif type(conn) == "function" then
pcall(conn)
end
end
local function make_draggable(frame, handle, libraryRef)
local amIDragging = false
local activeDragInput
local whereDidIStart
local whereWasIBefore
local targetDragPosition = frame.Position
local dragLerpSpeed = is_mobile and 18 or 22
local renderDragConn
handle = handle or frame
local function stopDragLoop()
disconnect_signal(renderDragConn)
renderDragConn = nil
end
local function ensureDragLoop()
if renderDragConn and renderDragConn.Connected then
return
end
renderDragConn = run_service.RenderStepped:Connect(function(dt)
if not frame or not frame.Parent then
stopDragLoop()
return
end
local currentPos = frame.Position
local goalPos = targetDragPosition
local offsetDelta = math.abs(goalPos.X.Offset - currentPos.X.Offset) + math.abs(goalPos.Y.Offset - currentPos.Y.Offset)
if offsetDelta <= 0.1 then
if currentPos ~= goalPos then
frame.Position = goalPos
end
if not amIDragging then
stopDragLoop()
end
return
end
local alpha = math.clamp(1 - math.exp(-dragLerpSpeed * dt), 0, 0.45)
frame.Position = currentPos:Lerp(goalPos, alpha)
end)
if libraryRef and type(libraryRef._TrackConnection) == "function" then
libraryRef:_TrackConnection(renderDragConn)
end
end
local function updateTargetPositionYay(input)
local howMuchDidIMove = input.Position - whereDidIStart
targetDragPosition = UDim2.new(
whereWasIBefore.X.Scale,
whereWasIBefore.X.Offset + howMuchDidIMove.X,
whereWasIBefore.Y.Scale,
whereWasIBefore.Y.Offset + howMuchDidIMove.Y
)
end
local inputBeganConn = handle.InputBegan:Connect(function(input)
local isMouse = input.UserInputType == Enum.UserInputType.MouseButton1
local isTouch = input.UserInputType == Enum.UserInputType.Touch
if not isMouse and not isTouch then
return
end
amIDragging = true
activeDragInput = isTouch and input or nil
whereDidIStart = input.Position
whereWasIBefore = frame.Position
targetDragPosition = frame.Position
ensureDragLoop()
local inputEndConn
inputEndConn = input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then
if frame and frame.Parent then
frame.Position = targetDragPosition
end
amIDragging = false
activeDragInput = nil
stopDragLoop()
if inputEndConn then
inputEndConn:Disconnect()
inputEndConn = nil
end
end
end)
if libraryRef and type(libraryRef._TrackConnection) == "function" then
libraryRef:_TrackConnection(inputEndConn)
end
end)
local userInputChangedConn = input_service.InputChanged:Connect(function(input)
if not amIDragging then
return
end
if input.UserInputType == Enum.UserInputType.MouseMovement then
updateTargetPositionYay(input)
elseif input.UserInputType == Enum.UserInputType.Touch and (activeDragInput == nil or input == activeDragInput) then
updateTargetPositionYay(input)
end
end)
if libraryRef and type(libraryRef._TrackConnection) == "function" then
libraryRef:_TrackConnection(inputBeganConn)
libraryRef:_TrackConnection(userInputChangedConn)
end
end
local function start_position_tracker(libraryRef, anchorInstance, updateFn)
if type(updateFn) ~= "function" or not anchorInstance then
return nil
end
local active = true
local connections = {}
local function bindSignal(instance, propertyName)
if not instance then
return
end
table.insert(connections, libraryRef:_TrackConnection(instance:GetPropertyChangedSignal(propertyName):Connect(updateFn)))
end
bindSignal(anchorInstance, "AbsolutePosition")
bindSignal(anchorInstance, "AbsoluteSize")
table.insert(connections, libraryRef:_TrackConnection(anchorInstance.AncestryChanged:Connect(updateFn)))
if libraryRef and libraryRef.screen_gui then
bindSignal(libraryRef.screen_gui, "AbsoluteSize")
end
updateFn()
return function()
if not active then
return
end
active = false
for i = #connections, 1, -1 do
disconnect_signal(connections[i])
connections[i] = nil
end
end
end
local function attach_scrollbar(libraryRef, scrollFrame, parentInstance, options)
if not libraryRef or not scrollFrame or not parentInstance then
return nil
end
options = options or {}
local trackWidth = math.max(4, math.floor((options.TrackWidth or (6 * scale_factor)) + 0.5))
local thumbWidth = math.max(2, math.min(trackWidth - 1, math.floor((options.ThumbWidth or (3 * scale_factor)) + 0.5)))
local edgeInset = math.max(1, math.floor((options.EdgeInset or (2 * scale_factor)) + 0.5))
local verticalInset = math.max(2, math.floor((options.VerticalInset or (4 * scale_factor)) + 0.5))
local minThumbHeight = math.max(18, math.floor((options.MinThumbHeight or (26 * scale_factor)) + 0.5))
local idleThumbHeight = math.max(minThumbHeight, math.floor((options.IdleThumbHeight or (42 * scale_factor)) + 0.5))
local alwaysShowTrack = options.AlwaysShowTrack == true
local zIndex = options.ZIndex or ((scrollFrame.ZIndex or 1) + 2)
local xOffset = options.XOffset or 0
local trackFrame = create("Frame", {
Name = "AtomUIScrollbarTrack",
BackgroundColor3 = Color3.fromRGB(11, 11, 14),
BackgroundTransparency = 0.12,
BorderSizePixel = 0,
Visible = false,
ZIndex = zIndex,
Parent = parentInstance
})
create("UICorner", {CornerRadius = UDim.new(1, 0), Parent = trackFrame})
local thumbFrame = create("Frame", {
Name = "AtomUIScrollbarThumb",
AnchorPoint = Vector2.new(0.5, 0),
BackgroundColor3 = libraryRef.config.AccentColor,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 0, 0),
Size = UDim2.new(0, thumbWidth, 0, minThumbHeight),
ZIndex = zIndex + 1,
Parent = trackFrame
})
create("UICorner", {CornerRadius = UDim.new(1, 0), Parent = thumbFrame})
local function resolveCanvasHeight()
local canvasHeight = math.max(scrollFrame.CanvasSize.Y.Offset, 0)
local okAbsoluteCanvas, absoluteCanvasSize = pcall(function()
return scrollFrame.AbsoluteCanvasSize
end)
if okAbsoluteCanvas and typeof(absoluteCanvasSize) == "Vector2" then
canvasHeight = math.max(canvasHeight, absoluteCanvasSize.Y)
end
return canvasHeight
end
local function updateScrollbar()
if libraryRef._destroyed or not scrollFrame.Parent or not parentInstance.Parent or not trackFrame.Parent then
return false
end
local frameSize = scrollFrame.AbsoluteSize
local windowHeight = frameSize.Y
local frameVisible = scrollFrame.Visible and frameSize.X > 0 and frameSize.Y > 0
local canvasHeight = math.max(resolveCanvasHeight(), windowHeight)
local canScroll = frameVisible and canvasHeight > (windowHeight + 1)
if not frameVisible then
trackFrame.Visible = false
thumbFrame.Visible = false
return true
end
local parentAbsolute = parentInstance.AbsolutePosition
local frameAbsolute = scrollFrame.AbsolutePosition
local trackHeight = math.max(0, frameSize.Y - (verticalInset * 2))
if trackHeight <= 2 then
trackFrame.Visible = false
return true
end
local trackX = math.floor((frameAbsolute.X - parentAbsolute.X) + frameSize.X - trackWidth - edgeInset + xOffset + 0.5)
local trackY = math.floor((frameAbsolute.Y - parentAbsolute.Y) + verticalInset + 0.5)
trackFrame.Visible = true
trackFrame.Position = UDim2.fromOffset(trackX, trackY)
trackFrame.Size = UDim2.fromOffset(trackWidth, math.floor(trackHeight + 0.5))
trackFrame.BackgroundColor3 = Color3.fromRGB(11, 11, 14)
thumbFrame.BackgroundColor3 = libraryRef.config.AccentColor
if not canScroll then
trackFrame.Visible = alwaysShowTrack
thumbFrame.Visible = alwaysShowTrack
if alwaysShowTrack then
local restingThumbHeight = math.min(idleThumbHeight, trackHeight)
thumbFrame.Size = UDim2.fromOffset(thumbWidth, math.floor(restingThumbHeight + 0.5))
thumbFrame.Position = UDim2.fromOffset(math.floor(trackWidth * 0.5 + 0.5), math.floor(math.max(0, (trackHeight - restingThumbHeight) * 0.08) + 0.5))
end
return true
end
thumbFrame.Visible = true
local minimumThumbHeight = math.min(minThumbHeight, trackHeight)
local thumbHeight = math.clamp((windowHeight / canvasHeight) * trackHeight, minimumThumbHeight, trackHeight)
local maxScroll = math.max(canvasHeight - windowHeight, 0)
local scrollRatio = maxScroll > 0 and math.clamp(scrollFrame.CanvasPosition.Y / maxScroll, 0, 1) or 0
local thumbTravel = math.max(trackHeight - thumbHeight, 0)
thumbFrame.Size = UDim2.fromOffset(thumbWidth, math.floor(thumbHeight + 0.5))
thumbFrame.Position = UDim2.fromOffset(math.floor(trackWidth * 0.5 + 0.5), math.floor((thumbTravel * scrollRatio) + 0.5))
return true
end
local function bindProperty(propertyName)
local okSignal, signal = pcall(function()
return scrollFrame:GetPropertyChangedSignal(propertyName)
end)
if okSignal and signal then
libraryRef:_TrackConnection(signal:Connect(updateScrollbar))
end
end
bindProperty("CanvasPosition")
bindProperty("CanvasSize")
bindProperty("AbsoluteSize")
bindProperty("AbsoluteCanvasSize")
bindProperty("Visible")
local stopFloatingTracker = start_position_tracker(libraryRef, scrollFrame, updateScrollbar)
if stopFloatingTracker then
libraryRef:_TrackConnection(stopFloatingTracker)
end
if type(libraryRef._scrollbarRefreshers) == "table" then
table.insert(libraryRef._scrollbarRefreshers, updateScrollbar)
end
updateScrollbar()
return {
Track = trackFrame,
Thumb = thumbFrame,
Refresh = updateScrollbar
}
end
local function get_player_avatar(userId)
local didItWork, whatWeGot = pcall(function()
return players:GetUserThumbnailAsync(userId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size420x420)
end)
if didItWork then
return whatWeGot
end
return "rbxassetid://135756197673563"
end
local function resolve_avatar_3d(defaultUserId)
local fallbackUserId = tonumber(defaultUserId) or 1
local endpoint = "https://thumbnails.roblox.com/v1/users/avatar-3d?userId=" .. tostring(fallbackUserId)
local okBody, rawBody = pcall(function()
return game:HttpGet(endpoint)
end)
if not okBody or type(rawBody) ~= "string" or rawBody == "" then
return fallbackUserId, nil
end
local okDecode, payload = pcall(function()
return http_service:JSONDecode(rawBody)
end)
if not okDecode or type(payload) ~= "table" then
return fallbackUserId, nil
end
local resolvedUserId = tonumber(payload.targetId) or tonumber(payload.userId) or fallbackUserId
local imageUrl = type(payload.imageUrl) == "string" and payload.imageUrl or nil
return resolvedUserId, imageUrl
end
local function measure_text_width(text, textSize, font)
local textBoundsYay = text_service:GetTextSize(text, textSize, font or Enum.Font.GothamSemibold, Vector2.new(math.huge, math.huge))
return textBoundsYay.X
end
local function truncate_text(text, maxWidth, textSize, font)
local fullWidth = measure_text_width(text, textSize, font)
if fullWidth <= maxWidth then
return text
end
local truncated = text
while measure_text_width(truncated .. "...", textSize, font) > maxWidth and #truncated > 0 do
truncated = truncated:sub(1, -2)
end
return truncated .. "..."
end
local function normalize_search(text)
return string.lower(tostring(text or "")):gsub("^%s+", ""):gsub("%s+$", "")
end
local function normalize_dropdown(options)
local normalized = {}
if type(options) == "table" then
for _, option in ipairs(options) do
if option ~= nil then
table.insert(normalized, option)
end
end
end
if #normalized == 0 then
normalized[1] = "None"
end
return normalized
end
local function get_dropdown_signature(options)
if type(options) ~= "table" then
return "0"
end
local count = #options
if count <= 0 then
return "0"
end
local signaturePieces = table.create and table.create(count + 1, "") or {}
signaturePieces[1] = tostring(count)
for index, option in ipairs(options) do
signaturePieces[index + 1] = tostring(option)
end
return table.concat(signaturePieces, "\31")
end
local function get_decimal_places(value)
if type(value) ~= "number" then
return 0
end
local valueString = tostring(value)
local decimalPart = valueString:match("%.(%d+)")
if decimalPart then
return #decimalPart
end
local exponentPart = valueString:match("[eE]([%+%-]?%d+)")
if exponentPart then
local exponent = tonumber(exponentPart) or 0
if exponent < 0 then
return -exponent
end
end
return 0
end
local function round_to_decimals(value, decimals)
if decimals <= 0 then
if value >= 0 then
return math.floor(value + 0.5)
end
return math.ceil(value - 0.5)
end
local factor = 10 ^ decimals
if value >= 0 then
return math.floor(value * factor + 0.5) / factor
end
return math.ceil(value * factor - 0.5) / factor
end
local function resolve_precision(minValue, maxValue, increment, defaultValue)
local precision = 0
precision = math.max(precision, get_decimal_places(minValue))
precision = math.max(precision, get_decimal_places(maxValue))
precision = math.max(precision, get_decimal_places(increment))
precision = math.max(precision, get_decimal_places(defaultValue))
return math.clamp(precision, 0, 6)
end
local function normalize_slider_value(value, minValue, maxValue, increment, precision)
local numericValue = tonumber(value) or minValue
numericValue = math.clamp(numericValue, minValue, maxValue)
local normalizedIncrement = math.max(math.abs(tonumber(increment) or 1), 1e-6)
local steps = math.floor(((numericValue - minValue) / normalizedIncrement) + 0.5)
local snappedValue = minValue + (steps * normalizedIncrement)
snappedValue = round_to_decimals(snappedValue, precision)
return math.clamp(snappedValue, minValue, maxValue)
end
local function format_slider_value(value, precision)
if precision <= 0 then
return tostring(round_to_decimals(value, 0))
end
local formatted = string.format("%." .. tostring(precision) .. "f", value)
formatted = formatted:gsub("(%..-)0+$", "%1"):gsub("%.$", "")
return formatted
end
local function serialize_value(value)
local valueType = typeof(value)
if valueType == "Color3" then
return {
__type = "Color3",
r = value.R,
g = value.G,
b = value.B
}
end
if valueType == "EnumItem" and value.EnumType == Enum.KeyCode then
return {
__type = "KeyCode",
value = value.Name
}
end
return value
end
local function deserialize_value(value)
if type(value) ~= "table" or not value.__type then
return value
end
if value.__type == "Color3" and value.r and value.g and value.b then
return Color3.new(value.r, value.g, value.b)
end
if value.__type == "KeyCode" and value.value then
return Enum.KeyCode[value.value] or Enum.KeyCode.Unknown
end
return value
end
local function sanitize_config_name(name)
local cleanName = tostring(name or "default")
cleanName = cleanName:gsub("[\\/:*?\"<>|]", "_")
cleanName = cleanName:gsub("^%s+", ""):gsub("%s+$", "")
if cleanName == "" then
cleanName = "default"
end
return cleanName
end
local function get_config_folder()
return "AtomUIConfigs"
end
local function ensure_config_folder()
local folder = get_config_folder()
if isfolder and isfolder(folder) then
return true, folder
end
if makefolder then
pcall(function()
makefolder(folder)
end)
end
if isfolder and isfolder(folder) then
return true, folder
end
return false, folder
end
local function get_config_filename(configName)
return sanitize_config_name(configName) .. ".json"
end
local function get_writable_config_path(configName)
local fileName = get_config_filename(configName)
local okFolder, folder = ensure_config_folder()
if okFolder then
return folder .. "/" .. fileName, true
end
return fileName, false
end
local function get_readable_config_paths(configName)
local fileName = get_config_filename(configName)
local folder = get_config_folder()
return {
folder .. "/" .. fileName,
folder .. "\\" .. fileName,
fileName
}
end
local RUNTIME_INSTANCE_KEY = "__ATOM_UI_ACTIVE"
local SCREEN_GUI_NAME = "AtomUI"
local function get_shared_env()
if type(getgenv) == "function" then
local okEnv, sharedEnv = pcall(getgenv)
if okEnv and type(sharedEnv) == "table" then
return sharedEnv
end
end
return _G
end
local function destroy_existing_guis()
local roots = {}
local seenRoots = {}
local function addRoot(root)
if not root or seenRoots[root] then
return
end
seenRoots[root] = true
table.insert(roots, root)
end
addRoot(core_gui)
if type(gethui) == "function" then
local okHui, huiRoot = pcall(gethui)
if okHui then
addRoot(huiRoot)
end
end
if local_player then
local okPlayerGui, playerGui = pcall(function()
return local_player:FindFirstChild("PlayerGui")
end)
if okPlayerGui then
addRoot(playerGui)
end
end
for _, root in ipairs(roots) do
local okChildren, children = pcall(function()
return root:GetChildren()
end)
if okChildren and type(children) == "table" then
for _, child in ipairs(children) do
if child and child:IsA("ScreenGui") and child.Name == SCREEN_GUI_NAME then
pcall(function()
child:Destroy()
end)
end
end
end
end
end
local function cleanup_previous_instance()
local sharedEnv = get_shared_env()
local previousInstance = rawget(sharedEnv, RUNTIME_INSTANCE_KEY)
if previousInstance and type(previousInstance) == "table" and type(previousInstance.Destroy) == "function" then
pcall(function()
previousInstance:Destroy()
end)
end
rawset(sharedEnv, RUNTIME_INSTANCE_KEY, nil)
destroy_existing_guis()
end
function atom_ui.new(config)
cleanup_previous_instance()
local self = setmetatable({}, atom_ui)
self.config = config or {}
self.config.Name = self.config.Name or "o11 vision"
self.config.AccentColor = self.config.AccentColor or Color3.fromRGB(2, 133, 255)
self.config.BackgroundColor = self.config.BackgroundColor or Color3.fromRGB(16, 16, 16)
self.config.SecondaryColor = self.config.SecondaryColor or Color3.fromRGB(18, 18, 18)
self.config.TextColor = self.config.TextColor or Color3.fromRGB(255, 255, 255)
self.config.SubTextColor = self.config.SubTextColor or Color3.fromRGB(124, 124, 124)
self.config.CustomBackground = self.config.CustomBackground or false
self.config.BackgroundImage = self.config.BackgroundImage or ""
self.config.BackgroundTransparency = self.config.BackgroundTransparency or 0.35
-- ToggleImage: hub makers can pass a custom rbxassetid:// for the toggle button logo
self.config.ToggleImage = self.config.ToggleImage or nil
self.sections = {}
self.all_tabs = {}
self.active_tab = nil
self.notifications = {}
self.is_visible = true
self.dropdown_holder = nil
self.toggleKeyCode = Enum.KeyCode.RightControl
self.toggleButtonVisible = true
self._destroyed = false
self._connections = {}
self._trackedControls = {}
self._lastControlRegistration = 0
self._autoConfigName = sanitize_config_name((self.config.Name or "AtomUI") .. "_last")
local autoConfigSetting = self.config.AutoConfig
if autoConfigSetting == nil then
autoConfigSetting = self.config.AutoSaveConfig
end
if autoConfigSetting == nil then
autoConfigSetting = false
end
self._autoConfigEnabled = autoConfigSetting == true
self._autoConfigLoadAttempted = not self._autoConfigEnabled
self._autoConfigAccumulator = 0
self._autoConfigInterval = 1.2
self._autoConfigSnapshot = nil
self._isApplyingConfig = false
self._configPathHints = {}
self._smoothScrollFrames = {}
self._scrollbarRefreshers = {}
self._searchQuery = ""
self._fpsRollingSize = 60
self._fpsRollingWindow = table.create and table.create(self._fpsRollingSize, 0) or {}
self._fpsRollingTotal = 0
self._fpsRollingIndex = 1
self._fpsRollingCount = 0
self._latestFPSValue = 0
self._cachedViewportSize = Vector2.new(1280, 720)
self._cachedViewportWidth = 1280
self._cachedViewportHeight = 720
self._cachedViewportAreaScale = 1
self._blurEffectRef = nil
self._snowflakes = {}
self._snowSpawnAccumulator = 0
self._snowMaxFlakes = is_mobile and 45 or 90
self._overlayMode = "None"
self._overlayModes = {"Snow", "Rain", "Stars", "None"}
self._backgroundFxTime = 0
self._backgroundFxAccumulator = 0
self._textGradientAnimationTime = 0
self._gradientAnimationAccumulator = 0
self._overlayUpdateAccumulator = 0
self._watermarkUpdateAccumulator = 0
self._watermarkLastWidth = 0
self._refreshJobs = {}
self._notificationTimestamps = {}
self._uiVisualSettings = {
Blur = false,
Snow = false,
BackgroundEffects = false,
TextGradient = false,
ESPSelfPreview = false,
HideName = false
}
self._fontPresets = {
{Name = "Gotham", EnumFont = Enum.Font.Gotham, Family = "rbxasset://fonts/families/GothamSSm.json", Weight = Enum.FontWeight.SemiBold},
{Name = "Gotham Medium", EnumFont = Enum.Font.GothamMedium, Family = "rbxasset://fonts/families/GothamSSm.json", Weight = Enum.FontWeight.Medium},
{Name = "Montserrat", EnumFont = Enum.Font.Gotham, Family = "rbxasset://fonts/families/Montserrat.json", Weight = Enum.FontWeight.SemiBold},
{Name = "Nunito", EnumFont = Enum.Font.Gotham, Family = "rbxasset://fonts/families/Nunito.json", Weight = Enum.FontWeight.SemiBold},
{Name = "Bodoni", EnumFont = Enum.Font.Bodoni},
{Name = "Garamond", EnumFont = Enum.Font.Garamond},
{Name = "Source Sans", EnumFont = Enum.Font.SourceSans, Family = "rbxasset://fonts/families/SourceSansPro.json", Weight = Enum.FontWeight.SemiBold},
{Name = "Highway", EnumFont = Enum.Font.Highway},
{Name = "Antique", EnumFont = Enum.Font.Antique},
{Name = "Code", EnumFont = Enum.Font.Code}
}
self._fontPresetIndex = 1
self._gradientLabels = {}
self._gradientObjects = {}
self._espPreviewProvider = nil
self._espPreviewData = nil
self._espPreviewState = nil
self._espPreviewResolveAccumulator = 0
self._espPreviewUpdateAccumulator = 0
self._espPreviewWasShowing = false
self._espPreviewPanel = nil
self._espPreviewViewport = nil
self._espPreviewWorldModel = nil
self._espPreviewCamera = nil
self._espPreviewCharacter = nil
self._espPreviewLastCharacter = nil
self._espPreviewPartDefaults = {}
self._espPreviewHeadPart = nil
self._espPreviewRootPart = nil
self._espPreviewVisualState = nil
self._espPreviewVisualDirty = false
self._espPreviewProjectionCache = nil
self._espPreviewProjectionDirty = true
self._espPreviewHighlight = nil
self._espPreviewHeaderTag = nil
self._espPreviewBox = nil
self._espPreviewBoxStroke = nil
self._espPreviewHealthTrack = nil
self._espPreviewHealthFill = nil
self._espPreviewDot = nil
self._espPreviewTracer = nil
self._espPreviewName = nil
self._espPreviewItem = nil
self._espPreviewDistance = nil
self._espPreviewWalkTrack = nil
self._espPreviewAnimationId = nil
self._espPreviewAvatar3DUserId = tonumber(local_player.UserId) or 0
self._espPreviewAvatar3DImageUrl = nil
self._espPreviewRotationYaw = math.rad(180)
self._espPreviewRotationTargetYaw = math.rad(180)
self._espPreviewStaticMode = true
self._espPreviewAllowManualRotation = true
self._espPreviewPivotYOffset = -2
self._espPreviewRotateCapture = nil
self._espPreviewIsRotating = false
self._espPreviewRotateInput = nil
self._espPreviewRotateLastX = 0
self:BuildUI()
rawset(get_shared_env(), RUNTIME_INSTANCE_KEY, self)
return self
end
function atom_ui:_TrackConnection(conn)
if conn then
table.insert(self._connections, conn)
if #self._connections % 100 == 0 then
local activeConnections = {}
for _, item in ipairs(self._connections) do
if item then
if typeof(item) == "RBXScriptConnection" then
if item.Connected then
table.insert(activeConnections, item)
end
else
table.insert(activeConnections, item)
end
end
end
self._connections = activeConnections
end
end
return conn
end
function atom_ui:RegisterControl(flag, getter, setter)
if type(flag) ~= "string" or flag == "" then return end
if type(getter) ~= "function" or type(setter) ~= "function" then return end
self._trackedControls[flag] = {
get = getter,
set = setter
}
self._lastControlRegistration = os.clock()
end
function atom_ui:_RegisterRefreshJob(interval, isAliveFn, stepFn)
if type(stepFn) ~= "function" then
return nil
end
local job = {
Interval = math.max(tonumber(interval) or 0.5, 0.05),
IsAlive = isAliveFn,
Step = stepFn,
Accumulator = 0
}
table.insert(self._refreshJobs, job)
return job
end
function atom_ui:_StepRefreshJobs(dt)
if not self._refreshJobs then
return
end
local resolvedDt = tonumber(dt) or 0
for index = #self._refreshJobs, 1, -1 do
local job = self._refreshJobs[index]
local keepJob = true
if type(job) ~= "table" or type(job.Step) ~= "function" then
keepJob = false
elseif type(job.IsAlive) == "function" then
local okAlive, isAlive = pcall(job.IsAlive)
keepJob = okAlive and isAlive ~= false
end
if not keepJob then
table.remove(self._refreshJobs, index)
else
job.Accumulator = (job.Accumulator or 0) + resolvedDt