Run it
Copy the script and paste it into your executor.
Shared = {
HideName = ,
SaveKey = ,
Language = ,
Theme = ,
KeyFile = ,
SettingsFile = ,
CurrentKey = ,
}
HS = :()
isfile readfile (Shared.SettingsFile)
(()
d = HS:((Shared.SettingsFile))
(d) ==
(d.HideName) == Shared.HideName = d.HideName
(d.SaveKey) == Shared.SaveKey = d.SaveKey
(d.Language) == Shared.Language = d.Language
(d.Theme) == Shared.Theme = d.Theme
)
Shared.()
writefile
(()
(Shared.SettingsFile, HS:({
HideName = Shared.HideName,
SaveKey = Shared.SaveKey,
Language = Shared.Language,
Theme = Shared.Theme,
}))
)
Shared.()
writefile Shared.CurrentKey #Shared.CurrentKey >
(() (Shared.KeyFile, Shared.CurrentKey) )
Shared.()
delfile isfile (Shared.KeyFile)
(() (Shared.KeyFile) )
scrambleChars, bindings, started = {}, {},
() utf8.(.(, ))
Shared.(name)
Shared.HideName name
t = {}
i = , #name
scrambleChars[i] = scrambleChars[i] ()
t[i] = scrambleChars[i]
.(t)
()
started
started =
.(()
.()
Shared.HideName
i = ,
scrambleChars[i] == .() <
scrambleChars[i] = ()
i = #bindings, , -
b = bindings[i]
b.label.Parent
.(bindings, i)
s = Shared.(b.name)
b.fmt s = b.(s)
b.label.Text ~= s b.label.Text = s
#bindings ==
started =
)
Shared.(label, name, fmt)
.(bindings, {label = label, name = name, fmt = fmt})
s = Shared.(name)
label.Text = fmt (s) s
()
(keyData)
XYZ_TIER =
(keyData) == keyData.tier
s = (keyData.tier)
XYZ_TIER = s:(, ):() .. s:():()
XYZ_TIER:():() XYZ_TIER =
ProtectionConfig = {
SecretKey = ,
HubName =
}
player = :().LocalPlayer
player
player:( .. ProtectionConfig.HubName)
(ProtectionConfig.HubName .. )
:()
.Loaded:()
Players = :()
ReplicatedStorage = :()
RunService = :()
Workspace = :()
UserInputService = :()
HttpService = :()
Lighting = :()
TeleportService = :()
StarterGui = :()
VirtualInputManager = :()
VirtualUser = :()
LocalPlayer = Players.LocalPlayer
LocalPlayer:()
()
ok, renv = ()
ok (renv) == (renv.print) == renv.print
XYZBanner = {
Print = (),
Started = .(),
Last = .(),
Done = ,
Total = ,
}
MarioBanner = XYZBanner
XYZBanner.()
ok, executor = (identifyexecutor)
ok (executor) ~= executor =
rule = .(, )
XYZBanner.(.({
,
,
rule,
,
.. executor .. .. :().LocalPlayer.Name,
rule,
}, ))
XYZBanner.(label)
now = .()
XYZBanner.Done = .(XYZBanner.Done + , XYZBanner.Total)
filled = .(XYZBanner.Done / XYZBanner.Total * + )
XYZBanner.(.(,
.(, filled) .. .(, - filled),
.(XYZBanner.Done / XYZBanner.Total * ), label, .((now - XYZBanner.Last) * )))
XYZBanner.Last = now
XYZBanner.()
rule = .(, )
XYZBanner.(.({
rule,
.(, .((.() - XYZBanner.Started) * )),
rule,
}, ))
(XYZBanner.Show)
(XYZBanner.Step, )
environment = ()
environment.XYZUnload (environment.XYZUnload)
environment.BloxStrikeUnload (environment.BloxStrikeUnload)
LPH_OBFUSCATED
(fn) fn
LPH_JIT, LPH_JIT_MAX, LPH_NO_VIRTUALIZE = Passthrough, Passthrough, Passthrough
vector2New = .new
cframeLookAt = .lookAt
osClock = .clock
()
TweenService = :()
UserInputService = :()
GuiService = :()
RunService = :()
HttpService = :()
Players = :()
TeleportService = :()
MarketplaceService = :()
StatsService = :()
LocalPlayer = Players.LocalPlayer
Library = {}
Library.Flags = {}
Library.Windows = {}
(source, map)
opts = {}
(source) ==
opts.Name = source
(source) ==
key, value (source)
opts[key] = value
from, to (map {})
opts[to] == opts[from] ~=
opts[to] = opts[from]
opts.Range
opts.Min = opts.Min opts.Range[]
opts.Max = opts.Max opts.Range[]
opts
(tab, opts, element, frame, kind)
element._type = kind
element._frame = frame
element._listeners = element._listeners {}
window = tab tab.Window
searchText = opts.Name opts.Text opts.Title
element._searchName = (searchText) == searchText
element._searchKey = .(element._searchName)
tab tab._items
.(tab._items, element)
opts.Flag
Library.Flags[opts.Flag] = element
window
window:(opts.Flag, element)
element._userVisible = opts.Visible ~=
element:()
element._userVisible
element:()
element._userVisible (tab tab._userVisible == )
element:(visible)
visible = visible
visible == element._userVisible element._destroyed
element._userVisible = visible
visible
element._onHide
element.()
element.Open (element.SetOpen) ==
element:()
frame
frame.Visible = visible
window
window._controlsDirty =
frame element._userVisible
frame.Visible =
element:(tip)
element._tooltip = tip
frame window element._tooltipBound
element._tooltipBound =
window:(element, frame)
element:()
element._tooltip
opts.Tooltip ~= opts.Tooltip ~=
element:(opts.Tooltip)
element:()
element._destroyed
element._destroyed =
window window._tipOwner == element
window:()
_, disconnect (element._listeners)
()
element._listeners = {}
tab tab._items
index = .(tab._items, element)
index
.(tab._items, index)
opts.Flag Library.Flags[opts.Flag] == element
Library.Flags[opts.Flag] =
window
window._controlsDirty =
frame
frame:()
element
Library.Theme = {
Background = .(, , ),
Surface = .(, , ),
Surface2 = .(, , ),
Surface3 = .(, , ),
Stroke = .(, , ),
StrokeHover = .(, , ),
Accent = .(, , ),
AccentDark = .(, , ),
Text = .(, , ),
Muted = .(, , ),
Warning = .(, , ),
Success = .(, , ),
Error = .(, , ),
}
THEME_KEYS = {
, , , , , ,
, , , , , , ,
}
(r, g, b)
.(r, g, b)
(background, surface, surface2, surface3, strokeColor, strokeHover, accent, accentDark, text, muted)
{
Background = (.(background)),
Surface = (.(surface)),
Surface2 = (.(surface2)),
Surface3 = (.(surface3)),
Stroke = (.(strokeColor)),
StrokeHover = (.(strokeHover)),
Accent = (.(accent)),
AccentDark = (.(accentDark)),
Text = (.(text)),
Muted = (.(muted)),
}
Library.ThemePresets = {
XYZ = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Midnight = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Ocean = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Rose = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Emerald = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Amber = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Mono = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Nebula = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Synthwave = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Sakura = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Velvet = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Crimson = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Sunset = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Gold = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Cyber = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Toxic = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Matcha = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Aurora = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Frost = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Abyss = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Halloween = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Haunted = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Christmas = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Valentine = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Lunar = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Tropical = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Dracula = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
Coffee = ({ , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }, { , , }),
}
Library.ThemeName =
Library.DefaultTheme =
Library.Assets = {
Shadow = ,
Glow = ,
Logo = ,
}
LUCIDE_URL =
lucideSet =
()
lucideSet ~=
lucideSet
ok, result = (()
source = :(LUCIDE_URL)
(source)()
)
ok (result) ==
lucideSet = result
lucideSet =
( .. (result))
lucideSet
FONT_PRESETS = {
ValleySans = {
Regular = ,
Medium = ,
SemiBold = ,
},
}
FONT_WEIGHTS = {
Regular = { , .FontWeight.Regular },
Medium = { , .FontWeight.Medium },
SemiBold = { , .FontWeight.SemiBold },
Bold = { , .FontWeight.Bold },
}
Library:(opts)
opts = (opts, {})
(writefile) ~= (isfile) ~= (getcustomasset) ~=
()
name = opts.Name
weights = opts.Weights FONT_PRESETS[name]
(weights) ~=
( .. name)
folder = opts.Folder
(()
(isfolder) == (makefolder) == (folder)
(folder)
)
faces = {}
weightName, url (weights)
info = FONT_WEIGHTS[weightName]
info
path = folder .. .. name .. .. weightName ..
ok =
(path)
ok = (()
(path, :(url))
)
ok
.(faces, { name = weightName, weight = info[], style = , assetId = (path) })
( .. weightName .. .. name)
#faces ==
familyPath = folder .. .. name ..
okWrite = (()
(familyPath, HttpService:({ name = name, faces = faces }))
)
okWrite
family = (familyPath)
(weightName, fallback)
info = FONT_WEIGHTS[weightName]
info weights[weightName]
Font.(family, info[])
fallback
Library.Fonts.Regular = (, Library.Fonts.Regular)
Library.Fonts.Medium = (, (, Library.Fonts.Medium))
Library.Fonts.Bold = (, (, Library.Fonts.Bold))
Library:()
() ~=
(value)
(value) ==
.. .(, value)
(value) ==
value:()
.. value
value:() value:() value:() value:() (Assets value == Assets.Logo)
value
(icon)
(icon) ==
(icon.Image) icon.Image, icon.RectOffset, icon.RectSize, , icon.Tint ==
asset = (icon)
asset
asset, , , ,
(icon) ~=
name = icon:(, )
set = ()
set
entry = (set.Icons set.Icons[name]) set[name]
(entry) ==
image = entry.Image
(image) ==
image = .. (image)
sheet = (set.Spritesheets set.Spritesheets[(image)]) image
sheet, entry.ImageRectPosition, entry.ImageRectSize
(entry) ==
entry
( .. name)
BUTTON_HINT_ICON =
FONT_FAMILY =
Library.Fonts = {
Regular = Font.(FONT_FAMILY, .FontWeight.Regular),
Medium = Font.(FONT_FAMILY, .FontWeight.Medium),
Bold = Font.(FONT_FAMILY, .FontWeight.SemiBold),
}
Theme = Library.Theme
Assets = Library.Assets
EMBEDDED_LOGO =
(data)
lookup = {}
alphabet =
i = , #alphabet
lookup[alphabet:(i)] = i -
out = {}
i = , #data,
a, b, c, d = data:(i, i + )
n = lookup[a] * + lookup[b] * + (lookup[c] ) * + (lookup[d] )
x, y, z = n // , n // % , n %
d ~=
out[#out + ] = .(x, y, z)
c ~=
out[#out + ] = .(x, y)
out[#out + ] = .(x)
.(out)
(()
getasset = ((getcustomasset) == getcustomasset)
((getsynasset) == getsynasset)
(writefile) ~= (getasset) ~=
folder =
(isfolder) == (makefolder) == (folder)
(folder)
path = folder ..
png = (EMBEDDED_LOGO)
fresh = (isfile) == (readfile) == (path) (path) == png
fresh
(path, png)
asset = (path)
(asset) == asset ~=
Assets.Logo = asset
)
Fonts = Library.Fonts
TOUCH = UserInputService.TouchEnabled UserInputService.KeyboardEnabled
Library.Touch = TOUCH
SIDEBAR_WIDTH =
HEADER_HEIGHT =
TAB_TILE_HEIGHT =
CARD_HEIGHT = TOUCH
CARD_HEIGHT_DESC = TOUCH
DROPDOWN_OPTION_HEIGHT = TOUCH
CHIP_HEIGHT = TOUCH
PROFILE_HEIGHT =
SEARCH_WIDTH =
STACK_BELOW =
METRICS = {
Card = {
Compact = ,
Height = CARD_HEIGHT,
DescHeight = CARD_HEIGHT_DESC,
Chip = CHIP_HEIGHT,
Option = DROPDOWN_OPTION_HEIGHT,
TextSize = ,
DescSize = ,
TitleHeight = ,
DescGap = ,
DescLine = ,
Slider = CARD_HEIGHT_DESC,
},
Row = {
Compact = ,
Height = TOUCH ,
DescHeight = TOUCH ,
Chip = TOUCH ,
Option = TOUCH ,
TextSize = ,
DescSize = ,
TitleHeight = ,
DescGap = ,
DescLine = ,
Slider = TOUCH ,
},
}
tweenInfoCache = {}
themeBindings = {}
themeHooks = {}
themeLookup = {}
orphanMarks = {}
bindingCount =
sweepAt =
(map, counted)
object (map)
object.Parent ==
orphanMarks[object]
map[object] =
orphanMarks[object] =
counted
bindingCount -=
orphanMarks[object] =
orphanMarks[object] =
()
(themeBindings, )
(themeHooks, )
sweepAt = .(, bindingCount * )
(color)
.(color.R * + ) .. .. .(color.G * + ) .. .. .(color.B * + )
()
.(themeLookup)
index = #THEME_KEYS, , -
name = THEME_KEYS[index]
themeLookup[(Library.Theme[name])] = name
()
(object, props)
property, value (props)
(value) ==
name = themeLookup[(value)]
bound = themeBindings[object]
name
bound
bound = {}
themeBindings[object] = bound
bindingCount +=
bindingCount >= sweepAt
()
bound[property] = name
bound
bound[property] =
shownTheme = .(Library.Theme)
themeDriver =
(object, bound, colors)
property, name (bound)
color = colors[name]
color
object[property] = color
(colors)
object, bound (themeBindings)
(paintBinding, object, bound, colors)
(duration)
themeDriver
themeDriver:()
themeDriver =
from, colors = {}, {}
_, name (THEME_KEYS)
target = Library.Theme[name]
current = shownTheme[name] target
current ~= target
from[name] = current
colors[name] = current
()
_, name (THEME_KEYS)
colors[name] = Library.Theme[name]
shownTheme[name] = Library.Theme[name]
(colors)
duration <= (from) ==
()
elapsed =
themeDriver = RunService.Heartbeat:((deltaTime)
elapsed += deltaTime
elapsed >= duration
themeDriver:()
themeDriver =
()
alpha = - ( - elapsed / duration) ^
name, start (from)
color = start:(Library.Theme[name], alpha)
colors[name] = color
shownTheme[name] = color
(colors)
)
(owner, fn)
themeHooks[owner] = fn
()
(object, props, duration, style, direction, raw)
duration = duration
raw
(object, props)
duration <=
key, value (props)
object[key] = value
style = style .EasingStyle.Quart
direction = direction .EasingDirection.Out
key = duration .. style.Name .. direction.Name
info = tweenInfoCache[key]
info
info = TweenInfo.(duration, style, direction)
tweenInfoCache[key] = info
t = TweenService:(object, info, props)
t:()
t
(className, props, children)
inst = .(className)
key, value (props)
key ~=
inst[key] = value
(inst, props)
children
_, child (children)
child.Parent = inst
props.Parent
inst.Parent = props.Parent
inst
Library.Roundness =
Library._cornerBases = {}
(parent, radius)
radius = radius .(, )
object = (, {
CornerRadius = radius.Scale == .(, .(radius.Offset * Library.Roundness + )) radius,
Parent = parent,
})
radius.Scale ==
Library._cornerBases[object] = radius.Offset
object
Library.(parent, radius)
(, { CornerRadius = radius, Parent = parent })
Library.(pixels)
.(pixels * Library.Roundness + )
(parent, color, transparency, thickness)
(, {
Color = color Theme.Stroke,
Transparency = transparency ,
Thickness = thickness ,
ApplyStrokeMode = .ApplyStrokeMode.Border,
Parent = parent,
})
(parent, left, right, top, bottom)
(, {
PaddingLeft = .(, left ),
PaddingRight = .(, right ),
PaddingTop = .(, top ),
PaddingBottom = .(, bottom ),
Parent = parent,
})
(props)
defaults = {
BackgroundTransparency = ,
TextColor3 = Theme.Text,
TextSize = ,
FontFace = Fonts.Medium,
TextXAlignment = .TextXAlignment.Left,
TextYAlignment = .TextYAlignment.Center,
TextTruncate = .TextTruncate.AtEnd,
}
key, value (props)
defaults[key] = value
(, defaults)
(parent, size, position, transparency, rotation)
image = (, {
AnchorPoint = .(, ),
Position = position,
Size = size,
BackgroundTransparency = ,
Image = Assets.Glow,
ImageColor3 = .(, , ),
ImageTransparency = ,
Visible = ,
ZIndex = ,
Parent = parent,
})
gradient = (, {
Rotation = rotation ,
Parent = image,
})
(gradient, ()
gradient.Color = ColorSequence.(.(, , ), Theme.Accent)
)
image
(parent)
(, {
Position = .(, ),
Size = .(, , , ),
BackgroundColor3 = .(, , ),
BackgroundTransparency = ,
BorderSizePixel = ,
ZIndex = ,
Parent = parent,
})
CUSTOM_ICON_SCALE =
(imageLabel, icon, fixedSize)
image, rectOffset, rectSize, custom, tint = (icon)
image
imageLabel.Image = image
imageLabel.ImageRectOffset = rectOffset .zero
imageLabel.ImageRectSize = rectSize .zero
icon == Assets.Logo
tint =
keepColours = custom tint
imageLabel:(, keepColours )
keepColours
bound = themeBindings[imageLabel]
bound
bound.ImageColor3 =
(imageLabel, { ImageColor3 = .(, , ) }, )
scale = custom fixedSize icon ~= Assets.Logo ((icon) == (icon.Scale) CUSTOM_ICON_SCALE)
scaler = imageLabel:()
scale ~=
scaler = scaler (, { Name = , Parent = imageLabel })
scaler.Scale = scale
scaler
scaler:()
(imageLabel, color, active, duration)
imageLabel:()
(imageLabel, { ImageTransparency = active }, duration)
(imageLabel, { ImageColor3 = color }, duration)
()
hidden
(()
(gethui) ==
hidden = ()
)
(hidden) ==
hidden
core
(()
core = :()
probe = .()
probe.Parent = core
probe:()
)
core
core
LocalPlayer:()
insetFrame, insetValue = , .zero
()
frame = ()
insetFrame ~= frame
insetFrame = frame
insetValue = GuiService:()
UserInputService:() - insetValue
(input)
input.UserInputType == .UserInputType.MouseButton1 input.UserInputType == .UserInputType.Touch
()
UserInputService:() == .UserInputType.Touch
(button, getScroller)
startCanvas
button.InputBegan:((input)
(input)
frame = ()
startCanvas = frame frame.CanvasPosition
)
()
frame = ()
(startCanvas frame (frame.CanvasPosition - startCanvas).Magnitude > )
(point, object)
position, size = object.AbsolutePosition, object.AbsoluteSize
point.X >= position.X point.X <= position.X + size.X
point.Y >= position.Y point.Y <= position.Y + size.Y
(input)
input.UserInputType == .UserInputType.MouseMovement input.UserInputType == .UserInputType.Touch
(parent, icon, color, position)
holder = (, {
AnchorPoint = .(, ),
Position = position,
Size = .(, ),
BackgroundTransparency = ,
Parent = parent,
})
image = (, {
Size = .(, ),
BackgroundTransparency = ,
ImageColor3 = color,
ScaleType = .ScaleType.Fit,
Parent = holder,
})
(image, icon)
holder, image
(parent, anchorX, offsetX)
holder = (, {
AnchorPoint = .(anchorX, ),
Position = .(anchorX, offsetX, , ),
Size = .(, ),
BackgroundTransparency = ,
Parent = parent,
})
indicator = {}
image = (, {
AnchorPoint = .(, ),
Position = .(, ),
Size = .(, ),
BackgroundTransparency = ,
ImageColor3 = Theme.Muted,
ScaleType = .ScaleType.Fit,
Parent = holder,
})
(image, )
image.Image ~=
indicator:(open)
(image, {
Rotation = open ,
ImageColor3 = open Theme.Accent Theme.Muted,
}, , .EasingStyle.Quint)
indicator
image:()
(rotation, transparency)
({
AnchorPoint = .(, ),
Position = .(, ),
Size = .(, ),
Text = ,
TextSize = ,
TextXAlignment = .TextXAlignment.Center,
TextColor3 = Theme.Muted,
TextTransparency = transparency,
Rotation = rotation,
Parent = holder,
})
down = (, )
up = (, )
indicator:(open)
(down, { TextTransparency = open }, )
(up, { TextTransparency = open , TextColor3 = open Theme.Accent Theme.Muted }, )
indicator
ripplePool = {}
(button)
mouse = ()
diameter = .(button.AbsoluteSize.X, button.AbsoluteSize.Y) *
circle = .(ripplePool)
circle (()
circle.Parent =
)
circle =
circle
circle = (, {
AnchorPoint = .(, ),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = ,
})
(circle, .(, ))
circle.Position = .(mouse.X - button.AbsolutePosition.X, mouse.Y - button.AbsolutePosition.Y)
circle.Size = .(, )
circle.BackgroundColor3 = Theme.Accent
circle.BackgroundTransparency =
circle.ZIndex = button.ZIndex +
circle.Parent = button
(circle, { Size = .(diameter, diameter), BackgroundTransparency = }, )
.(, ()
circle.Parent ~= button button.Parent
(circle.Destroy, circle)
circle.Parent =
#ripplePool <
.(ripplePool, circle)
circle:()
)
(strokeObject)
strokeObject strokeObject:()
(strokeObject, { Color = Theme.Accent, Transparency = }, )
.(, ()
(strokeObject, { Color = Theme.Stroke, Transparency = }, )
)
(target, strokeObject)
target.MouseEnter:(()
(strokeObject, { Color = Theme.StrokeHover }, )
)
target.MouseLeave:(()
(strokeObject, { Color = Theme.Stroke }, )
)
KEY_NAMES = {
[.KeyCode.LeftControl] = ,
[.KeyCode.RightControl] = ,
[.KeyCode.LeftShift] = ,
[.KeyCode.RightShift] = ,
[.KeyCode.LeftAlt] = ,
[.KeyCode.RightAlt] = ,
[.KeyCode.Return] = ,
[.KeyCode.Escape] = ,
[.KeyCode.Backspace] = ,
}
(keyCode)
keyCode ==
KEY_NAMES[keyCode] keyCode.Name
(callback, ...)
(callback) ~=
ok, err = (callback, ...)
ok
( .. (err))
(callback)
result =
.(()
result = .((callback))
)
result ==
.()
.(result, , result.n)
(min, max, step)
decimals =
text = (step)
dot = text:()
dot
decimals = #text - dot
pattern = .. decimals ..
{
snap = (value)
value = .(value / step + ) * step
.(value, min, max)
,
format = (value)
.(pattern, value)
,
}
(tab)
tab._metrics METRICS.Card
(frame, window)
width = frame.AbsoluteSize.X / window.Scale.Scale
.(.(width * ), , )
(tab, className, height, opts)
compact = (tab).Compact
props = {
Size = .(, , , height),
BackgroundColor3 = Theme.Surface2,
BackgroundTransparency = compact ,
BorderSizePixel = ,
LayoutOrder = tab:(),
Parent = tab.List,
}
className ==
props.AutoButtonColor =
props.Text =
frame = (className, props)
frame:SetAttribute("NoDrag", true)
corner(frame)
local frameStroke = stroke(frame, Theme.Stroke, compact and 1 or 0)
if compact then
frameStroke:SetAttribute("Hidden", true)
end
return frame, frameStroke
end
local function titleDesc(parent, name, desc, rightReserve, metrics)
metrics = metrics or METRICS.Card
local title, descLabel
if desc then
local top = math.floor((metrics.DescHeight - (metrics.DescGap + metrics.DescLine)) / 2)
title = label({
Position = UDim2.fromOffset(14, top),
Size = UDim2.new(1, -(14 + rightReserve), 0, metrics.TitleHeight),
Text = name,
TextSize = metrics.TextSize,
Parent = parent,
})
descLabel = label({
Position = UDim2.fromOffset(14, top + metrics.DescGap),
Size = UDim2.new(1, -(14 + rightReserve), 0, metrics.DescLine),
Text = desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = parent,
})
else
title = label({
Position = UDim2.fromOffset(14, 0),
Size = UDim2.new(1, -(14 + rightReserve), 1, 0),
Text = name,
TextSize = metrics.TextSize,
Parent = parent,
})
end
return title, descLabel
end
local function beginElement(tab, opts, map, className, rightReserve, fallbackName)
opts = normalize(opts, map)
local metrics = metricsOf(tab)
local height = opts.Desc and metrics.DescHeight or metrics.Height
local frame, frameStroke = card(tab, className or "Frame", height, opts)
hoverStroke(frame, frameStroke)
local title, descLabel
if rightReserve then
title, descLabel = titleDesc(frame, opts.Name or fallbackName, opts.Desc, rightReserve, metrics)
end
return opts, frame, frameStroke, height, title, descLabel
end
local function reserveRight(title, descLabel, width)
if title then
title.Size = UDim2.new(1, -(14 + width), title.Size.Y.Scale, title.Size.Y.Offset)
end
if descLabel then
descLabel.Size = UDim2.new(1, -(14 + width), 0, descLabel.Size.Y.Offset)
end
end
local Tab = {}
Tab.__index = Tab
function Tab:_nextOrder()
self._order = self._order + 1
return self._order
end
function Tab:Section(text)
local visible = not (type(text) == "table" and text.Visible == false)
if type(text) == "table" then
text = text.Name or text.Title or ""
end
local inset = metricsOf(self).Compact and 14 or 2
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, 28),
BackgroundTransparency = 1,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
local text_ = label({
Position = UDim2.fromOffset(inset, 8),
Size = UDim2.new(0, 0, 0, 16),
AutomaticSize = Enum.AutomaticSize.X,
Text = string.upper(text),
TextSize = 12,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = holder,
})
local ruleInset = inset == 2 and 0 or inset
local rule = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -ruleInset, 0, 16),
Size = UDim2.new(1, -12, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = holder,
})
local function fitRule()
rule.Size = UDim2.new(1, -(text_.AbsoluteSize.X / self.Window.Scale.Scale + inset + 12 + ruleInset), 0, 1)
end
text_:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitRule)
task.defer(fitRule)
return finishElement(self, { Visible = visible }, {
Set = function(_, value)
text_.Text = string.upper(value)
end,
}, holder, "Section")
end
function Tab:Divider(opts)
local caption = type(opts) == "table" and (opts.Text or opts.Name) or type(opts) == "string" and opts or nil
if caption then
-- A centred caption with a rule on each side.
local inset = metricsOf(self).Compact and 14 or 2
local holder = create("Frame", {
Size = UDim2.new(1, 0, 0, 18),
BackgroundTransparency = 1,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
local text_ = label({
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(0, 0, 0, 14),
AutomaticSize = Enum.AutomaticSize.X,
Text = caption,
TextSize = 11,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextTruncate = Enum.TextTruncate.None,
Parent = holder,
})
padding(text_, 8, 8)
local rules = {}
for index, anchor in ipairs({ 0, 1 }) do
rules[index] = create("Frame", {
AnchorPoint = Vector2.new(anchor, 0.5),
Position = UDim2.new(anchor, anchor == 0 and inset or -inset, 0.5, 0),
Size = UDim2.new(0.5, -inset, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = holder,
})
end
local function fitRules()
local half = text_.AbsoluteSize.X / self.Window.Scale.Scale / 2
for _, rule in ipairs(rules) do
rule.Size = UDim2.new(0.5, -(inset + half), 0, 1)
end
end
text_:GetPropertyChangedSignal("AbsoluteSize"):Connect(fitRules)
task.defer(fitRules)
return finishElement(self, { Visible = not (type(opts) == "table" and opts.Visible == false) }, {
Set = function(_, value)
text_.Text = tostring(value)
end,
}, holder, "Divider")
end
local line = create("Frame", {
Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
if metricsOf(self).Compact then
line.BackgroundTransparency = 1
line.Size = UDim2.new(1, 0, 0, 9)
create("Frame", {
Position = UDim2.fromOffset(14, 4),
Size = UDim2.new(1, -28, 0, 1),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
Parent = line,
})
end
return finishElement(self, { Visible = not (type(opts) == "table" and opts.Visible == false) }, {}, line, "Divider")
end
function Tab:Label(opts)
opts = normalize(opts, { Name = "Text", Title = "Text" })
local text_ = label({
Size = UDim2.new(1, 0, 0, 18),
Text = Library._rich(opts.Text or ""),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Library._color(opts.Color) or Theme.Muted,
RichText = true,
LayoutOrder = self:_nextOrder(),
Parent = self.List,
})
if metricsOf(self).Compact then
text_.TextSize = 12
padding(text_, 14, 14)
else
padding(text_, 2)
end
local handle = {
Set = function(_, value)
text_.Text = Library._rich(value)
end,
Get = function()
return text_.Text
end,
SetColor = function(_, color)
tween(text_, { TextColor3 = Library._color(color) or Theme.Muted }, 0.25)
end,
}
if type(opts.Update) == "function" then
local rate = math.max(tonumber(opts.UpdateRate) or 1, 0.05)
local running = true
handle._listeners = handle._listeners or {}
table.insert(handle._listeners, function()
running = false
end)
task.spawn(function()
while running and text_.Parent do
local ok, value = isolatedCall(opts.Update)
if ok and value ~= nil then
text_.Text = Library._rich(value)
elseif not ok then
warn("[AirFlow] label update error: " .. tostring(value))
end
task.wait(rate)
end
end)
function handle:SetUpdateRate(seconds)
rate = math.max(tonumber(seconds) or rate, 0.05)
end
end
return finishElement(self, opts, handle, text_, "Label")
end
function Tab:Paragraph(opts)
opts = normalize(opts, { Title = "Name" })
local frame = card(self, "Frame", 0, opts)
frame.AutomaticSize = Enum.AutomaticSize.Y
if metricsOf(self).Compact then
padding(frame, 14, 14, 6, 8)
else
padding(frame, 14, 14, 11, 12)
end
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
Parent = frame,
})
label({
Size = UDim2.new(1, 0, 0, 14),
Text = opts.Name or "",
LayoutOrder = 1,
Parent = frame,
})
local content = label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = Library._rich(opts.Content or ""),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
TextYAlignment = Enum.TextYAlignment.Top,
RichText = true,
LayoutOrder = 2,
Parent = frame,
})
return finishElement(self, opts, {
Set = function(_, value)
content.Text = Library._rich(value)
end,
}, frame, "Paragraph")
end
local function rowButton(tab, opts)
local primary = opts.Style == "Primary"
local boxHeight = TOUCH and 34 or 28
local height = boxHeight + 6 + (opts.Desc and 16 or 0)
local button = card(tab, "TextButton", height, opts)
local box = create("Frame", {
Position = UDim2.fromOffset(14, 3),
Size = UDim2.new(1, -28, 0, boxHeight),
BackgroundColor3 = primary and Theme.Accent or Theme.Surface3,
BackgroundTransparency = primary and 0.12 or 0.45,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = button,
})
corner(box, UDim.new(0, 6))
local boxStroke = stroke(box, primary and Theme.Accent or Theme.Stroke, primary and 0.4 or 0)
local row = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Parent = box,
})
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = row,
})
local textColor = primary and Theme.AccentDark or Theme.Text
if opts.Icon then
local image = create("ImageLabel", {
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = primary and Theme.AccentDark or Theme.Accent,
ScaleType = Enum.ScaleType.Fit,
LayoutOrder = 1,
Parent = row,
})
applyIcon(image, opts.Icon)
end
local title = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = opts.Name or "Button",
TextSize = 13,
TextColor3 = textColor,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = row,
})
if opts.Desc then
label({
Position = UDim2.fromOffset(14, boxHeight + 6),
Size = UDim2.new(1, -28, 0, 14),
Text = opts.Desc,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = button,
})
end
local restBg = primary and 0.12 or 0.45
button.MouseEnter:Connect(function()
tween(box, { BackgroundTransparency = primary and 0 or 0.2 }, 0.12)
tween(boxStroke, primary and { Transparency = 0 } or { Color = Theme.StrokeHover }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(box, { BackgroundTransparency = restBg }, 0.25)
tween(boxStroke, primary and { Transparency = 0.4 } or { Color = Theme.Stroke }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(box)
safeCall(opts.Callback)
end)
return finishElement(tab, opts, {
SetText = function(_, value)
title.Text = value
end,
}, button, "Button")
end
function Tab:Button(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc" })
if metricsOf(self).Compact then
return rowButton(self, opts)
end
local primary = opts.Style == "Primary"
local height = opts.Desc and CARD_HEIGHT_DESC or CARD_HEIGHT
local button, buttonStroke = card(self, "TextButton", height, opts)
button.ClipsDescendants = true
if primary then
tween(button, { BackgroundColor3 = Theme.Accent, BackgroundTransparency = 0.12 }, 0)
tween(buttonStroke, { Color = Theme.Accent }, 0)
buttonStroke.Transparency = 0.4
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0 }, 0.12)
tween(buttonStroke, { Transparency = 0 }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.12 }, 0.25)
tween(buttonStroke, { Transparency = 0.4 }, 0.25)
end)
else
hoverStroke(button, buttonStroke)
end
local textColor = primary and Theme.AccentDark or Theme.Text
local iconOffset = 0
if opts.Icon then
glowIcon(button, opts.Icon, primary and Theme.AccentDark or Theme.Accent, UDim2.new(0, 14, 0.5, 0))
iconOffset = 26
end
local title = titleDesc(button, opts.Name or "Button", opts.Desc, 30)
if iconOffset > 0 then
for _, child in ipairs(button:GetChildren()) do
if child:IsA("TextLabel") then
child.Position = child.Position + UDim2.fromOffset(iconOffset, 0)
child.Size = child.Size - UDim2.fromOffset(iconOffset, 0)
end
end
end
if primary then
for _, child in ipairs(button:GetChildren()) do
if child:IsA("TextLabel") then
tween(child, { TextColor3 = textColor }, 0)
end
end
end
glowIcon(button, BUTTON_HINT_ICON, primary and Theme.AccentDark or Theme.Muted, UDim2.new(1, -30, 0.5, 0))
button.MouseButton1Click:Connect(function()
ripple(button)
safeCall(opts.Callback)
end)
return finishElement(self, opts, {
SetText = function(_, value)
title.Text = value
end,
}, button, "Button")
end
function Tab:Toggle(opts)
local button, buttonStroke
opts, button, buttonStroke = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default" }, "TextButton", 56, "Toggle")
local pill = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -14, 0.5, 0),
Size = TOUCH and UDim2.fromOffset(44, 24) or UDim2.fromOffset(36, 20),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = button,
})
corner(pill, UDim.new(1, 0))
stroke(pill, Theme.Stroke)
local knob = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 3, 0.5, 0),
Size = TOUCH and UDim2.fromOffset(18, 18) or UDim2.fromOffset(14, 14),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = pill,
})
corner(knob, UDim.new(1, 0))
local pillGlow = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, 24, 1, 24),
BackgroundTransparency = 1,
Image = Assets.Glow,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ZIndex = 0,
Parent = pill,
})
local self_ = { Value = opts.Default == true }
local function render(animate)
local on = self_.Value
local duration = animate and 0.25 or 0
tween(pill, { BackgroundColor3 = on and Theme.Accent or Theme.Surface3 }, duration)
tween(pillGlow, { ImageTransparency = on and 0.75 or 1 }, duration)
local knobSize = TOUCH and 18 or 14
tween(knob, {
Position = on and UDim2.new(0, (TOUCH and 44 or 36) - 3 - knobSize, 0.5, 0) or UDim2.new(0, 3, 0.5, 0),
BackgroundColor3 = on and Theme.AccentDark or Theme.Muted,
}, duration, Enum.EasingStyle.Back)
if animate then
tween(knob, { Size = UDim2.fromOffset(knobSize + 4, knobSize - 2) }, 0.08)
task.delay(0.08, function()
tween(knob, { Size = UDim2.fromOffset(knobSize, knobSize) }, 0.2, Enum.EasingStyle.Back)
end)
end
end
local fromClick = false
function self_:Set(value, silent)
value = value == true
if value == self_.Value then
return
end
self_.Value = value
render(true)
if not fromClick then
flashStroke(buttonStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
render(false)
button.MouseButton1Click:Connect(function()
fromClick = true
self_:Set(not self_.Value)
fromClick = false
end)
local handle = finishElement(self, opts, self_, button, "Toggle")
if self_.Value then
safeCall(opts.Callback, true)
end
return handle
end
function Tab:Slider(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default", Increment = "Step" })
local min = opts.Min or 0
local max = opts.Max or 100
local step = opts.Step or 1
local suffix = opts.Suffix or ""
local numbers = numberFormat(min, max, step)
local metrics = metricsOf(self)
local compact = metrics.Compact
local height = metrics.Slider
local frame, frameStroke = card(self, "Frame", height, opts)
hoverStroke(frame, frameStroke)
label({
Position = UDim2.fromOffset(14, compact and 5 or 12),
Size = UDim2.new(1, -120, 0, 18),
Text = opts.Name or "Slider",
TextSize = metrics.TextSize,
Parent = frame,
})
local valueChip = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, compact and -14 or -10, 0, compact and 2 or 9),
Size = UDim2.fromOffset(40, 24),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = frame,
})
corner(valueChip, UDim.new(0, 6))
local chipStroke = stroke(valueChip)
local valueLabel = label({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 13,
TextColor3 = Theme.Accent,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Parent = valueChip,
})
local valueBox = create("TextBox", {
Position = UDim2.fromOffset(9, 0),
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
Text = "",
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Medium,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
Visible = false,
Parent = valueChip,
})
create("UISizeConstraint", { MinSize = Vector2.new(14, 0), Parent = valueBox })
local suffixLabel = label({
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
Text = suffix,
TextSize = 13,
TextColor3 = Theme.Accent,
TextTruncate = Enum.TextTruncate.None,
Visible = false,
Parent = valueChip,
})
local chipButton = create("TextButton", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 2,
Parent = valueChip,
})
local editing = false
local function fitChip(instant)
local width
if editing then
width = 9 + valueBox.AbsoluteSize.X / (self.Window.Scale.Scale) + suffixLabel.TextBounds.X + 9
suffixLabel.Position = UDim2.fromOffset(9 + valueBox.AbsoluteSize.X / (self.Window.Scale.Scale), 0)
else
width = valueLabel.TextBounds.X + 18
end
width = math.max(width, 36)
if instant then
valueChip.Size = UDim2.fromOffset(width, 24)
else
tween(valueChip, { Size = UDim2.fromOffset(width, 24) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
if not editing then
fitChip(false)
end
end)
valueBox:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if editing then
fitChip(false)
end
end)
local track = create("Frame", {
Position = UDim2.new(0, 14, 0, height - (compact and 13 or 18)),
Size = UDim2.new(1, -28, 0, 5),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = frame,
})
corner(track, UDim.new(1, 0))
local fill = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
local knobGlow = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(28, 28),
BackgroundTransparency = 1,
Image = Assets.Glow,
ImageColor3 = Theme.Accent,
ImageTransparency = 0.85,
ZIndex = 2,
Parent = track,
})
local knob = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(12, 12),
BackgroundColor3 = Theme.Accent,
BorderSizePixel = 0,
ZIndex = 3,
Parent = track,
})
corner(knob, UDim.new(1, 0))
local hit = create("TextButton", {
Position = UDim2.new(0, 8, 0, height - (compact and 13 or 18) - (TOUCH and 19 or 13)),
Size = UDim2.new(1, -16, 0, TOUCH and 40 or 28),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 4,
Parent = frame,
})
local self_ = { Value = math.clamp(opts.Default or min, min, max) }
local dragging = false
chipButton.MouseEnter:Connect(function()
tween(chipStroke, { Color = Theme.StrokeHover }, 0.12)
end)
chipButton.MouseLeave:Connect(function()
if not valueBox:IsFocused() then
tween(chipStroke, { Color = Theme.Stroke }, 0.2)
end
end)
chipButton.MouseButton1Click:Connect(function()
editing = true
valueBox.Text = numbers.format(self_.Value)
valueLabel.Visible = false
valueBox.Visible = true
suffixLabel.Visible = suffix ~= ""
chipButton.Visible = false
tween(chipStroke, { Color = Theme.StrokeHover }, 0.12)
valueBox:CaptureFocus()
task.defer(fitChip, false)
end)
valueBox.FocusLost:Connect(function()
local typed = tonumber(valueBox.Text)
editing = false
valueBox.Visible = false
suffixLabel.Visible = false
valueLabel.Visible = true
chipButton.Visible = true
tween(chipStroke, { Color = Theme.Stroke }, 0.2)
if typed then
self_:Set(typed)
end
fitChip(false)
end)
hit.MouseEnter:Connect(function()
if not dragging then
tween(knobGlow, { ImageTransparency = 0.78, Size = UDim2.fromOffset(34, 34) }, 0.15)
end
end)
hit.MouseLeave:Connect(function()
if not dragging then
tween(knobGlow, { ImageTransparency = 0.85, Size = UDim2.fromOffset(28, 28) }, 0.2)
end
end)
local snap = numbers.snap
local function fraction()
return (max - min) == 0 and 0 or (self_.Value - min) / (max - min)
end
local function moveTo(frac, duration, style)
style = style or Enum.EasingStyle.Linear
tween(fill, { Size = UDim2.new(frac, 0, 1, 0) }, duration, style)
tween(knob, { Position = UDim2.new(frac, 0, 0.5, 0) }, duration, style)
tween(knobGlow, { Position = UDim2.new(frac, 0, 0.5, 0) }, duration, style)
end
local function render(duration, style)
moveTo(fraction(), duration, style)
valueLabel.Text = numbers.format(self_.Value) .. suffix
end
local function settleTo(previousFrac)
local target = fraction()
local width = math.max(track.AbsoluteSize.X, 1)
local overshoot = (target >= previousFrac and 1 or -1) * (5 / width)
moveTo(math.clamp(target + overshoot, 0, 1), 0.22, Enum.EasingStyle.Quint)
task.delay(0.22, function()
if not dragging then
moveTo(fraction(), 0.18, Enum.EasingStyle.Quint)
end
end)
valueLabel.Text = numbers.format(self_.Value) .. suffix
end
local function bounceKnob()
tween(knob, { Size = UDim2.fromOffset(14, 12) }, 0.08)
tween(knobGlow, { Size = UDim2.fromOffset(32, 32), ImageTransparency = 0.78 }, 0.12)
task.delay(0.1, function()
tween(knob, { Size = UDim2.fromOffset(12, 12) }, 0.25, Enum.EasingStyle.Quint)
tween(knobGlow, { Size = UDim2.fromOffset(28, 28), ImageTransparency = 0.85 }, 0.25)
end)
end
function self_:Set(value, silent)
value = snap(tonumber(value) or min)
if value == self_.Value then
return
end
local previousFrac = fraction()
self_.Value = value
if dragging then
render(0.05)
else
settleTo(previousFrac)
bounceKnob()
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
local function updateFromX(x)
local frac = math.clamp((x - track.AbsolutePosition.X) / track.AbsoluteSize.X, 0, 1)
self_:Set(min + (max - min) * frac)
end
hit.InputBegan:Connect(function(input)
if isPress(input) then
dragging = true
self_.Dragging = true
tween(knob, { Size = UDim2.fromOffset(16, 16) }, 0.15, Enum.EasingStyle.Back)
tween(knobGlow, { Size = UDim2.fromOffset(44, 44), ImageTransparency = 0.7 }, 0.15)
updateFromX(pointerPosition().X)
end
end)
self.Window:_listen("Changed", function(input)
if dragging and isMove(input) then
updateFromX(pointerPosition().X)
end
end, self_)
self.Window:_listen("Ended", function(input)
if dragging and isPress(input) then
dragging = false
self_.Dragging = false
tween(knob, { Size = UDim2.fromOffset(12, 12) }, 0.2)
tween(knobGlow, { Size = UDim2.fromOffset(28, 28), ImageTransparency = 0.85 }, 0.2)
safeCall(opts.OnRelease, self_.Value)
end
end, self_)
self_.Value = snap(self_.Value)
render(0)
task.defer(fitChip, true)
return finishElement(self, opts, self_, frame, "Slider")
end
function Tab:Dropdown(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentOption = "Default", Value = "Default", MultipleOptions = "Multi", Values = "Options" })
if opts.Multi and type(opts.Default) ~= "table" and opts.Default ~= nil then
opts.Default = { opts.Default }
elseif not opts.Multi and type(opts.Default) == "table" then
opts.Default = opts.Default[1]
end
local multi = opts.Multi == true
local options = opts.Options or {}
local frame, frameStroke, height
opts, frame, frameStroke, height = beginElement(self, opts, {}, "Frame")
local metrics = metricsOf(self)
local compact = metrics.Compact
local chipHeight = metrics.Chip
local window = self.Window
local ROW = TOUCH and 40 or 30
local MAX_ROWS = opts.MaxRows or 6
-- Stacked: the chip spans the whole row, with the name (if any) above it.
local stacked = compact and opts.Stacked == true
local stackTop = 2
if stacked then
stackTop = (opts.Name or "") ~= "" and 20 or 2
chipHeight = TOUCH and 34 or 30
height = stackTop + chipHeight + 4
frame.Size = UDim2.new(1, 0, 0, height)
end
local header = create("TextButton", {
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = frame,
})
local titleLabel, descLabel
if not stacked then
titleLabel, descLabel = titleDesc(header, opts.Name or "Dropdown", opts.Desc, 180, metrics)
elseif stackTop > 2 then
titleLabel = label({
Position = UDim2.fromOffset(14, 1),
Size = UDim2.new(1, -28, 0, 16),
Text = opts.Name,
TextSize = metrics.TextSize,
Parent = header,
})
end
local chip = create("Frame", {
AnchorPoint = stacked and Vector2.zero or Vector2.new(1, 0.5),
Position = stacked and UDim2.fromOffset(14, stackTop) or UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(60, chipHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = header,
})
corner(chip, UDim.new(0, 6))
local chipStroke = stroke(chip)
local valueLabel = label({
Position = UDim2.fromOffset(10, 0),
Size = UDim2.new(1, -34, 1, 0),
TextColor3 = Theme.Muted,
TextSize = 13,
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = true,
Parent = chip,
})
local arrow = arrowIndicator(chip, 1, -6)
local measure = label({
Size = UDim2.fromOffset(0, chipHeight),
AutomaticSize = Enum.AutomaticSize.X,
TextSize = 13,
Visible = false,
Parent = chip,
})
local fullText = ""
local function trimmed()
local maxText = (stacked and frame.AbsoluteSize.X / window.Scale.Scale - 28 or compact and rowControlWidth(frame, window) or 190) - 10 - 34
measure.Text = fullText
if measure.TextBounds.X <= maxText then
return fullText
end
local text = fullText
while #text > 1 do
text = text:sub(1, -2)
measure.Text = text .. ".."
if measure.TextBounds.X <= maxText then
return text .. ".."
end
end
return ".."
end
local function fitChip(instant)
if stacked then
chip.Size = UDim2.new(1, -28, 0, chipHeight)
return
end
local width
if compact then
width = rowControlWidth(frame, window)
else
width = math.clamp(valueLabel.TextBounds.X + 10 + 34, 60, 190)
end
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
chip.Size = UDim2.fromOffset(width, chipHeight)
else
tween(chip, { Size = UDim2.fromOffset(width, chipHeight) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
if not compact then
fitChip(false)
end
end)
if compact then
-- The chip follows the row every frame; re-measuring the text waits
-- until the width stops changing so resizing stays cheap.
local lastWidth, pending = 0, false
frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
local width = frame.AbsoluteSize.X
if width == lastWidth then
return
end
lastWidth = width
fitChip(true)
if not pending then
pending = true
task.delay(0.12, function()
pending = false
valueLabel.Text = trimmed()
end)
end
end)
end
-- Popup: floats over the window under the chip, with search, bulk
-- actions for multi selection and a scrolling list of checkbox rows.
-- The popup is a plain clipped frame that unfolds out of the chip, so text
-- stays crisp. The shadow sits outside the clip.
local popup = create("Frame", {
AnchorPoint = Vector2.new(1, 0),
Size = UDim2.fromOffset(220, 0),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 45,
Parent = window.Body,
})
popup:SetAttribute("NoDrag", true)
local popupShadow = create("ImageLabel", {
Position = UDim2.fromOffset(-18, -12),
Size = UDim2.new(1, 36, 1, 36),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
Parent = popup,
})
local panel = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = popup,
})
corner(panel, UDim.new(0, 10))
local panelStroke = stroke(panel, Theme.Stroke, 1)
edgeHighlight(panel)
local inner = create("Frame", {
Size = UDim2.new(1, 0, 0, 120),
BackgroundTransparency = 1,
Parent = panel,
})
padding(inner, 8, 8, 8, 8)
local searchHolder = create("Frame", {
Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = inner,
})
corner(searchHolder, UDim.new(0, 7))
local searchStroke = stroke(searchHolder)
local searchIconHolder, searchIcon = glowIcon(searchHolder, "search", Theme.Muted, UDim2.new(0, 10, 0.5, 0))
searchIconHolder.Size = UDim2.fromOffset(14, 14)
local searchBox = create("TextBox", {
Position = UDim2.fromOffset(32, 0),
Size = UDim2.new(1, -40, 1, 0),
BackgroundTransparency = 1,
Text = "",
PlaceholderText = opts.SearchPlaceholder or "Search",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
ClearTextOnFocus = false,
Parent = searchHolder,
})
searchBox.Focused:Connect(function()
tween(searchStroke, { Color = Theme.StrokeHover }, 0.15)
tween(searchIcon, { ImageColor3 = Theme.Accent }, 0.15)
end)
searchBox.FocusLost:Connect(function()
tween(searchStroke, { Color = Theme.Stroke }, 0.2)
tween(searchIcon, { ImageColor3 = Theme.Muted }, 0.2)
end)
local actions = create("Frame", {
Size = UDim2.new(1, 0, 0, 28),
BackgroundTransparency = 1,
Visible = multi,
Parent = inner,
})
local function actionButton(text, position)
local button = create("TextButton", {
Position = position,
Size = UDim2.new(0.5, -3, 1, 0),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Parent = actions,
})
corner(button, UDim.new(0, 6))
local buttonStroke = stroke(button)
label({
Size = UDim2.fromScale(1, 1),
Text = text,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Center,
Parent = button,
})
button.MouseEnter:Connect(function()
tween(button, { BackgroundTransparency = 0.05 }, 0.15)
tween(buttonStroke, { Color = Theme.StrokeHover }, 0.15)
end)
button.MouseLeave:Connect(function()
tween(button, { BackgroundTransparency = 0.35 }, 0.25)
tween(buttonStroke, { Color = Theme.Stroke }, 0.25)
end)
button.MouseButton1Click:Connect(function()
ripple(button)
end)
return button
end
local selectAll = actionButton(opts.SelectAllText or "Select all", UDim2.fromOffset(0, 0))
local clearAll = actionButton(opts.ClearAllText or "Clear all", UDim2.new(0.5, 3, 0, 0))
local list = create("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, 100),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
-- Sized by hand: AutomaticCanvasSize under the window's UIScale
-- sometimes leaves the canvas short, so the list would not scroll.
CanvasSize = UDim2.new(),
Parent = inner,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = list,
})
local noResults = label({
Size = UDim2.new(1, 0, 0, ROW),
Text = opts.NoResultsText or "No results",
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
LayoutOrder = 0,
Visible = false,
Parent = list,
})
local self_ = { Open = false }
local selected = {}
local optionButtons = {}
local filter = ""
local function matches(option)
return filter == "" or string.find(string.lower(tostring(option)), filter, 1, true) ~= nil
end
-- Option labels are rich text, so the part matching the search can light up.
local function escapeRich(text)
return (text:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"))
end
local function optionText(option)
local text = tostring(option)
local at = filter ~= "" and string.find(string.lower(text), filter, 1, true)
if not at then
return escapeRich(text)
end
local stop = at + #filter - 1
return escapeRich(text:sub(1, at - 1))
.. '<font color="#' .. Theme.Accent:ToHex() .. '"><b>' .. escapeRich(text:sub(at, stop)) .. "</b></font>"
.. escapeRich(text:sub(stop + 1))
end
if multi then
for _, value in ipairs(opts.Default or {}) do
selected[value] = true
end
elseif opts.Default ~= nil then
selected[opts.Default] = true
end
local function currentValue()
if multi then
local picked = {}
for _, option in ipairs(options) do
if selected[option] then
table.insert(picked, option)
end
end
return picked
end
for _, option in ipairs(options) do
if selected[option] then
return option
end
end
return nil
end
-- A picked row rests on a faint accent tint; hovering lifts either kind.
local function restRow(entry, duration)
local on, hover = entry.On == true, entry.Hover == true
tween(entry.Frame, {
BackgroundColor3 = on and Theme.Accent or Theme.Surface3,
BackgroundTransparency = on and (hover and 0.8 or 0.88) or (hover and 0.55 or 1),
}, duration)
tween(entry.Label, { TextColor3 = (on or hover) and Theme.Text or Theme.Muted }, duration)
end
local function paintOption(button, animate)
local on = selected[button.Option] == true
if button.On == on and animate then
return
end
button.On = on
local duration = animate and 0.18 or 0
restRow(button, duration)
if multi then
tween(button.Box, { BackgroundColor3 = on and Theme.Accent or Theme.Surface2 }, duration)
tween(button.BoxStroke, { Color = on and Theme.Accent or Theme.Stroke }, duration)
else
tween(button.Bar, {
BackgroundTransparency = on and 0 or 1,
Size = UDim2.fromOffset(3, on and ROW - 14 or 4),
}, animate and 0.3 or 0, Enum.EasingStyle.Quint)
end
tween(button.Check, { ImageTransparency = on and 0 or 1 }, duration)
if animate and on then
button.CheckScale.Scale = 0.5
tween(button.CheckScale, { Scale = 1 }, 0.3, Enum.EasingStyle.Back)
else
button.CheckScale.Scale = 1
end
end
local function renderValue(animate)
local value = currentValue()
if #options == 0 then
fullText = opts.EmptyText or "None"
elseif multi then
-- Two names at most, then a count, so a long pick stays readable.
if #value > 2 then
fullText = tostring(value[1]) .. ", " .. tostring(value[2]) .. " +" .. (#value - 2)
else
fullText = #value > 0 and table.concat(value, ", ") or (opts.NoneText or "None")
end
else
fullText = value ~= nil and tostring(value) or (opts.NoneText or "None")
end
valueLabel.Text = trimmed()
-- A picked value reads in full colour; "None" stays muted.
local picked = #options > 0 and (if multi then #value > 0 else value ~= nil)
tween(valueLabel, { TextColor3 = picked and Theme.Text or Theme.Muted }, animate and 0.15 or 0)
for _, button in pairs(optionButtons) do
paintOption(button, animate)
end
end
local function visibleCount()
local count = 0
for _, option in ipairs(options) do
if matches(option) then
count += 1
end
end
return count
end
local searchShown = true
local targetSize = Vector2.new(220, 120)
local opened = false
local function layoutPopup(instant)
searchShown = opts.Searchable ~= false and #options > (opts.SearchAfter or 0)
searchHolder.Visible = searchShown
local top = 0
if searchShown then
top += 38
end
actions.Position = UDim2.fromOffset(0, top)
if multi and #options > 0 then
actions.Visible = true
top += 34
else
actions.Visible = false
end
local count = visibleCount()
noResults.Visible = count == 0
local rows = math.clamp(count, 1, MAX_ROWS)
local listHeight = rows * ROW + (rows - 1) * 2
list.CanvasSize = UDim2.fromOffset(0, math.max(count, 1) * (ROW + 2) - 2)
local scale = window.Scale.Scale
-- Shrink the list to whichever side of the chip has more room, so the
-- popup never runs off a short window (phones in landscape).
local host = window:_popupHost(frame)
local bodyHeight = host.AbsoluteSize.Y / scale
local chipTop = (chip.AbsolutePosition.Y - host.AbsolutePosition.Y) / scale
local room = math.max(bodyHeight - (chipTop + chip.AbsoluteSize.Y / scale) - 14, chipTop - 14) - top - 16
if room >= ROW then
listHeight = math.min(listHeight, room)
end
list.Position = UDim2.fromOffset(0, top)
list.Size = UDim2.new(1, 0, 0, listHeight)
local width = math.max(math.floor(chip.AbsoluteSize.X / scale), opts.PopupWidth or 210)
local bodyWidth = host.AbsoluteSize.X / scale
width = math.min(width, math.max(bodyWidth - 16, 120))
targetSize = Vector2.new(width, top + listHeight + 16)
inner.Size = UDim2.new(1, 0, 0, targetSize.Y)
if not opened then
return
end
if instant then
popup.Size = UDim2.fromOffset(targetSize.X, targetSize.Y)
else
tween(popup, { Size = UDim2.fromOffset(targetSize.X, targetSize.Y) }, 0.22, Enum.EasingStyle.Quint)
end
end
local function applyFilter()
for option, button in pairs(optionButtons) do
local shown = matches(option)
button.Frame.Visible = shown
if shown then
button.Label.Text = optionText(option)
end
end
end
local scroller = nil
local function place()
local body = window:_popupHost(frame)
local scale = window.Scale.Scale
if scroller then
local centre = chip.AbsolutePosition + chip.AbsoluteSize / 2
if not pointInside(centre, scroller) then
self_:SetOpen(false)
return
end
end
local bodySize = body.AbsoluteSize / scale
local chipPosition = (chip.AbsolutePosition - body.AbsolutePosition) / scale
local chipSize = chip.AbsoluteSize / scale
local right = math.clamp(chipPosition.X + chipSize.X, targetSize.X + 8, math.max(bodySize.X - 8, targetSize.X + 8))
local below = chipPosition.Y + chipSize.Y + 6
-- Opening upward pins the content to the bottom edge so it unfolds from the chip.
if below + targetSize.Y > bodySize.Y - 8 and chipPosition.Y - 6 - targetSize.Y >= 8 then
popup.AnchorPoint = Vector2.new(1, 1)
popup.Position = UDim2.fromOffset(right, chipPosition.Y - 6)
inner.AnchorPoint = Vector2.new(0, 1)
inner.Position = UDim2.fromScale(0, 1)
else
popup.AnchorPoint = Vector2.new(1, 0)
popup.Position = UDim2.fromOffset(right, below)
inner.AnchorPoint = Vector2.zero
inner.Position = UDim2.fromScale(0, 0)
end
end
local stopFollowing = nil
local closeGeneration = 0
-- The page behind stops scrolling while the popup is open, so wheel and
-- swipe input always reach the list instead of dragging the page away.
local lockedScroller = nil
local function unlockScroller()
if lockedScroller then
lockedScroller.ScrollingEnabled = true
lockedScroller = nil
end
end
local function scrollToSelected()
list.CanvasPosition = Vector2.zero
if multi then
return
end
local value = currentValue()
if value == nil then
return
end
for index, option in ipairs(options) do
if option == value then
list.CanvasPosition = Vector2.new(0, math.max(0, (index - 2) * (ROW + 2)))
return
end
end
end
local function setOpen(open)
if open == self_.Open then
return
end
self_.Open = open
arrow:Set(open)
tween(chipStroke, { Color = open and Theme.Accent or Theme.Stroke, Transparency = open and 0.35 or 0 }, 0.2)
tween(chip, { BackgroundColor3 = open and Theme.Surface2 or Theme.Surface }, 0.2)
closeGeneration += 1
if open then
if window._closePopup and window._closePopup ~= self_._close then
window._closePopup()
end
window._closePopup = self_._close
scroller = frame:FindFirstAncestorWhichIsA("ScrollingFrame")
if scroller and scroller.ScrollingEnabled then
lockedScroller = scroller
scroller.ScrollingEnabled = false
end
-- Drawn over a floating panel when the dropdown lives in one.
popup.Parent = window:_popupHost(frame)
filter = ""
searchBox.Text = ""
searchBox.PlaceholderText = opts.SearchPlaceholder or (#options > 6 and ("Search " .. #options .. " options") or "Search")
applyFilter()
layoutPopup(true)
renderValue(false)
scrollToSelected()
opened = true
popup.Size = UDim2.fromOffset(targetSize.X, 0)
place()
popup.Visible = true
tween(popup, { Size = UDim2.fromOffset(targetSize.X, targetSize.Y) }, 0.3, Enum.EasingStyle.Quint)
tween(panelStroke, { Transparency = 0 }, 0.12)
tween(popupShadow, { ImageTransparency = 0.4 }, 0.3)
if not stopFollowing then
stopFollowing = window:_listen("Render", place)
end
else
if window._closePopup == self_._close then
window._closePopup = nil
end
unlockScroller()
if searchBox:IsFocused() then
searchBox:ReleaseFocus()
end
opened = false
tween(popup, { Size = UDim2.fromOffset(targetSize.X, 0) }, 0.2, Enum.EasingStyle.Quint)
tween(popupShadow, { ImageTransparency = 1 }, 0.16)
local generation = closeGeneration
task.delay(0.14, function()
if closeGeneration == generation and not self_.Open then
tween(panelStroke, { Transparency = 1 }, 0.06)
end
end)
task.delay(0.21, function()
if closeGeneration == generation and not self_.Open then
popup.Visible = false
if stopFollowing then
stopFollowing()
stopFollowing = nil
end
end
end)
end
end
self_._close = function()
setOpen(false)
end
local function commit()
renderValue(true)
safeCall(opts.Callback, currentValue())
end
local function buildOptions()
local keep = {}
for index, option in ipairs(options) do
keep[option] = index
end
for option, button in pairs(optionButtons) do
if not keep[option] then
button.Frame:Destroy()
optionButtons[option] = nil
end
end
for index, option in ipairs(options) do
local existing = optionButtons[option]
if existing then
existing.Frame.LayoutOrder = index
continue
end
local row = create("TextButton", {
Size = UDim2.new(1, -6, 0, ROW),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
LayoutOrder = index,
Parent = list,
})
corner(row, UDim.new(0, 7))
-- Multi selection ticks a checkbox on the left. A single pick
-- gets an accent bar on the left edge and a check on the right.
local box, boxStroke, bar, check
if multi then
box = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 8, 0.5, 0),
Size = UDim2.fromOffset(18, 18),
BackgroundColor3 = Theme.Surface2,
BorderSizePixel = 0,
Parent = row,
})
corner(box, UDim.new(0, 5))
boxStroke = stroke(box, Theme.Stroke)
check = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(12, 12),
BackgroundTransparency = 1,
ImageColor3 = Theme.AccentDark,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = box,
})
else
bar = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 3, 0.5, 0),
Size = UDim2.fromOffset(3, 4),
BackgroundColor3 = Theme.Accent,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Parent = row,
})
corner(bar, UDim.new(1, 0))
check = create("ImageLabel", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -10, 0.5, 0),
Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Accent,
ImageTransparency = 1,
ScaleType = Enum.ScaleType.Fit,
Parent = row,
})
end
applyIcon(check, "check")
local checkScale = create("UIScale", { Parent = check })
local text_ = label({
Position = UDim2.fromOffset(multi and 36 or 14, 0),
Size = UDim2.new(1, multi and -44 or -46, 1, 0),
Text = optionText(option),
TextSize = 13,
TextColor3 = Theme.Muted,
RichText = true,
Parent = row,
})
local entry = { Option = option, Frame = row, Box = box, BoxStroke = boxStroke, Bar = bar, Check = check, CheckScale = checkScale, Label = text_, On = nil, Hover = false }
local isTap = tapGuard(row, function()
return list
end)
row.MouseEnter:Connect(function()
-- Touch fires enter without a matching leave; skip the hover.
if usingTouch() then
return
end
entry.Hover = true
restRow(entry, 0.15)
if boxStroke and not selected[option] then
tween(boxStroke, { Color = Theme.StrokeHover }, 0.15)
end
end)
row.MouseLeave:Connect(function()
entry.Hover = false
restRow(entry, 0.25)
if boxStroke and not selected[option] then
tween(boxStroke, { Color = Theme.Stroke }, 0.25)
end
end)
row.MouseButton1Click:Connect(function()
if not isTap() then
return
end
if selected[option] then
if not multi and opts.AllowNone == false then
setOpen(false)
return
end
selected[option] = nil
else
if not multi then
selected = {}
end
selected[option] = true
end
commit()
if not multi then
setOpen(false)
end
end)
optionButtons[option] = entry
paintOption(entry, false)
end
applyFilter()
if self_.Open then
layoutPopup(false)
end
end
selectAll.MouseButton1Click:Connect(function()
for _, option in ipairs(options) do
if matches(option) then
selected[option] = true
end
end
commit()
end)
clearAll.MouseButton1Click:Connect(function()
selected = {}
commit()
end)
function self_:Set(value, silent)
selected = {}
if multi then
for _, item in ipairs(type(value) == "table" and value or { value }) do
selected[item] = true
end
elseif value ~= nil then
selected[value] = true
end
renderValue(self_.Open)
flashStroke(frameStroke)
if not silent then
safeCall(opts.Callback, currentValue())
end
end
function self_:Get()
return currentValue()
end
local function sameValue(a, b)
if type(a) ~= "table" or type(b) ~= "table" then
return a == b
end
if #a ~= #b then
return false
end
for index, item in ipairs(a) do
if b[index] ~= item then
return false
end
end
return true
end
-- A kept pick only counts once its option exists, so a list filled in after a
-- config load changes the value; the callback runs so scripts see it.
function self_:Refresh(newOptions, keepSelection, silent)
local before = currentValue()
options = newOptions or {}
if not keepSelection then
selected = {}
end
buildOptions()
renderValue(false)
if not silent and not sameValue(before, currentValue()) then
safeCall(opts.Callback, currentValue())
end
end
-- Every pick, including ones whose option isn't in the list right now, so
-- saving never drops them.
function self_:_saveValue()
local picked = currentValue()
if multi then
local seen = {}
for _, item in ipairs(picked) do
seen[item] = true
end
for item in pairs(selected) do
if not seen[item] then
table.insert(picked, item)
end
end
return picked
end
if picked == nil then
picked = next(selected)
end
return picked
end
function self_:SetOpen(open)
setOpen(open == true)
end
local headerTap = tapGuard(header, function()
return frame:FindFirstAncestorWhichIsA("ScrollingFrame")
end)
header.MouseButton1Click:Connect(function()
if headerTap() then
setOpen(not self_.Open)
end
end)
-- The chip answers the pointer before it is opened.
header.MouseEnter:Connect(function()
if not self_.Open and not usingTouch() then
tween(chipStroke, { Color = Theme.StrokeHover }, 0.15)
tween(chip, { BackgroundColor3 = Theme.Surface2 }, 0.15)
end
end)
header.MouseLeave:Connect(function()
if not self_.Open then
tween(chipStroke, { Color = Theme.Stroke }, 0.25)
tween(chip, { BackgroundColor3 = Theme.Surface }, 0.25)
end
end)
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
filter = string.lower(searchBox.Text)
applyFilter()
if self_.Open then
layoutPopup(false)
end
end)
self_._listeners = {
function()
setOpen(false)
unlockScroller()
if stopFollowing then
stopFollowing()
stopFollowing = nil
end
popup:Destroy()
end,
}
window:_listen("Began", function(input)
if not self_.Open then
return
end
if input.KeyCode == Enum.KeyCode.Escape then
setOpen(false)
elseif isPress(input) then
local point = pointerPosition()
if not pointInside(point, popup) and not pointInside(point, header) then
setOpen(false)
end
end
end, self_)
buildOptions()
renderValue(false)
task.defer(fitChip, true)
return finishElement(self, opts, self_, frame, "Dropdown")
end
Library._amountSuffixes = { k = 1e3, m = 1e6, b = 1e9, t = 1e12, qa = 1e15, qi = 1e18 }
function Library._parseAmount(text)
text = string.lower(tostring(text)):gsub("[%s,_%$]", "")
text = text:gsub("kg$", ""):gsub("/s$", "")
local number = tonumber(text)
if number then
return number
end
local digits, suffix = text:match("^(.-)(%a+)$")
local scale = suffix and Library._amountSuffixes[suffix]
number = digits and tonumber(digits)
if number and scale then
return number * scale
end
return nil
end
function Tab:Input(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", PlaceholderText = "Placeholder", CurrentValue = "Default", Value = "Default" }, "Frame", 160, "Input")
local compact = metricsOf(self).Compact
local boxHeight = compact and metricsOf(self).Chip + 2 or 30
local boxHolder = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(170, boxHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
corner(boxHolder, UDim.new(0, 6))
local boxStroke = stroke(boxHolder)
local boxIcon = opts.Icon
if boxIcon then
glowIcon(boxHolder, boxIcon, Theme.Muted, UDim2.new(0, 8, 0.5, 0))
end
local box = create("TextBox", {
Position = UDim2.fromOffset(boxIcon and 30 or 8, 0),
Size = UDim2.new(1, boxIcon and -38 or -16, 1, 0),
BackgroundTransparency = 1,
Text = opts.Default or "",
PlaceholderText = opts.Placeholder or "",
PlaceholderColor3 = Theme.Muted,
TextColor3 = Theme.Text,
TextSize = compact and 13 or 14,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = true,
Parent = boxHolder,
})
boxHolder.ClipsDescendants = true
local lastText = box.Text
local value = opts.Numeric and Library._parseAmount(lastText) or nil
if opts.Numeric and not value then
lastText = ""
end
local focused = false
local textInset = (boxIcon and 30 or 8) + 8
local function fitBox(instant)
local width
if compact then
width = rowControlWidth(frame, self.Window)
else
local cardWidth = frame.AbsoluteSize.X / self.Window.Scale.Scale
local maxWidth = math.clamp(cardWidth - 14 - 110 - 20, 100, 200)
local measured = box.TextBounds.X
if #box.Text == 0 then
measured = math.min(box.TextBounds.X, 90)
end
width = math.clamp(measured + textInset + 12, 90, maxWidth) + (focused and 8 or 0)
width = math.min(width, maxWidth + 8)
end
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
boxHolder.Size = UDim2.fromOffset(width, boxHeight)
else
tween(boxHolder, { Size = UDim2.fromOffset(width, boxHeight) }, 0.18)
end
end
local lastWidth = 0
frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
if frame.AbsoluteSize.X ~= lastWidth then
lastWidth = frame.AbsoluteSize.X
fitBox(true)
end
end)
box:GetPropertyChangedSignal("Text"):Connect(function()
fitBox(false)
end)
box:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitBox(false)
end)
task.defer(fitBox, true)
box.Focused:Connect(function()
focused = true
tween(boxStroke, { Color = Theme.StrokeHover }, 0.15)
fitBox(false)
end)
box.FocusLost:Connect(function(enterPressed)
focused = false
tween(boxStroke, { Color = Theme.Stroke }, 0.15)
fitBox(false)
if opts.Numeric then
local number = Library._parseAmount(box.Text)
if not number then
box.Text = lastText
return
end
lastText, value = box.Text, number
safeCall(opts.Callback, number, enterPressed)
return
end
safeCall(opts.Callback, box.Text, enterPressed)
end)
return finishElement(self, opts, {
Set = function(_, newValue)
box.Text = tostring(newValue)
if opts.Numeric then
local number = Library._parseAmount(box.Text)
if number then
lastText, value = box.Text, number
else
box.Text = lastText
end
end
flashStroke(frameStroke)
end,
Get = function()
if opts.Numeric then
return value
end
return box.Text
end,
_saveValue = opts.Numeric and function()
return lastText
end or nil,
}, frame, "Input")
end
function Tab:Keybind(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentKeybind = "Default", Value = "Default" }, "Frame", 110, "Keybind")
if type(opts.Default) == "string" then
opts.Default = Enum.KeyCode[opts.Default]
end
local compact = metricsOf(self).Compact
local chipHeight = metricsOf(self).Chip
local chip = create("TextButton", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(44, chipHeight),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
ClipsDescendants = true,
Parent = frame,
})
corner(chip, UDim.new(0, 6))
local chipStroke = stroke(chip)
local chipLabel = label({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 13,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Parent = chip,
})
local self_ = { Value = opts.Default, Listening = false }
local function fitChip(instant)
local width = math.max(chipLabel.TextBounds.X + 20, TOUCH and 44 or 36)
reserveRight(titleLabel, descLabel, width + (compact and 24 or 20))
if instant then
chip.Size = UDim2.fromOffset(width, chipHeight)
else
tween(chip, { Size = UDim2.fromOffset(width, chipHeight) }, 0.2)
end
end
chipLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitChip(false)
end)
local function render()
chipLabel.Text = self_.Listening and "..." or keyName(self_.Value)
tween(chipStroke, { Color = self_.Listening and Theme.StrokeHover or Theme.Stroke }, 0.15)
tween(chipLabel, { TextColor3 = self_.Listening and Theme.Accent or Theme.Muted }, 0.15)
end
function self_:Set(keyCode, silent)
local wasListening = self_.Listening
self_.Value = keyCode
self_.Listening = false
render()
if not wasListening then
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.OnChanged, keyCode)
end
end
chip.MouseButton1Click:Connect(function()
self_.Listening = not self_.Listening
render()
end)
self.Window:_listen("Began", function(input, gameProcessed)
if input.UserInputType ~= Enum.UserInputType.Keyboard then
return
end
if self_.Listening then
self.Window._consumedKey = input.KeyCode
self.Window._consumedAt = os.clock()
if input.KeyCode == Enum.KeyCode.Escape then
self_.Listening = false
render()
else
self_:Set(input.KeyCode)
end
return
end
if not gameProcessed and self_.Value ~= nil and input.KeyCode == self_.Value then
safeCall(opts.Callback, input.KeyCode)
end
end, self_)
render()
task.defer(fitChip, true)
function self_:Get()
return self_.Value
end
self_._onHide = function()
if self_.Listening then
self_.Listening = false
render()
end
end
return finishElement(self, opts, self_, frame, "Keybind")
end
function Tab:ColorPicker(opts)
local PANEL_HEIGHT = 194
local frame, frameStroke, height
opts, frame, frameStroke, height = beginElement(self, opts, { Title = "Name", Description = "Desc", Color = "Default", CurrentValue = "Default", Value = "Default" }, "Frame")
frame.ClipsDescendants = true
local metrics = metricsOf(self)
local compact = metrics.Compact
local header = create("TextButton", {
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
Parent = frame,
})
titleDesc(header, opts.Name or "Color", opts.Desc, 140, metrics)
-- Closed, the row shows the hex code beside a rounded swatch.
local swatch = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -38 or -34, 0.5, 0),
Size = UDim2.fromOffset(22, 22),
BorderSizePixel = 0,
Parent = header,
})
corner(swatch, UDim.new(0, 6))
stroke(swatch, Theme.Stroke)
local swatchHex = label({
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -68 or -64, 0.5, 0),
Size = UDim2.fromOffset(64, 16),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Right,
Parent = header,
})
local arrow = arrowIndicator(header, 1, compact and -14 or -12)
local panel = create("Frame", {
Position = UDim2.fromOffset(14, height + 2),
Size = UDim2.new(1, -28, 0, PANEL_HEIGHT - 12),
BackgroundTransparency = 1,
Visible = false,
Parent = frame,
})
-- Saturation runs left to right, brightness top to bottom.
local svBox = create("TextButton", {
Size = UDim2.new(1, 0, 0, 118),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Parent = panel,
})
corner(svBox, UDim.new(0, 8))
stroke(svBox, Theme.Stroke)
local svHueGradient = create("UIGradient", {
Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.new(1, 0, 0)),
Parent = svBox,
})
local svDark = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Parent = svBox,
})
corner(svDark, UDim.new(0, 8))
create("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(1, 0),
}),
Rotation = 90,
Parent = svDark,
})
-- Both handles are a white ring filled with the colour under them, with a
-- faint dark edge so they read on light and dark spots alike.
local function knob(parent, size)
local ring = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(size, size),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
ZIndex = 3,
Parent = parent,
})
corner(ring, UDim.new(1, 0))
stroke(ring, Color3.new(0, 0, 0), 0.7)
local fill = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, -4, 1, -4),
BorderSizePixel = 0,
ZIndex = 3,
Parent = ring,
})
corner(fill, UDim.new(1, 0))
return ring, fill
end
local svCursor, svFill = knob(svBox, 14)
-- Hue is a slim horizontal track under the square.
local hueBar = create("TextButton", {
Position = UDim2.fromOffset(0, 132),
Size = UDim2.new(1, 0, 0, 10),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Text = "",
AutoButtonColor = false,
Parent = panel,
})
corner(hueBar, UDim.new(1, 0))
create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 0, 0)),
ColorSequenceKeypoint.new(1 / 6, Color3.fromRGB(255, 255, 0)),
ColorSequenceKeypoint.new(2 / 6, Color3.fromRGB(0, 255, 0)),
ColorSequenceKeypoint.new(3 / 6, Color3.fromRGB(0, 255, 255)),
ColorSequenceKeypoint.new(4 / 6, Color3.fromRGB(0, 0, 255)),
ColorSequenceKeypoint.new(5 / 6, Color3.fromRGB(255, 0, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 0, 0)),
}),
Parent = hueBar,
})
local hueCursor, hueFill = knob(hueBar, 16)
hueCursor.Position = UDim2.fromScale(0, 0.5)
-- Bottom row: a preview chip and hex field on the left, RGB on the right.
local hexHolder = create("Frame", {
Position = UDim2.fromOffset(0, 154),
Size = UDim2.fromOffset(compact and 110 or 124, 28),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = panel,
})
corner(hexHolder, UDim.new(0, 7))
local hexStroke = stroke(hexHolder)
local preview = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 6, 0.5, 0),
Size = UDim2.fromOffset(16, 16),
BorderSizePixel = 0,
Parent = hexHolder,
})
corner(preview, UDim.new(0, 4))
stroke(preview, Theme.Stroke)
local hexBox = create("TextBox", {
Position = UDim2.fromOffset(30, 0),
Size = UDim2.new(1, -38, 1, 0),
BackgroundTransparency = 1,
Text = "",
TextTruncate = Enum.TextTruncate.None,
ClipsDescendants = false,
TextColor3 = Theme.Text,
TextSize = 13,
FontFace = Fonts.Regular,
TextXAlignment = Enum.TextXAlignment.Left,
ClearTextOnFocus = false,
Parent = hexHolder,
})
local rgbLabel = label({
Position = UDim2.fromOffset(compact and 118 or 134, 154),
Size = UDim2.new(1, compact and -118 or -134, 0, 28),
TextXAlignment = Enum.TextXAlignment.Right,
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = panel,
})
local self_ = { Open = false }
local hue, sat, val = Color3.toHSV(opts.Default or Theme.Accent)
local dragTarget = nil
local function toHex(color)
return string.format("#%02X%02X%02X",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
local function render(duration)
local color = Color3.fromHSV(hue, sat, val)
self_.Value = color
swatch.BackgroundColor3 = color
preview.BackgroundColor3 = color
svFill.BackgroundColor3 = color
hueFill.BackgroundColor3 = Color3.fromHSV(hue, 1, 1)
swatchHex.Text = toHex(color)
svHueGradient.Color = ColorSequence.new(Color3.new(1, 1, 1), Color3.fromHSV(hue, 1, 1))
tween(svCursor, { Position = UDim2.fromScale(sat, 1 - val) }, duration, Enum.EasingStyle.Quint)
tween(hueCursor, { Position = UDim2.fromScale(hue, 0.5) }, duration, Enum.EasingStyle.Quint)
if not hexBox:IsFocused() then
hexBox.Text = toHex(color)
end
rgbLabel.Text = string.format("%d %d %d",
math.floor(color.R * 255 + 0.5),
math.floor(color.G * 255 + 0.5),
math.floor(color.B * 255 + 0.5))
end
local function commit(duration, silent)
render(duration)
if not silent then
safeCall(opts.Callback, self_.Value)
end
end
function self_:Set(color, silent)
hue, sat, val = Color3.toHSV(color)
commit(0.25, silent)
flashStroke(frameStroke)
end
function self_:Get()
return self_.Value
end
local function setOpen(open)
self_.Open = open
tween(frame, { Size = UDim2.new(1, 0, 0, open and (height + PANEL_HEIGHT) or height) }, 0.35, Enum.EasingStyle.Quint)
arrow:Set(open)
if open then
panel.Visible = true
else
task.delay(0.35, function()
if not self_.Open then
panel.Visible = false
end
end)
end
end
function self_:SetOpen(open)
setOpen(open == true)
end
header.MouseButton1Click:Connect(function()
setOpen(not self_.Open)
end)
local function updateSV(position)
sat = math.clamp((position.X - svBox.AbsolutePosition.X) / svBox.AbsoluteSize.X, 0, 1)
val = 1 - math.clamp((position.Y - svBox.AbsolutePosition.Y) / svBox.AbsoluteSize.Y, 0, 1)
commit(0.04)
end
local function updateHue(position)
hue = math.clamp((position.X - hueBar.AbsolutePosition.X) / hueBar.AbsoluteSize.X, 0, 0.999)
commit(0.04)
end
svBox.InputBegan:Connect(function(input)
if isPress(input) then
dragTarget = "sv"
tween(svCursor, { Size = UDim2.fromOffset(18, 18) }, 0.2, Enum.EasingStyle.Quint)
updateSV(pointerPosition())
end
end)
hueBar.InputBegan:Connect(function(input)
if isPress(input) then
dragTarget = "hue"
tween(hueCursor, { Size = UDim2.fromOffset(20, 20) }, 0.2, Enum.EasingStyle.Quint)
updateHue(pointerPosition())
end
end)
self.Window:_listen("Changed", function(input)
if not dragTarget or not isMove(input) then
return
end
local position = pointerPosition()
if dragTarget == "sv" then
updateSV(position)
else
updateHue(position)
end
end, self_)
self.Window:_listen("Ended", function(input)
if dragTarget and isPress(input) then
dragTarget = nil
tween(svCursor, { Size = UDim2.fromOffset(14, 14) }, 0.25, Enum.EasingStyle.Quint)
tween(hueCursor, { Size = UDim2.fromOffset(16, 16) }, 0.25, Enum.EasingStyle.Quint)
end
end, self_)
hexBox.Focused:Connect(function()
tween(hexStroke, { Color = Theme.StrokeHover }, 0.15)
end)
hexBox.FocusLost:Connect(function()
tween(hexStroke, { Color = Theme.Stroke }, 0.15)
local r, g, b = hexBox.Text:match("^%s*#?(%x%x)(%x%x)(%x%x)%s*$")
if r then
self_:Set(Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)))
else
hexBox.Text = toHex(self_.Value)
end
end)
render(0)
return finishElement(self, opts, self_, frame, "ColorPicker")
end
function Tab:Stepper(opts)
local frame, frameStroke, _, titleLabel, descLabel
opts, frame, frameStroke, _, titleLabel, descLabel = beginElement(self, opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default", Increment = "Step" }, "Frame", 150, "Stepper")
local min = opts.Min or 0
local max = opts.Max or 100
local step = opts.Step or 1
local suffix = opts.Suffix or ""
local numbers = numberFormat(min, max, step)
local compact = metricsOf(self).Compact
local chipHeight = metricsOf(self).Chip
local group = create("Frame", {
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, compact and -14 or -10, 0.5, 0),
Size = UDim2.fromOffset(0, chipHeight),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = frame,
})
corner(group, UDim.new(0, 6))
local groupStroke = stroke(group)
create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
Parent = group,
})
local function stepButton(text, order)
local button = create("TextButton", {
Size = UDim2.fromOffset(chipHeight, chipHeight),
BackgroundTransparency = 1,
Text = text,
TextColor3 = Theme.Muted,
TextSize = 18,
FontFace = Fonts.Medium,
AutoButtonColor = false,
LayoutOrder = order,
Parent = group,
})
button.MouseEnter:Connect(function()
tween(button, { TextColor3 = Theme.Accent }, 0.12)
end)
button.MouseLeave:Connect(function()
tween(button, { TextColor3 = Theme.Muted }, 0.2)
end)
return button
end
local minus = stepButton("−", 1)
local valueLabel = label({
Size = UDim2.new(0, 30, 1, 0),
TextSize = 13,
TextColor3 = Theme.Accent,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = group,
})
local plus = stepButton("+", 3)
local function fitValue(instant)
local width = math.max(valueLabel.TextBounds.X + 12, 30)
if instant then
valueLabel.Size = UDim2.new(0, width, 1, 0)
else
tween(valueLabel, { Size = UDim2.new(0, width, 1, 0) }, 0.2)
end
end
valueLabel:GetPropertyChangedSignal("TextBounds"):Connect(function()
fitValue(false)
end)
group:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
reserveRight(titleLabel, descLabel, group.AbsoluteSize.X / self.Window.Scale.Scale + (compact and 24 or 20))
end)
local self_ = { Value = math.clamp(opts.Default or min, min, max) }
local snap = numbers.snap
local fromButton = false
local function render()
valueLabel.Text = numbers.format(self_.Value) .. suffix
tween(minus, { TextTransparency = self_.Value <= min and 0.6 or 0 }, 0.15)
tween(plus, { TextTransparency = self_.Value >= max and 0.6 or 0 }, 0.15)
end
function self_:Set(value, silent)
value = snap(tonumber(value) or min)
if value == self_.Value then
return
end
self_.Value = value
render()
if not fromButton then
flashStroke(frameStroke)
end
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
local function bump(direction)
fromButton = true
self_:Set(self_.Value + direction * step)
fromButton = false
tween(groupStroke, { Color = Theme.StrokeHover }, 0.08)
task.delay(0.12, function()
tween(groupStroke, { Color = Theme.Stroke }, 0.2)
end)
end
local function holdRepeat(button, direction)
local holding = false
button.InputBegan:Connect(function(input)
if not isPress(input) then
return
end
holding = true
bump(direction)
task.delay(0.4, function()
while holding do
bump(direction)
task.wait(0.07)
end
end)
end)
button.InputEnded:Connect(function(input)
if isPress(input) then
holding = false
end
end)
button.MouseLeave:Connect(function()
holding = false
end)
end
holdRepeat(minus, -1)
holdRepeat(plus, 1)
render()
task.defer(fitValue, true)
return finishElement(self, opts, self_, frame, "Stepper")
end
function Tab:Progress(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", CurrentValue = "Default", Value = "Default" })
local metrics = metricsOf(self)
local compact = metrics.Compact
local extra = compact and 6 or 10
local frame, frameStroke = card(self, "Frame", (opts.Desc and metrics.DescHeight or metrics.Height) + extra, opts)
hoverStroke(frame, frameStroke)
local top
if opts.Desc then
top = math.floor((metrics.DescHeight - (metrics.DescGap + metrics.DescLine)) / 2) - 2
else
top = compact and 5 or 12
end
label({
Position = UDim2.fromOffset(14, top),
Size = UDim2.new(1, -110, 0, 18),
Text = opts.Name or "Progress",
TextSize = metrics.TextSize,
Parent = frame,
})
if opts.Desc then
label({
Position = UDim2.fromOffset(14, top + metrics.DescGap),
Size = UDim2.new(1, -110, 0, metrics.DescLine),
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = frame,
})
end
local valueLabel = label({
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -14, 0, top),
Size = UDim2.fromOffset(90, 18),
TextXAlignment = Enum.TextXAlignment.Right,
TextSize = 13,
TextColor3 = Library._color(opts.TextColor) or Theme.Accent,
RichText = true,
Parent = frame,
})
local track = create("Frame", {
Position = UDim2.new(0, 14, 1, compact and -12 or -16),
Size = UDim2.new(1, -28, 0, 5),
BackgroundColor3 = Theme.Surface3,
BorderSizePixel = 0,
Parent = frame,
})
corner(track, UDim.new(1, 0))
local fill = create("Frame", {
Size = UDim2.new(0, 0, 1, 0),
BackgroundColor3 = Library._color(opts.Color) or Theme.Accent,
BorderSizePixel = 0,
Parent = track,
})
corner(fill, UDim.new(1, 0))
local self_ = { Value = math.clamp(opts.Default or 0, 0, 1) }
local formatter = opts.Format
local function render(duration)
local frac = self_.Value
tween(fill, { Size = UDim2.new(frac, 0, 1, 0) }, duration, Enum.EasingStyle.Quint)
if type(formatter) == "function" then
valueLabel.Text = Library._rich(formatter(frac))
else
valueLabel.Text = string.format("%d%%", math.floor(frac * 100 + 0.5))
end
end
function self_:Set(value, silent)
value = math.clamp(tonumber(value) or 0, 0, 1)
if value == self_.Value then
return
end
self_.Value = value
render(0.35)
if not silent then
safeCall(opts.Callback, value)
end
end
function self_:Get()
return self_.Value
end
function self_:SetColor(color)
tween(fill, { BackgroundColor3 = Library._color(color) or Theme.Accent }, 0.25)
end
-- The percentage's colour; nil goes back to the accent.
function self_:SetTextColor(color)
tween(valueLabel, { TextColor3 = Library._color(color) or Theme.Accent }, 0.25)
end
-- Runs render now, and again on a theme change for theme tags in Format.
onTheme(valueLabel, function()
render(0)
end)
return finishElement(self, opts, self_, frame, "Progress")
end
-- An always-open list the player puts in order: drag a row (by its grip on
-- touch, anywhere on PC) or nudge it with the arrows. Past MaxRows the list
-- scrolls, and dragging near its top or bottom edge scrolls it along.
function Tab:OrderList(opts)
opts = normalize(opts, { Title = "Name", Description = "Desc", Options = "Items", Values = "Items" })
local metrics = metricsOf(self)
local compact = metrics.Compact
local window = self.Window
local ROW = TOUCH and 40 or 32
local GAP = 4
local STEP = ROW + GAP
local GRIP = TOUCH and 34 or 26
local ARROW = TOUCH and 30 or 22
local maxRows = math.max(math.floor(tonumber(opts.MaxRows) or 6), 1)
local numbered = opts.Numbered ~= false
local arrows = opts.Arrows ~= false
local frame = card(self, "Frame", 0, opts)
frame.AutomaticSize = Enum.AutomaticSize.Y
if compact then
padding(frame, 14, 14, 6, 8)
else
padding(frame, 14, 14, 11, 12)
end
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
Parent = frame,
})
local name = tostring(opts.Name or "")
label({
Size = UDim2.new(1, 0, 0, 18),
Text = name,
TextSize = metrics.TextSize,
Visible = name ~= "",
LayoutOrder = 1,
Parent = frame,
})
if opts.Desc then
label({
Size = UDim2.new(1, 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
Text = opts.Desc,
TextSize = metrics.DescSize,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextWrapped = true,
TextTruncate = Enum.TextTruncate.None,
LayoutOrder = 2,
Parent = frame,
})
end
local scroller = create("ScrollingFrame", {
Size = UDim2.new(1, 0, 0, ROW + 2),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = TOUCH and 4 or 3,
ScrollBarImageColor3 = Theme.Accent,
ScrollBarImageTransparency = 0.4,
ScrollingDirection = Enum.ScrollingDirection.Y,
-- Sized by hand, like the dropdown list, so it scrolls under UIScale.
CanvasSize = UDim2.new(),
ScrollingEnabled = false,
LayoutOrder = 3,
Parent = frame,
})
scroller:SetAttribute("NoDrag", true)
local content = create("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Parent = scroller,
})
local empty = label({
Size = UDim2.new(1, 0, 0, ROW),
Text = tostring(opts.EmptyText or "Nothing to order"),
TextSize = 13,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
Visible = false,
Parent = content,
})
-- A lucide icon, or a text glyph when the icon set didn't load.
local function iconButton(parent, icon, glyph, rotation, props)
props.BackgroundTransparency = 1
props.Text = ""
props.AutoButtonColor = false
props.Parent = parent
local button = create("TextButton", props)
local image = create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = TOUCH and UDim2.fromOffset(16, 16) or UDim2.fromOffset(14, 14),
BackgroundTransparency = 1,
ImageColor3 = Theme.Muted,
ScaleType = Enum.ScaleType.Fit,
Parent = button,
})
applyIcon(image, icon)
if image.Image ~= "" then
return button, { Object = image, Color = "ImageColor3", Fade = "ImageTransparency" }
end
image:Destroy()
local text_ = label({
Size = UDim2.fromScale(1, 1),
Text = glyph,
TextSize = 18,
TextColor3 = Theme.Muted,
TextXAlignment = Enum.TextXAlignment.Center,
TextTruncate = Enum.TextTruncate.None,
Rotation = rotation,
Parent = button,
})
return button, { Object = text_, Color = "TextColor3", Fade = "TextTransparency" }
end
local function tint(icon, color, transparency, duration)
tween(icon.Object, { [icon.Color] = color, [icon.Fade] = transparency }, duration)
end
local self_ = {}
local entries = {}
-- The last order Set asked for, including items not in the list right now,
-- so a saved order still applies once Refresh brings those items in.
local remembered = {}
local dragging = nil
local nudge, beginDrag
local function slotY(slot)
return 1 + (slot - 1) * STEP
end
local function values()
local list = {}
for index, entry in ipairs(entries) do
list[index] = entry.Value
end
return list
end
local function layout(instant)
local count = #entries
for slot, entry in ipairs(entries) do
entry.Index.Text = tostring(slot)
if entry ~= dragging then
tween(entry.Frame, { Position = UDim2.fromOffset(3, slotY(slot)) }, instant and 0 or 0.22, Enum.EasingStyle.Quint)
end
if entry.UpIcon then
tween(entry.UpIcon.Object, { [entry.UpIcon.Fade] = slot == 1 and 0.7 or 0 }, instant and 0 or 0.15)
tween(entry.DownIcon.Object, { [entry.DownIcon.Fade] = slot == count and 0.7 or 0 }, instant and 0 or 0.15)
end
end
end
local function fit()
local count = #entries
local canvas = math.max(count, 1) * STEP - GAP + 2
local shown = math.clamp(count, 1, maxRows)
local height = shown * STEP - GAP + 2
local scrolling = count > maxRows
scroller.Size = UDim2.new(1, 0, 0, height)
scroller.CanvasSize = UDim2.fromOffset(0, canvas)
content.Size = UDim2.new(1, scrolling and -8 or 0, 0, canvas)
if not dragging then
scroller.ScrollingEnabled = scrolling
end
empty.Visible = count == 0
local maxScroll = math.max(canvas - height, 0)
if scroller.CanvasPosition.Y > maxScroll then
scroller.CanvasPosition = Vector2.new(0, maxScroll)
end
end
local function changed(silent)
self_.Value = values()
if not silent then
safeCall(opts.Callback, values())
end
end
local function listScroller()
if scroller.ScrollingEnabled then
return scroller
end
return frame:FindFirstAncestorWhichIsA("ScrollingFrame")
end
local function newEntry(value)
local row = create("Frame", {
Size = UDim2.new(1, -6, 0, ROW),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
Parent = content,
})
corner(row, UDim.new(0, 7))
local entry = { Value = value, Frame = row }
entry.Stroke = stroke(row)
entry.Scale = create("UIScale", { Parent = row })
local grip
grip, entry.GripIcon = iconButton(row, "grip-vertical", "=", 0, {
Size = UDim2.new(0, GRIP, 1, 0),
})
local x = GRIP
entry.Index = label({
Position = UDim2.fromOffset(x, 0),
Size = UDim2.new(0, 22, 1, 0),
TextSize = 12,
TextColor3 = Theme.Accent,
TextTruncate = Enum.TextTruncate.None,
Visible = numbered,
Parent = row,
})
if numbered then
x += 22
end
local right = arrows and ARROW * 2 + 8 or 10
entry.Text = label({
Position = UDim2.fromOffset(x, 0),
Size = UDim2.new(1, -(x + right), 1, 0),
Text = tostring(value),
TextSize = 13,
Parent = row,
})
local buttons = {}
if arrows then
local up, down
up, entry.UpIcon = iconButton(row, "chevron-up", "›", -90, {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -(ARROW + 6), 0, 0),
Size = UDim2.new(0, ARROW, 1, 0),
})
down, entry.DownIcon = iconButton(row, "chevron-down", "›", 90, {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -4, 0, 0),
Size = UDim2.new(0, ARROW, 1, 0),
})
for button, direction in pairs({ [up] = -1, [down] = 1 }) do
local icon = direction < 0 and entry.UpIcon or entry.DownIcon
local isTap = tapGuard(button, listScroller)
button.MouseEnter:Connect(function()
if not usingTouch() then
tween(icon.Object, { [icon.Color] = Theme.Accent }, 0.12)
end
end)
button.MouseLeave:Connect(function()
tween(icon.Object, { [icon.Color] = Theme.Muted }, 0.2)
end)
button.MouseButton1Click:Connect(function()
if isTap() then
nudge(entry, direction)
end
end)
table.insert(buttons, button)
end
end
grip.InputBegan:Connect(function(input)
if isPress(input) then
beginDrag(entry)
end
end)
-- On PC the whole row drags; on touch only the grip does, so a swipe
-- across the rows still scrolls.
row.InputBegan:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
local point = pointerPosition()
for _, button in ipairs(buttons) do
if pointInside(point, button) then
return
end
end
beginDrag(entry)
end)
row.MouseEnter:Connect(function()
if not usingTouch() and entry ~= dragging then
tween(entry.Stroke, { Color = Theme.StrokeHover }, 0.12)
tint(entry.GripIcon, Theme.Text, 0, 0.12)
end
end)
row.MouseLeave:Connect(function()
if entry ~= dragging then
tween(entry.Stroke, { Color = Theme.Stroke }, 0.25)
tint(entry.GripIcon, Theme.Muted, 0, 0.25)
end
end)
return entry
end
-- Reuses the rows whose item is still in the list, so a reorder slides
-- rows instead of rebuilding them.
local function build(list)
local pool = {}
for _, entry in ipairs(entries) do
pool[entry.Value] = pool[entry.Value] or {}
table.insert(pool[entry.Value], entry)
end
local nextEntries = {}
for index, value in ipairs(list) do
local bucket = pool[value]
local entry = bucket and table.remove(bucket, 1)
if not entry then
entry = newEntry(value)
entry.Frame.Position = UDim2.fromOffset(3, slotY(index))
end
nextEntries[index] = entry
end
for _, bucket in pairs(pool) do
for _, entry in ipairs(bucket) do
entry.Frame:Destroy()
end
end
entries = nextEntries
end
-- items sorted by where they sit in ranking; the rest keep their order after.
local function arrange(items, ranking)
local left = {}
for _, value in ipairs(items) do
left[value] = (left[value] or 0) + 1
end
local result = {}
for _, pass in ipairs({ ranking, items }) do
for _, value in ipairs(pass) do
if (left[value] or 0) > 0 then
left[value] -= 1
table.insert(result, value)
end
end
end
return result
end
local function scrollIntoView(slot)
local top = slotY(slot) - 1
local view = scroller.AbsoluteWindowSize.Y / window.Scale.Scale
local y = scroller.CanvasPosition.Y
if top < y then
y = top
elseif top + ROW + 2 > y + view then
y = top + ROW + 2 - view
end
if y ~= scroller.CanvasPosition.Y then
tween(scroller, { CanvasPosition = Vector2.new(0, math.max(y, 0)) }, 0.2)
end
end
function nudge(entry, direction)
if dragging then
return
end
local from = table.find(entries, entry)
local to = from and from + direction
if not to or to < 1 or to > #entries then
return
end
entries[from], entries[to] = entries[to], entries[from]
layout(false)
tween(entry.Stroke, { Color = Theme.Accent }, 0.08)
task.delay(0.18, function()
if entry ~= dragging then
tween(entry.Stroke, { Color = Theme.Stroke }, 0.3)
end
end)
scrollIntoView(to)
changed(false)
end
local grabOffset, lastPointer = 0, nil
local startEntries, stopRender, lockedPage = nil, nil, nil
-- Moves the held row under the pointer and slides the others out of its way.
local function follow(pointer)
local scale = window.Scale.Scale
local y = (pointer.Y - grabOffset - scroller.AbsolutePosition.Y) / scale + scroller.CanvasPosition.Y
y = math.clamp(y, slotY(1), slotY(#entries))
dragging.Frame.Position = UDim2.fromOffset(3, y)
local slot = math.clamp(math.floor((y - 1) / STEP + 0.5) + 1, 1, #entries)
local current = table.find(entries, dragging)
if current ~= slot then
table.remove(entries, current)
table.insert(entries, slot, dragging)
layout(false)
end
end
-- Holding the row near the list's top or bottom edge scrolls it, faster
-- the closer the pointer gets.
local function autoScroll(deltaTime)
if not dragging or not lastPointer then
return
end
local scale = window.Scale.Scale
local top = scroller.AbsolutePosition.Y
local bottom = top + scroller.AbsoluteSize.Y
local zone = ROW * scale
local speed = 0
if lastPointer.Y < top + zone then
speed = -(top + zone - lastPointer.Y) / zone
elseif lastPointer.Y > bottom - zone then
speed = (lastPointer.Y - (bottom - zone)) / zone
end
if speed == 0 then
return
end
local maxScroll = math.max(scroller.CanvasSize.Y.Offset - scroller.AbsoluteWindowSize.Y / scale, 0)
local y = math.clamp(scroller.CanvasPosition.Y + math.clamp(speed, -1.5, 1.5) * 360 * deltaTime, 0, maxScroll)
if y ~= scroller.CanvasPosition.Y then
scroller.CanvasPosition = Vector2.new(0, y)
follow(lastPointer)
end
end
function beginDrag(entry)
if dragging or #entries < 2 or self_._destroyed then
return
end
dragging = entry
startEntries = table.clone(entries)
lastPointer = pointerPosition()
grabOffset = lastPointer.Y - entry.Frame.AbsolutePosition.Y
entry.Frame.ZIndex = 5
tween(entry.Frame, { BackgroundColor3 = Theme.Surface3 }, 0.12)
tween(entry.Stroke, { Color = Theme.Accent }, 0.12)
tween(entry.Scale, { Scale = 1.02 }, 0.18, Enum.EasingStyle.Back)
tint(entry.GripIcon, Theme.Accent, 0, 0.12)
-- Neither the list nor the page scrolls under the finger while dragging.
scroller.ScrollingEnabled = false
local page = frame:FindFirstAncestorWhichIsA("ScrollingFrame")
if page and page.ScrollingEnabled then
lockedPage = page
page.ScrollingEnabled = false
end
stopRender = window:_listen("Render", autoScroll)
end
local function endDrag(silent)
local entry = dragging
if not entry then
return
end
dragging = nil
lastPointer = nil
if stopRender then
stopRender()
stopRender = nil
end
if lockedPage then
lockedPage.ScrollingEnabled = true
lockedPage = nil
end
tween(entry.Frame, { BackgroundColor3 = Theme.Surface }, 0.2)
tween(entry.Stroke, { Color = Theme.Stroke }, 0.25)
tween(entry.Scale, { Scale = 1 }, 0.2)
tint(entry.GripIcon, Theme.Muted, 0, 0.25)
task.delay(0.22, function()
if entry ~= dragging then
entry.Frame.ZIndex = 1
end
end)
layout(false)
fit()
for index, other in ipairs(entries) do
if startEntries[index] ~= other then
changed(silent)
break
end
end
end
window:_listen("Changed", function(input)
if dragging and isMove(input) then
lastPointer = pointerPosition()
follow(lastPointer)
end
end, self_)
window:_listen("Ended", function(input)
if dragging and isPress(input) then
endDrag(false)
end
end, self_)
table.insert(self_._listeners, function()
endDrag(true)
end)
self_._onHide = function()
endDrag(false)
end
-- Reorders the items already in the list. Unknown items are remembered for
-- a later Refresh; items missing from the list keep their order after.
function self_:Set(list, silent)
if type(list) ~= "table" then
return
end
endDrag(true)
remembered = table.clone(list)
build(arrange(values(), remembered))
layout(false)
fit()
changed(silent)
end
function self_:Get()
return values()
end
-- Replaces the items. Items that were already there keep the player's
-- order unless keepOrder is false; new ones go to the end.
function self_:Refresh(items, keepOrder, silent)
endDrag(true)
items = type(items) == "table" and table.clone(items) or {}
if keepOrder ~= false then
items = arrange(items, self_:_saveValue())
else
remembered = {}
end
build(items)
layout(false)
fit()
changed(silent)
end
-- The shown order, then remembered items that aren't in the list right now.
function self_:_saveValue()
local list = values()
local left = {}
for _, value in ipairs(list) do
left[value] = (left[value] or 0) + 1
end
for _, value in ipairs(remembered) do
if (left[value] or 0) > 0 then
left[value] -= 1
else
table.insert(list, value)
end
end
return list
end
build(type(opts.Items) == "table" and opts.Items or {})
layout(true)
fit()
self_.Value = values()
return finishElement(self, opts, self_, frame, "OrderList")
end
for name, method in pairs(table.clone(Tab)) do
if type(method) == "function" and name:sub(1, 1) ~= "_" and name:sub(1, 6) ~= "Create" then
Tab["Create" .. name] = method
end
end
local function detectExecutor()
local name
pcall(function()
if typeof(identifyexecutor) == "function" then
name = (identifyexecutor())
elseif typeof(getexecutorname) == "function" then
name = getexecutorname()
end
end)
if type(name) == "string" and #name > 0 then
return name
end
return RunService:IsStudio() and "Studio" or "Unknown"
end
local function executorReport(opts)
local executor = detectExecutor()
local supported = opts.SupportedExecutors
if type(supported) == "table" then
local lowered = string.lower(executor)
for _, name in ipairs(supported) do
if string.find(lowered, string.lower(tostring(name)), 1, true) then
return executor, true, "Your executor is supported and fully compatible."
end
end
return executor, false, "Not on the supported list. Some features may not work."
end
local capabilities = {
{ "writefile", writefile },
{ "readfile", readfile },
{ "getcustomasset", getcustomasset },
{ "setclipboard", setclipboard or toclipboard },
{ "request", request or http_request or (syn and syn.request) },
}
local missing = {}
for _, entry in ipairs(capabilities) do
if type(entry[2]) ~= "function" then
table.insert(missing, entry[1])
end
end
if #missing == 0 then
return executor, true, "Your executor is supported and fully compatible."
end
return executor, false, "Missing " .. table.concat(missing, ", ") .. ". Some features may not work."
end
-- "Don't ask again" on the unsupported executor warning: the executor's
-- name goes in a file in the script folder, and the warning is skipped while
-- the same executor is in use. Resetting the folder brings it back.
-- A table, not separate locals: the main chunk is at Luau's 200 local limit.
local UnsupportedSkip = {}
function UnsupportedSkip.Path(folder)
return tostring(folder) .. "/unsupported_skip.txt"
end
function UnsupportedSkip.Available()
return type(writefile) == "function" and type(readfile) == "function" and type(isfile) == "function"
end
function UnsupportedSkip.Saved(folder, executor)
if not UnsupportedSkip.Available() then
return false
end
local path = UnsupportedSkip.Path(folder)
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or nil
end)
return ok and saved == executor
end
function UnsupportedSkip.Write(folder, path, content)
if not UnsupportedSkip.Available() then
return false
end
return pcall(function()
if type(isfolder) == "function" and type(makefolder) == "function" then
local built = ""
for segment in tostring(folder):gmatch("[^/\\]+") do
built = built == "" and segment or built .. "/" .. segment
if not isfolder(built) then
makefolder(built)
end
end
end
writefile(path, content)
end)
end
function UnsupportedSkip.Save(folder, executor)
return UnsupportedSkip.Write(folder, UnsupportedSkip.Path(folder), executor)
end
-- Custom disclaimers ticked "Don't show again": one key per line in
-- disclaimers.txt. The key is the disclaimer's Id, or a hash of its title
-- and text, so rewording a disclaimer shows it again.
function UnsupportedSkip.DisclaimerKey(entry)
if entry.Id ~= nil then
return (tostring(entry.Id):gsub("[\r\n]", " "))
end
local source = tostring(entry.Title) .. "\0" .. tostring(entry.Text)
local hash = 5381
for index = 1, #source do
hash = (hash * 33 + source:byte(index)) % 4294967296
end
return string.format("%08x", hash)
end
function UnsupportedSkip.DisclaimerAccepted(folder, key)
if not UnsupportedSkip.Available() then
return false
end
local path = tostring(folder) .. "/disclaimers.txt"
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or ""
end)
if not ok or type(saved) ~= "string" then
return false
end
for line in saved:gmatch("[^\r\n]+") do
if line == key then
return true
end
end
return false
end
function UnsupportedSkip.AcceptDisclaimer(folder, key)
if UnsupportedSkip.DisclaimerAccepted(folder, key) then
return true
end
local path = tostring(folder) .. "/disclaimers.txt"
local ok, saved = pcall(function()
return isfile(path) and readfile(path) or ""
end)
saved = ok and type(saved) == "string" and saved or ""
if saved ~= "" and not saved:match("\n$") then
saved ..= "\n"
end
return UnsupportedSkip.Write(folder, path, saved .. key .. "\n")
end
local gameInfoCache = nil
local function fetchGameInfo()
if gameInfoCache == nil then
local ok, info = pcall(function()
return MarketplaceService:GetProductInfo(game.PlaceId)
end)
gameInfoCache = ok and type(info) == "table" and info or false
end
return gameInfoCache or nil
end
local function detectGameName()
local info = fetchGameInfo()
if info and info.Name then
return info.Name
end
return RunService:IsStudio() and "Studio" or "Unknown game"
end
local REGION_NAMES = {
US = "United States", GB = "United Kingdom", DE = "Germany", FR = "France",
NL = "Netherlands", SG = "Singapore", JP = "Japan", AU = "Australia",
BR = "Brazil", IN = "India", HK = "Hong Kong", CA = "Canada",
}
local function detectRegion(callback)
task.spawn(function()
local requester = request or http_request or (syn and syn.request) or (http and http.request)
local region
if type(requester) == "function" then
pcall(function()
local response = requester({ Url = "https://ipinfo.io/json", Method = "GET" })
local body = type(response) == "table" and (response.Body or response.body) or nil
if type(body) == "string" then
local data = HttpService:JSONDecode(body)
if type(data) == "table" and data.country then
local label = REGION_NAMES[data.country] or data.country
region = data.city and (data.city .. ", " .. label) or label
end
end
end)
end
callback(region or "Unavailable")
end)
end
local NOTIFY_COLORS = setmetatable({}, {
__index = function(_, kind)
if kind == "Success" or kind == "Warning" or kind == "Error" then
return Theme[kind]
end
return nil
end,
})
local Window = {}
Window.__index = Window
function Library.Window(_, opts)
opts = normalize(opts, { Name = "Title", LoadingSubtitle = "Subtitle", ToggleUIKeybind = "Keybind" })
if type(opts.Keybind) == "string" then
opts.Keybind = Enum.KeyCode[opts.Keybind]
end
local size = opts.Size or UDim2.fromOffset(760, 520)
local keybind = opts.Keybind or Enum.KeyCode.RightControl
local self = setmetatable({
Tabs = {},
CurrentTab = nil,
Open = true,
Keybind = keybind,
_connections = {},
_controls = {},
_inputListeners = { Began = {}, Changed = {}, Ended = {}, Render = {} },
_frameSteps = {},
_destroyed = false,
_identity = { Names = {}, Avatars = {} },
_pinned = {},
Minimized = false,
_hideName = false,
_hideAvatar = false,
Title = opts.Title or opts.Name or "XYZ",
_logoIcon = opts.Icon or Assets.Logo,
_sessionStart = os.clock(),
_islandLocked = opts.IslandDraggable == false,
}, Window)
local saving = type(opts.ConfigurationSaving) == "table" and opts.ConfigurationSaving or {}
self.ConfigFolder = saving.FolderName or "AirflowUI"
self.ConfigName = saving.FileName
self._configListeners = {}
self._pendingFlags = {}
local startTheme = opts.DefaultTheme or opts.Theme
if startTheme ~= nil then
Library.DefaultTheme = startTheme
end
self:_applyDefaultTheme()
self._uiScale = math.clamp(tonumber(opts.UIScale) or 1, 0.6, 1.5)
self._adaptive = opts.AdaptiveSize ~= false
self.UIScale = self._uiScale
if opts.Density ~= nil then
Library:SetDensity(opts.Density)
end
if opts.CornerRadius ~= nil then
Library:SetCornerRadius(opts.CornerRadius, true)
end
local function dispatch(kind)
return function(...)
for _, handler in ipairs(self._inputListeners[kind]) do
handler(...)
end
end
end
local renderDispatch = dispatch("Render")
table.insert(self._connections, RunService.RenderStepped:Connect(function(deltaTime)
renderDispatch(deltaTime)
for _, step in ipairs(self._frameSteps) do
step(deltaTime)
end
end))
table.insert(self._connections, UserInputService.InputBegan:Connect(dispatch("Began")))
table.insert(self._connections, UserInputService.InputChanged:Connect(dispatch("Changed")))
table.insert(self._connections, UserInputService.InputEnded:Connect(dispatch("Ended")))
local gui = create("ScreenGui", {
Name = opts.Name or "XYZUI",
IgnoreGuiInset = true,
ResetOnSpawn = false,
DisplayOrder = 999,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
})
self.Gui = gui
local root = create("Frame", {
Name = "Window",
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = size,
BackgroundTransparency = 1,
Parent = gui,
})
self.Root = root
local scale = create("UIScale", { Parent = root })
self.Scale = scale
local shadow = create("ImageLabel", {
Position = UDim2.fromOffset(-25, -25),
Size = UDim2.new(1, 50, 1, 50),
BackgroundTransparency = 1,
Image = Assets.Shadow,
ImageColor3 = Color3.new(0, 0, 0),
ImageTransparency = 0.6,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(49, 49, 450, 450),
Parent = root,
})
self.Shadow = shadow
self._shadowRest = 0.6
-- The body is a plain Frame so resizing never re-renders the window into a
-- texture. It only moves into the fader CanvasGroup while fading.
local fader = create("CanvasGroup", {
Name = "Fader",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
GroupTransparency = 1,
ZIndex = 2,
Parent = root,
})
self._fader = fader
self._bodyAlpha = 1
local body = create("Frame", {
Name = "Body",
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Theme.Background,
BorderSizePixel = 0,
ClipsDescendants = true,
ZIndex = 2,
Parent = fader,
})
self.Body = body
corner(body, UDim.new(0, 10))
self.BodyStroke = stroke(body, Theme.Stroke)
edgeHighlight(body)
-- Background image: made first so everything else in the body draws over it.
self._backgroundImage = create("ImageLabel", {
Name = "BackgroundImage",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Crop,
ImageTransparency = 1,
Visible = false,
ZIndex = 0,
Parent = body,
})
corner(self._backgroundImage, UDim.new(0, 10))
self.BackgroundTransparency = math.clamp(tonumber(opts.BackgroundTransparency) or 0.35, 0, 1)
self._backgroundGeneration = 0
-- The sidebar is a flush, solid rail down the left edge, split from the
-- content by a single hairline.
local sidebar = create("Frame", {
Name = "Sidebar",
Size = UDim2.new(0, SIDEBAR_WIDTH, 1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ClipsDescendants = true,
Parent = body,
})
self.Sidebar = sidebar
-- One rounded fill that runs past the rail's right edge, where the rail
-- clips it: the left corners round with the window, the right edge is
-- square, and nothing overlaps, so it fades evenly when see-through.
local sidebarFill = create("Frame", {
Name = "Fill",
Size = UDim2.new(1, 24, 1, 0),
BackgroundColor3 = Theme.Surface,
BorderSizePixel = 0,
ZIndex = 0,
Parent = sidebar,
})
corner(sidebarFill, UDim.new(0, 10))
self.SidebarFill = sidebarFill
create("Frame", {
Name = "Divider",
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.fromScale(1, 0),
Size = UDim2.new(0, 1, 1, 0),
BackgroundColor3 = Theme.Stroke,
BorderSizePixel = 0,
ZIndex = 2,
Parent = sidebar,
})
local header = create("Frame", {
Name = "Header",
Size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
BackgroundTransparency = 1,
Parent = sidebar,
})
local logoText = create("TextLabel", {
Text = "Xyz",
Position = UDim2.fromOffset(16, 7),
Size = UDim2.fromOffset(52, 30),
BackgroundTransparency = 1,
TextColor3 = Color3.fromRGB(255, 255, 255),
Font = Enum.Font.GothamBlack,
TextSize = 28,
TextXAlignment = Enum.TextXAlignment.Left,
Parent = header,
})
logoText:SetAttribute("NoTheme", true)
local logoGrad = create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.fromRGB(150, 150, 158)),
ColorSequenceKeypoint.new(0.5, Color3.fromRGB(255, 255, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(150, 150, 158)),
}),
Offset = Vector2.new(-1, 0),
Parent = logoText,
})
TweenService:Create(logoGrad, TweenInfo.new(2.4, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, false, 1.2), { Offset = Vector2.new(1, 0) }):Play()
label({
Position = UDim2.fromOffset(70, 7),
Size = UDim2.fromOffset(66, 30),
Text = "Scripts",
TextSize = 17,
FontFace = Fonts.Bold,
TextColor3 = Theme.Muted,
Parent = header,
})
local subtitle = opts.Version or opts.Subtitle
if subtitle then
label({
Position = UDim2.fromOffset(18, 40),
Size = UDim2.new(1, -30, 0, 14),
Text = tostring(subtitle),
TextSize = 12,
FontFace = Fonts.Regular,
TextColor3 = Theme.Muted,
Parent = header,
})
end
local profileEnabled = opts.Profile ~= false
local tabList = create("ScrollingFrame", {
Name = "Tabs",
Position = UDim2.fromOffset(0, HEADER_HEIGHT),
Size = UDim2.new(1, 0, 1, -(HEADER_HEIGHT + (profileEnabled and PROFILE_HEIGHT + 4 or 12))),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 0,
ScrollingDirection = Enum.ScrollingDirection.Y,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.new(),
Parent = sidebar,
})
self.TabList = tabList
-- The highlight sits beside the stack of buttons, so it scrolls and clips
-- with them and glides between tiles.
local indicator = create("Frame", {
Position = UDim2.fromOffset(8, 2),
Size = UDim2.new(1, -16, 0, TAB_TILE_HEIGHT),
BackgroundColor3 = Theme.Surface3,
BackgroundTransparency = 0.35,
BorderSizePixel = 0,
Visible = false,
Parent = tabList,
})
corner(indicator, UDim.new(0, 7))
-- The active tab is just a quiet neutral fill with white text; the bar
-- is kept for callers but stays hidden.
local pill = create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Size = UDim2.fromOffset(2, 14),
BackgroundColor3 = Theme.Accent,
Visible = false,
BorderSizePixel = 0,
Parent = indicator,
})
corner(pill, UDim.new(1, 0))
self._indicatorPill = pill
self.Indicator = indicator
local stack = create("Frame", {
Name = "Stack",
Position = UDim2.fromOffset(8, 2),
Size = UDim2.new(1, -16, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
ZIndex = 2,
Parent = tabList,
})
create("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 2),
Parent = stack,
})
self._tabStack = stack
-- More tabs than fit: a fade and a small "more" chip at the hidden end.
self:_overflowHint(sidebar, tabList, stack, true, Theme.Surface)
if profileEnabled then
self:_buildProfile(sidebar)
end
local content = create("Frame", {
Name = "Content",
Position = UDim2.fromOffset(SIDEBAR_WIDTH + 1, 0),
Size = UDim2.new(1, -(SIDEBAR_WIDTH + 1), 1, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
Parent = body,
})
self.Content = content
self:_sidebarResize(sidebar, tonumber(opts.SidebarWidth) or SIDEBAR_WIDTH)
self._outLayer = create("CanvasGroup", {
Name = "TransitionOut",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 2,
Parent = content,
})
self._inLayer = create("CanvasGroup", {
Name = "TransitionIn",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Visible = false,
ZIndex = 3,
Parent = content,
})
-- Top-right controls: "-" minimises to the small bar, "X" unloads.
local function controlButton(right, closeStyle)
local button = create("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, right, 0, 14),
Size = UDim2.fromOffset(34, 34),
BackgroundColor3 = closeStyle and Theme.Error or Theme.Surface3,
BackgroundTransparency = 1,
Text = "",
AutoButtonColor = false,
ZIndex = 5,
Parent = content,
})
button:SetAttribute("NoDrag", true)
corner(button, UDim.new(0, 8))
local parts = {}
if closeStyle then
for _, rotation in ipairs({ 45, -45 }) do
parts[#parts + 1] = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(16, 2),
Rotation = rotation,
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = button,
})
end
else
parts[1] = create("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(14, 2),
BackgroundColor3 = Theme.Muted,
BorderSizePixel = 0,
Parent = button,
})
end
for _, part in ipairs(parts) do
corner(part, UDim.new(1, 0))
end
button.MouseEnter:Connect(function()
Obfuscated. The code is scrambled, so you can't read what it does. Common for key systems, but take care.
Loads code from the internet. It downloads more code when it runs (loadstring + HttpGet), which can change after it was posted. From: raw.githubusercontent.com, ipinfo.io.

Universal