local DefaultTheme = {
background = Color3.fromRGB(37, 37, 37),
sidebar = Color3.fromRGB(47, 47, 47),
text = Color3.fromRGB(204, 204, 204),
keyword = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
builtin = {
color = Color3.fromRGB(132, 214, 247),
isBold = true,
},
string = {
color = Color3.fromRGB(173, 241, 149),
isBold = false,
},
number = {
color = Color3.fromRGB(255, 198, 0),
isBold = false,
},
comment = {
color = Color3.fromRGB(102, 102, 102),
isBold = false,
},
lprop = {
color = Color3.fromRGB(97, 161, 241),
isBold = false,
},
lmethod = {
color = Color3.fromRGB(253, 251, 172),
isBold = false,
},
bool = {
color = Color3.fromRGB(255, 198, 0),
isBold = true,
},
operator = {
color = Color3.fromRGB(204, 204, 204),
isBold = false,
},
method = {
color = Color3.fromRGB(253, 251, 172),
isBold = false,
},
luau = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
["nil"] = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
["function"] = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
["local"] = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
["self"] = {
color = Color3.fromRGB(248, 109, 124),
isBold = true,
},
}
local Lexer = {}
local Prefix, Suffix, Cleaner = "^[%c%s]*", "[%c%s]*", "[%c%s]+"
local UNICODE = "[%z\x01-\x7F\xC2-\xF4][\x80-\xBF]+"
local NUMBER_A = "0[xX][%da-fA-F_]+"
local NUMBER_B = "0[bB][01_]+"
local NUMBER_C = "%d+%.?%d*[eE][%+%-]?%d+"
local NUMBER_D = "%d+[%._]?[%d_eE]*"
local OPERATORS = "[:;<>/~%*%(%)%-={},%.#%^%+%%]+"
local BRACKETS = "[%[%]]+"
local IDEN = "[%a_][%w_]*"
local STRING_EMPTY = "(['\"])%1"
local STRING_PLAIN = "(['\"])[^\n]-([^\\]%1)"
local STRING_INTER = "`[^\n]-`"
local STRING_INCOMP_A = "(['\"]).-\n"
local STRING_INCOMP_B = "(['\"])[^\n]*"
local STRING_MULTI = "%[(=*)%[.-%]%1%]"
local STRING_MULTI_INCOMP = "%[=*%[.-.*"
local COMMENT_MULTI = "%-%-%[(=*)%[.-%]%1%]"
local COMMENT_MULTI_INCOMP = "%-%-%[=*%[.-.*"
local COMMENT_PLAIN = "%-%-.-\n"
local COMMENT_INCOMP = "%-%-.*"
local language = {
keyword = {
["and"] = "keyword",
["break"] = "keyword",
["continue"] = "keyword",
["do"] = "keyword",
["else"] = "keyword",
["elseif"] = "keyword",
["end"] = "keyword",
["export"] = "keyword",
["false"] = "keyword",
["for"] = "keyword",
["function"] = "keyword",
["if"] = "keyword",
["in"] = "keyword",
["local"] = "keyword",
["nil"] = "keyword",
["not"] = "keyword",
["or"] = "keyword",
["repeat"] = "keyword",
["return"] = "keyword",
["self"] = "keyword",
["then"] = "keyword",
["true"] = "keyword",
["type"] = "keyword",
["typeof"] = "keyword",
["until"] = "keyword",
["while"] = "keyword",
},
builtin = {
["assert"] = "function",
["error"] = "function",
["getmetatable"] = "function",
["ipairs"] = "function",
["newproxy"] = "function",
["next"] = "function",
["pairs"] = "function",
["pcall"] = "function",
["print"] = "function",
["rawequal"] = "function",
["rawget"] = "function",
["rawlen"] = "function",
["rawset"] = "function",
["select"] = "function",
["setmetatable"] = "function",
["tonumber"] = "function",
["tostring"] = "function",
["unpack"] = "function",
["xpcall"] = "function",
["collectgarbage"] = "function",
["_G"] = "table",
["_VERSION"] = "string",
["bit32"] = "table",
["coroutine"] = "table",
["debug"] = "table",
["math"] = "table",
["os"] = "table",
["string"] = "table",
["table"] = "table",
["utf8"] = "table",
["DebuggerManager"] = "function",
["delay"] = "function",
["gcinfo"] = "function",
["PluginManager"] = "function",
["require"] = "function",
["settings"] = "function",
["spawn"] = "function",
["tick"] = "function",
["time"] = "function",
["UserSettings"] = "function",
["wait"] = "function",
["warn"] = "function",
["Delay"] = "function",
["ElapsedTime"] = "function",
["elapsedTime"] = "function",
["printidentity"] = "function",
["Spawn"] = "function",
["Stats"] = "function",
["stats"] = "function",
["Version"] = "function",
["version"] = "function",
["Wait"] = "function",
["ypcall"] = "function",
["game"] = "Instance",
["plugin"] = "Instance",
["script"] = "Instance",
["shared"] = "Instance",
["workspace"] = "Instance",
["Game"] = "Instance",
["Workspace"] = "Instance",
["Axes"] = "table",
["BrickColor"] = "table",
["CatalogSearchParams"] = "table",
["CFrame"] = "table",
["Color3"] = "table",
["ColorSequence"] = "table",
["ColorSequenceKeypoint"] = "table",
["DateTime"] = "table",
["DockWidgetPluginGuiInfo"] = "table",
["Enum"] = "table",
["Faces"] = "table",
["FloatCurveKey"] = "table",
["Font"] = "table",
["Instance"] = "table",
["NumberRange"] = "table",
["NumberSequence"] = "table",
["NumberSequenceKeypoint"] = "table",
["OverlapParams"] = "table",
["PathWaypoint"] = "table",
["PhysicalProperties"] = "table",
["Random"] = "table",
["Ray"] = "table",
["RaycastParams"] = "table",
["Rect"] = "table",
["Region3"] = "table",
["Region3int16"] = "table",
["RotationCurveKey"] = "table",
["SharedTable"] = "table",
["task"] = "table",
["TweenInfo"] = "table",
["UDim"] = "table",
["UDim2"] = "table",
["Vector2"] = "table",
["Vector2int16"] = "table",
["Vector3"] = "table",
["Vector3int16"] = "table",
},
libraries = {
bit32 = {
arshift = "function",
band = "function",
bnot = "function",
bor = "function",
btest = "function",
bxor = "function",
countlz = "function",
countrz = "function",
extract = "function",
lrotate = "function",
lshift = "function",
replace = "function",
rrotate = "function",
rshift = "function",
},
buffer = {
copy = "function",
create = "function",
fill = "function",
fromstring = "function",
len = "function",
readf32 = "function",
readf64 = "function",
readi8 = "function",
readi16 = "function",
readi32 = "function",
readu16 = "function",
readu32 = "function",
readu8 = "function",
readstring = "function",
tostring = "function",
writef32 = "function",
writef64 = "function",
writei16 = "function",
writei32 = "function",
writei8 = "function",
writestring = "function",
writeu16 = "function",
writeu32 = "function",
writeu8 = "function",
},
coroutine = {
close = "function",
create = "function",
isyieldable = "function",
resume = "function",
running = "function",
status = "function",
wrap = "function",
yield = "function",
},
debug = {
dumpheap = "function",
getmemorycategory = "function",
info = "function",
loadmodule = "function",
profilebegin = "function",
profileend = "function",
resetmemorycategory = "function",
setmemorycategory = "function",
traceback = "function",
},
math = {
abs = "function",
acos = "function",
asin = "function",
atan2 = "function",
atan = "function",
ceil = "function",
clamp = "function",
cos = "function",
cosh = "function",
deg = "function",
exp = "function",
floor = "function",
fmod = "function",
frexp = "function",
ldexp = "function",
log10 = "function",
log = "function",
max = "function",
min = "function",
modf = "function",
noise = "function",
pow = "function",
rad = "function",
random = "function",
randomseed = "function",
round = "function",
sign = "function",
sin = "function",
sinh = "function",
sqrt = "function",
tan = "function",
tanh = "function",
huge = "number",
pi = "number",
},
os = {
clock = "function",
date = "function",
difftime = "function",
time = "function",
},
string = {
byte = "function",
char = "function",
find = "function",
format = "function",
gmatch = "function",
gsub = "function",
len = "function",
lower = "function",
match = "function",
pack = "function",
packsize = "function",
rep = "function",
reverse = "function",
split = "function",
sub = "function",
unpack = "function",
upper = "function",
},
table = {
clear = "function",
clone = "function",
concat = "function",
create = "function",
find = "function",
foreach = "function",
foreachi = "function",
freeze = "function",
getn = "function",
insert = "function",
isfrozen = "function",
maxn = "function",
move = "function",
pack = "function",
remove = "function",
sort = "function",
unpack = "function",
},
utf8 = {
char = "function",
codepoint = "function",
codes = "function",
graphemes = "function",
len = "function",
nfcnormalize = "function",
nfdnormalize = "function",
offset = "function",
charpattern = "string",
},
Axes = {
new = "function",
},
BrickColor = {
Black = "function",
Blue = "function",
DarkGray = "function",
Gray = "function",
Green = "function",
new = "function",
New = "function",
palette = "function",
Random = "function",
random = "function",
Red = "function",
White = "function",
Yellow = "function",
},
CatalogSearchParams = {
new = "function",
},
CFrame = {
Angles = "function",
fromAxisAngle = "function",
fromEulerAngles = "function",
fromEulerAnglesXYZ = "function",
fromEulerAnglesYXZ = "function",
fromMatrix = "function",
fromOrientation = "function",
lookAt = "function",
new = "function",
identity = "CFrame",
},
Color3 = {
fromHex = "function",
fromHSV = "function",
fromRGB = "function",
new = "function",
toHSV = "function",
},
ColorSequence = {
new = "function",
},
ColorSequenceKeypoint = {
new = "function",
},
DateTime = {
fromIsoDate = "function",
fromLocalTime = "function",
fromUniversalTime = "function",
fromUnixTimestamp = "function",
fromUnixTimestampMillis = "function",
now = "function",
},
DockWidgetPluginGuiInfo = {
new = "function",
},
Enum = {},
Faces = {
new = "function",
},
FloatCurveKey = {
new = "function",
},
Font = {
fromEnum = "function",
fromId = "function",
fromName = "function",
new = "function",
},
Instance = {
new = "function",
},
NumberRange = {
new = "function",
},
NumberSequence = {
new = "function",
},
NumberSequenceKeypoint = {
new = "function",
},
OverlapParams = {
new = "function",
},
PathWaypoint = {
new = "function",
},
PhysicalProperties = {
new = "function",
},
Random = {
new = "function",
},
Ray = {
new = "function",
},
RaycastParams = {
new = "function",
},
Rect = {
new = "function",
},
Region3 = {
new = "function",
},
Region3int16 = {
new = "function",
},
RotationCurveKey = {
new = "function",
},
SharedTable = {
clear = "function",
clone = "function",
cloneAndFreeze = "function",
increment = "function",
isFrozen = "function",
new = "function",
size = "function",
update = "function",
},
task = {
cancel = "function",
defer = "function",
delay = "function",
desynchronize = "function",
spawn = "function",
synchronize = "function",
wait = "function",
},
TweenInfo = {
new = "function",
},
UDim = {
new = "function",
},
UDim2 = {
fromOffset = "function",
fromScale = "function",
new = "function",
},
Vector2 = {
new = "function",
one = "Vector2",
xAxis = "Vector2",
yAxis = "Vector2",
zero = "Vector2",
},
Vector2int16 = {
new = "function",
},
Vector3 = {
fromAxis = "function",
FromAxis = "function",
fromNormalId = "function",
FromNormalId = "function",
new = "function",
one = "Vector3",
xAxis = "Vector3",
yAxis = "Vector3",
zAxis = "Vector3",
zero = "Vector3",
},
Vector3int16 = {
new = "function",
},
},
}
local enumLibraryTable = language.libraries.Enum
for _, enum in ipairs(Enum:GetEnums()) do
enumLibraryTable[tostring(enum)] = "Enum"
end
local lua_keyword = language.keyword
local lua_builtin = language.builtin
local lua_libraries = language.libraries
Lexer.language = language
local lua_matches = {
{ Prefix .. IDEN .. Suffix, "var" },
{ Prefix .. NUMBER_A .. Suffix, "number" },
{ Prefix .. NUMBER_B .. Suffix, "number" },
{ Prefix .. NUMBER_C .. Suffix, "number" },
{ Prefix .. NUMBER_D .. Suffix, "number" },
{ Prefix .. STRING_EMPTY .. Suffix, "string" },
{ Prefix .. STRING_PLAIN .. Suffix, "string" },
{ Prefix .. STRING_INCOMP_A .. Suffix, "string" },
{ Prefix .. STRING_INCOMP_B .. Suffix, "string" },
{ Prefix .. STRING_MULTI .. Suffix, "string" },
{ Prefix .. STRING_MULTI_INCOMP .. Suffix, "string" },
{ Prefix .. STRING_INTER .. Suffix, "string_inter" },
{ Prefix .. COMMENT_MULTI .. Suffix, "comment" },
{ Prefix .. COMMENT_MULTI_INCOMP .. Suffix, "comment" },
{ Prefix .. COMMENT_PLAIN .. Suffix, "comment" },
{ Prefix .. COMMENT_INCOMP .. Suffix, "comment" },
{ Prefix .. OPERATORS .. Suffix, "operator" },
{ Prefix .. BRACKETS .. Suffix, "operator" },
{ Prefix .. UNICODE .. Suffix, "iden" },
{ "^.", "iden" },
}
local PATTERNS, TOKENS = {}, {}
for i, m in lua_matches do
PATTERNS[i] = m[1]
TOKENS[i] = m[2]
end
function Lexer.scan(s: string)
local index = 1
local size = #s
local previousContent1, previousContent2, previousContent3, previousToken = "", "", "", ""
local thread = coroutine.create(function()
while index <= size do
local matched = false
for tokenType, pattern in ipairs(PATTERNS) do
local start, finish = string.find(s, pattern, index)
if start == nil then
continue
end
index = finish + 1
matched = true
local content = string.sub(s, start, finish)
local rawToken = TOKENS[tokenType]
local processedToken = rawToken
if rawToken == "var" then
local cleanContent = string.gsub(content, Cleaner, "")
if lua_keyword[cleanContent] then
processedToken = "keyword"
elseif lua_builtin[cleanContent] then
processedToken = "builtin"
elseif string.find(previousContent1, "%.[%s%c]*$") and previousToken ~= "comment" then
local parent = string.gsub(previousContent2, Cleaner, "")
local lib = lua_libraries[parent]
if lib and lib[cleanContent] and not string.find(previousContent3, "%.[%s%c]*$") then
processedToken = "builtin"
else
processedToken = "iden"
end
else
processedToken = "iden"
end
elseif rawToken == "string_inter" then
if not string.find(content, "[^\\]{") then
processedToken = "string"
else
processedToken = nil
local isString = true
local subIndex = 1
local subSize = #content
while subIndex <= subSize do
local subStart, subFinish = string.find(content, "^.-[^\\][{}]", subIndex)
if subStart == nil then
coroutine.yield("string", string.sub(content, subIndex), index)
break
end
if isString then
subIndex = subFinish + 1
coroutine.yield("string", string.sub(content, subStart, subFinish), start + subIndex - 1)
isString = false
else
subIndex = subFinish
local subContent = string.sub(content, subStart, subFinish - 1)
for innerToken, innerContent, innerTrim in Lexer.scan(subContent) do
coroutine.yield(innerToken, innerContent, start + subStart + innerTrim - 2)
end
isString = true
end
end
end
end
previousContent3 = previousContent2
previousContent2 = previousContent1
previousContent1 = content
previousToken = processedToken or rawToken
if processedToken then
coroutine.yield(processedToken, content, index)
end
break
end
if not matched then
return
end
end
return
end)
return function()
if coroutine.status(thread) == "dead" then
return
end
local success, token, content, trim = coroutine.resume(thread)
if success and token then
return token, content, trim
end
return
end
end
function Lexer.run(str: string)
local scanResult = {}
for token, src, trim in Lexer.scan(str) do
table.insert(scanResult, {trim = trim, token = token, src = src})
end
return scanResult
end
function Lexer.navigator()
local nav = {
Source = "",
TokenCache = table.create(50),
_RealIndex = 0,
_UserIndex = 0,
_ScanThread = nil,
}
function nav:Destroy()
self.Source = nil
self._RealIndex = nil
self._UserIndex = nil
self.TokenCache = nil
self._ScanThread = nil
end
function nav:SetSource(SourceString)
self.Source = SourceString
self._RealIndex = 0
self._UserIndex = 0
table.clear(self.TokenCache)
self._ScanThread = coroutine.create(function()
for Token, Src, trim in Lexer.scan(self.Source) do
self._RealIndex += 1
self.TokenCache[self._RealIndex] = { Token, Src }
coroutine.yield(Token, Src, trim)
end
end)
end
function nav.Next()
nav._UserIndex += 1
if nav._RealIndex >= nav._UserIndex then
return table.unpack(nav.TokenCache[nav._UserIndex])
else
if coroutine.status(nav._ScanThread) == "dead" then
return
else
local success, token, src = coroutine.resume(nav._ScanThread)
if success and token then
return token, src
else
return
end
end
end
end
function nav.Peek(PeekAmount)
local GoalIndex = nav._UserIndex + PeekAmount
if nav._RealIndex >= GoalIndex then
if GoalIndex > 0 then
return table.unpack(nav.TokenCache[GoalIndex])
else
return
end
else
if coroutine.status(nav._ScanThread) == "dead" then
return
else
local IterationsAway = GoalIndex - nav._RealIndex
local success, token, src = nil, nil, nil
for _ = 1, IterationsAway do
success, token, src = coroutine.resume(nav._ScanThread)
if not (success or token) then
break
end
end
return token, src
end
end
end
return nav
end
local LuaTable = {}; LuaTable.__index = LuaTable
function LuaTable:UpdateSize()
local Size = 0
for Line, _ in pairs(self.Code) do
if Line > Size then
Size = Line
end
end
self.Size = Size
return Size
end
function LuaTable:Push(Line, LuaArray)
for RelativeLine, LuaLine in pairs(LuaArray) do
table.insert(self.Code, Line + RelativeLine - 1, LuaLine)
end
self:UpdateSize()
return self
end
function LuaTable:Insert(Line, LuaArray)
return self:Push(Line + 1, LuaArray)
end
function LuaTable:Add(LuaArray)
return self:Insert(self.Size, LuaArray)
end
function LuaTable:Erase(RegionStart, RegionLength)
RegionLength = RegionLength or 1
for i = 1, RegionLength do
table.remove(self.Code, RegionStart)
end
self:UpdateSize()
return self
end
function LuaTable:Write(RegionStart, RegionLength, LuaArray)
return self
:Erase(RegionStart, RegionLength)
:Push(RegionStart, LuaArray)
end
function LuaTable.new(LuaLines)
local Fields = {
Code = LuaLines,
Size = #LuaLines
}
local self = setmetatable(Fields, LuaTable)
self:UpdateSize()
return self
end
function LuaTable.FromArray(LuaArray)
return LuaTable.new(LuaArray)
end
function LuaTable.FromString(str)
return LuaTable.new(string.split(str, "\n"))
end
function LuaTable.FromTuple(...)
local LuaLines = {...}
return LuaTable.new(LuaLines)
end
function LuaTable.FromScript(Script)
return LuaTable.FromString(Script.Source)
end
function LuaTable:ToString(RegionStart, RegionLength)
RegionStart = math.max(RegionStart or 1, 1)
local size = math.min(RegionLength or math.huge, self.Size - RegionStart + 1)
for Line = 1, size do
local LuaLine = self.Code[Line + RegionStart - 1] or ""
self.Code[Line + RegionStart - 1] = LuaLine
end
local LuaString = table.concat(self.Code, "\n", RegionStart, size + RegionStart - 1)
return LuaString
end
function LuaTable:ToScript(Script, RegionStart, RegionLength)
Script.Source = self:ToString(RegionStart, RegionLength)
end
function LuaTable:ToArray(RegionStart, RegionLength)
RegionStart = math.max(RegionStart or 1, 1)
local array = {}
local size = math.min(RegionLength or math.huge, self.Size - RegionStart + 1)
for index = 1, size do
local LuaLine = self.Code[index + RegionStart - 1]
array[index] = LuaLine or ""
end
return array
end
local freeRunnerThread = nil
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
local acquiredRunnerThread = freeRunnerThread
freeRunnerThread = nil
fn(...)
freeRunnerThread = acquiredRunnerThread
end
local function runEventHandlerInFreeThread()
while true do
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
end
end
local Connection = {}
Connection.__index = Connection
function Connection.new(signal, fn)
return setmetatable({
_connected = true,
_signal = signal,
_fn = fn,
_next = false,
}, Connection)
end
function Connection:Disconnect()
self._connected = false
if self._signal._handlerListHead == self then
self._signal._handlerListHead = self._next
else
local prev = self._signal._handlerListHead
while prev and prev._next ~= self do
prev = prev._next
end
if prev then
prev._next = self._next
end
end
end
setmetatable(Connection, {
__index = function(tb, key)
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(tb, key, value)
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
})
local Signal = {}
Signal.__index = Signal
function Signal.new()
return setmetatable({
_handlerListHead = false,
}, Signal)
end
function Signal:Connect(fn)
local connection = Connection.new(self, fn)
if self._handlerListHead then
connection._next = self._handlerListHead
self._handlerListHead = connection
else
self._handlerListHead = connection
end
return connection
end
function Signal:DisconnectAll()
self._handlerListHead = false
end
function Signal:Destroy()
self._handlerListHead = false
end
function Signal:Fire(...)
local item = self._handlerListHead
while item do
if item._connected then
if not freeRunnerThread then
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
coroutine.resume(freeRunnerThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
end
function Signal:Wait()
local waitingCoroutine = coroutine.running()
local cn;
cn = self:Connect(function(...)
cn:Disconnect()
task.spawn(waitingCoroutine, ...)
end)
return coroutine.yield()
end
function Signal:Once(fn)
local cn;
cn = self:Connect(function(...)
if cn._connected then
cn:Disconnect()
end
fn(...)
end)
return cn
end
setmetatable(Signal, {
__index = function(tb, key)
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
end,
__newindex = function(tb, key, value)
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
})
SignalModule = Signal
local TextService = game:GetService("TextService")
function getMultiLinePositions(text)
local multilines = string.split(text, "\n")
local multilinePositions = {}
local cursor = 1
for _, line in multilines do
table.insert(multilinePositions, {cursor, cursor + string.len(line)})
cursor += string.len(line) + 1
end
return multilines, multilinePositions
end
function getLinePosition(multilinePositions, position)
for i, multilinePosition in multilinePositions do
if (position >= multilinePosition[1] and position <= multilinePosition[2]) then
return i, position - multilinePosition[1] + 1
end
end
return -1, -1
end
function getLengthOfLine(text, textLabel: TextBox)
local absoluteSize = TextService:GetTextSize(
text,
textLabel.TextSize,
textLabel.Font,
Vector2.new(math.huge, math.huge)
)
return absoluteSize.X
end
function getShiftAmount(lower, upper, x)
if x < lower then
return x - lower
elseif x > upper then
return x - upper
else
return 0
end
end
local GetTextBoxScrolling = {}
function GetTextBoxScrolling.UpdateShift(textLabel: TextBox, cursorPosition, shift)
if cursorPosition == -1 then
return 0
end
local labelWindow = textLabel.AbsoluteSize.X - 1
local multilines, multilinePositions = getMultiLinePositions(textLabel.Text)
local linePosition, positionLength = getLinePosition(multilinePositions, cursorPosition)
local line = multilines[linePosition]
local stringUntilCursor = string.sub(line, 0, positionLength - 1)
local lineLength = getLengthOfLine(stringUntilCursor, textLabel)
local relativeShift = getShiftAmount(shift, shift + labelWindow, lineLength)
shift += relativeShift
return shift
end
local InitGui = {}
InitGui.NIL = newproxy()
function trySet(instance, property, value)
return pcall(function()
instance[property] = value
end)
end
function InitGui.instance(properties)
local className = properties.ClassName
local instance = Instance.new(className)
for k, v in properties do
if k ~= "ClassName" and k ~= "Parent" then
if v == InitGui.NIL then
v = nil
end
trySet(instance, k, v)
end
end
instance.Parent = properties.Parent
return instance
end
function InitGui.init()
local oseBackground = InitGui.instance {
Name = "OSEBackground",
ClassName = "Frame",
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0)
}
InitGui.instance {
Name = "LineNumberBackground",
ClassName = "Frame",
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(0, 16, 1, 0),
Parent = oseBackground,
}
local lineNumberContainer = InitGui.instance {
Name = "LineNumberContainer",
ClassName = "Frame",
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 5),
Size = UDim2.new(0, 16, 1, -10),
ZIndex = 2,
Parent = oseBackground
}
InitGui.instance {
ClassName = "UIListLayout",
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = lineNumberContainer,
}
local richOverlayContainer = InitGui.instance {
Name = "RichOverlayContainer",
ClassName = "Frame",
BackgroundTransparency = 1,
Active = true,
Position = UDim2.new(0, 25, 0, 5),
Size = UDim2.new(1, -30, 1, -10),
ClipsDescendants = true,
Parent = oseBackground
}
local shiftContainer = InitGui.instance {
Name = "ShiftContainer",
ClassName = "Frame",
BackgroundTransparency = 1,
Active = true,
Position = UDim2.new(),
Size = UDim2.new(3, 0, 1, 0),
ClipsDescendants = false,
Parent = richOverlayContainer,
}
InitGui.instance {
ClassName = "UIListLayout",
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = shiftContainer,
}
InitGui.instance {
Name = "CodeField",
ClassName = "TextBox",
BackgroundTransparency = 1,
ClearTextOnFocus = false,
MultiLine = true,
Position = UDim2.new(0, 25, 0, 5),
Size = UDim2.new(1, -30, 1, -10),
Font = Enum.Font.Code,
Text = "",
TextColor3 = Color3.new(.8, .8, .8),
TextSize = 14,
TextTransparency = .5,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
ClipsDescendants = true,
Parent = oseBackground,
}
return oseBackground
end
function InitGui.storage()
local storage = InitGui.instance {
Name = "Storage",
ClassName = "Folder"
}
InitGui.instance {
Name = "LineNumber",
ClassName = "TextLabel",
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundTransparency = 1,
Size = UDim2.new(1, -6, 0, 0),
ZIndex = 5,
Font = Enum.Font.SourceSans,
TextColor3 = Color3.new(.8, .8, .8),
TextSize = 14,
TextXAlignment = Enum.TextXAlignment.Right,
TextYAlignment = Enum.TextYAlignment.Center,
Parent = storage,
}
InitGui.instance {
Name = "RichOverlayLabel",
ClassName = "TextLabel",
AutomaticSize = Enum.AutomaticSize.XY,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 0),
ZIndex = 5,
Font = Enum.Font.Code,
TextColor3 = Color3.new(.8, .8, .8),
RichText = true,
TextSize = 14,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
Parent = storage,
}
return storage
end
local AutoIndent = {}
local function testForIndent(codeField, code, start, keyword)
if start - keyword:len() <= 0 then
return
end
local previousRawText = codeField.Text:sub(start - keyword:len(), start - 1)
local previousText = code:sub(start - keyword:len(), start - 1)
if previousRawText == keyword and previousRawText == previousText then
return true
end
end
local function countTabs(line)
-- local init = 1
-- local count = 0
-- repeat
-- local Start, End = string.find(line, "\9", init)
-- if Start and End then
-- init = Start + 1
-- count += 1
-- end
-- until not (Start and End)
-- return count
return string.match(line, "^\9*"):len()
end
local indentKeywords = {
["do"] = 1,
["then"] = 1,
["repeat"] = 1,
["{"] = 1,
["function()"] = 1,
}
function findCurrentLine(code, position)
local lines = string.split(code, "\n")
local selection = 0
local currentLineNumber = 0
for i, line in lines do
selection += 1 + line:len()
if position <= selection then
currentLineNumber = i
break
end
end
if not currentLineNumber then
warn("Failed to find current line number")
return false
end
return currentLineNumber, lines
end
function AutoIndent.OnScriptChange(scriptEditor, data)
local code = data.Code
local codeField = scriptEditor.Background.CodeField
local position = codeField.CursorPosition
local canIndent = false
local additionalTabs = 0
for keyword, tabs in indentKeywords do
if testForIndent(codeField, code, position, keyword .. "\n") and data.Gain == 1 then
canIndent = true
additionalTabs = tabs
break
end
end
local currentLineNumber, lines = findCurrentLine(code, position)
local previousLineNumber = currentLineNumber - 1
local currentText = lines[currentLineNumber]
local previousText = lines[previousLineNumber]
local justReached = previousText and currentText == "" and data.Gain == 1
if not canIndent and justReached then
--lonely function case
local funcPatterns = {
"^%s*function%s+[%w%.:]+%b()%s*",
"^%s*local%s+function%s+[%w%.:]+%b()%s*",
"^%s*function%s*%b()%s*",
}
for _, funcPattern in funcPatterns do
if previousText:match(funcPattern) == previousText then
canIndent = true
additionalTabs = 1
break
end
end
end
--previous indent case
if not canIndent and justReached then
local previousTabs = countTabs(previousText)
if previousTabs > 0 then
canIndent = true
additionalTabs = 0
end
end
if canIndent and previousLineNumber then
--print(lines)
--print(previousLineNumber)
local tabsCount = countTabs(lines[previousLineNumber]) + additionalTabs
--print(tabsCount)
--print(string.split(code:sub(1, position - 1) .. string.rep(" ", tabsCount) .. code:sub(position), "\n"))
data.Cursor += tabsCount
data.Code = code:sub(1, position - 1) .. string.rep("\9", tabsCount) .. code:sub(position)
end
end
local AutoWrap = {}
function isCharWhitespace(char)
return char == "\n" or char == " "
end
local allowedWraps = {
{"\"", "\""},
{"'", "'"},
{"(", ")"},
{"[", "]"},
{"{", "}"},
}
function getLastSelectedString(scriptEditor, autoWrapState)
local codeField = scriptEditor.Background.CodeField
autoWrapState._LastCursorPosition = codeField.CursorPosition
autoWrapState._LastSelectionStart = codeField.SelectionStart
local start = math.min(autoWrapState._LastCursorPosition, autoWrapState._LastSelectionStart)
local finish = math.max(autoWrapState._LastCursorPosition, autoWrapState._LastSelectionStart, 1) - 1
if start == -1 then
start = finish + 1
end
autoWrapState._LastSelectedString = string.sub(codeField.Text, start, finish)
autoWrapState._LastPreviousChar = string.sub(codeField.Text, start - 1, start - 1)
end
function AutoWrap.OnScriptEditorInstantiation(scriptEditor)
local codeField = scriptEditor.Background.CodeField
local autoWrapState = {
_LastSelectionStart = -1,
_LastCursorPosition = -1,
_LastSelectedString = "",
_LastPreviousChar = "",
}
scriptEditor._AutoWrapState = autoWrapState
codeField:GetPropertyChangedSignal("CursorPosition"):Connect(function()
task.defer(getLastSelectedString, scriptEditor, autoWrapState)
end)
codeField:GetPropertyChangedSignal("SelectionStart"):Connect(function()
task.defer(getLastSelectedString, scriptEditor, autoWrapState)
end)
end
function AutoWrap.OnScriptChange(scriptEditor, data)
if not scriptEditor.AutoWrapEnabled then
return
end
local autoWrapState = scriptEditor._AutoWrapState
local code = data.Code
local cursor = data.Cursor
local codeField: TextBox = scriptEditor.Background.CodeField
if codeField.CursorPosition ~= -1 then
-- local lastChar = string.sub(codeField.Text, codeField.CursorPosition - 1, codeField.CursorPosition - 1)
local lastCharPosition = codeField.CursorPosition - 1 + cursor
local lastChar = string.sub(code, lastCharPosition, lastCharPosition)
local lastSelectedString = autoWrapState._LastSelectedString
local cursorChar = string.sub(code, lastCharPosition + 1, lastCharPosition + 1)
local nextLastChar = string.sub(code, lastCharPosition - 1, lastCharPosition - 1)
for _, wrappedCharData in allowedWraps do
local startingWrappedChar = wrappedCharData[1]
if lastChar == startingWrappedChar and lastSelectedString ~= startingWrappedChar and lastSelectedString ~= "" and (autoWrapState._LastPreviousChar ~= startingWrappedChar or nextLastChar == startingWrappedChar) then
code = string.sub(code, 1, lastCharPosition) .. lastSelectedString .. wrappedCharData[2] .. string.sub(code, lastCharPosition + 1)
codeField.SelectionStart = if lastSelectedString ~= "" then lastCharPosition + 1 else -1
data.Cursor += lastSelectedString:len()
end
end
end
data.Code = code
end
local Storage = InitGui.storage()
Storage.Parent = script
local ScriptEditor = {}
ScriptEditor.__index = ScriptEditor
local lib_methods = {
bit32 = true,
coroutine = true,
debug = true,
math = true,
os = true,
string = true,
table = true,
utf8 = true,
task = true,
buffer = true,
Axes = 'builtin', BrickColor = 'builtin', CFrame = 'builtin', Color3 = 'builtin', ColorSequence = 'builtin', ColorSequenceKeypoint = 'builtin', DateTime = 'builtin', DockWidgetPluginGuiInfo = 'builtin',
Faces = 'builtin', Instance = 'builtin', NumberRange = 'builtin', NumberSequence = 'builtin', NumberSequenceKeypoint = 'builtin', PathWaypoint = 'builtin', PhysicalProperties = 'builtin', Random = 'builtin',
Ray = 'builtin', RaycastParams = 'builtin', Rect = 'builtin', Region3 = 'builtin', Region3int16 = 'builtin', TweenInfo = 'builtin', UDim = 'builtin', UDim2 = 'builtin', Vector2 = 'builtin',
Vector2int16 = 'builtin', Vector3 = 'builtin', Vector3int16 = 'builtin',
CatalogSearchParams = 'builtin', FloatCurveKey = 'builtin', Font = 'builtin', OverlapParams = 'builtin', SharedTable = 'builtin',
}
local HighlightColorsInfo = {
keyword = {"Keyword Color", true},
builtin = {"Built-in Function Color", true},
string = {"String Color", false},
number = {"Number Color", false},
comment = {"Comment Color", false},
lprop = {"Property Color", false},
lmethod = {"Method Color", false},
method = {"Function Name Color", false},
bool = {"Bool Color", true},
operator = {"Operator Color", false},
luau = {"Luau Keyword Color", true},
["nil"] = {"\"nil\" Color", true},
["function"] = {"\"function\" Color", true},
["local"] = {"\"local\" Color", true},
["self"] = {"\"self\" Color", true},
}
local tokenNameTypes = {
["local"] = "local",
["function"] = "function",
["self"] = "self",
["nil"] = "nil",
["true"] = "bool",
["false"] = "bool",
["type"] = "luau",
["typeof"] = "luau",
["export"] = "luau",
["continue"] = "luau",
}
local function replace(str, s, e, replacementString)
local before = string.sub(str, 1, s - 1)
local after = string.sub(str, e + 1, str:len())
return before .. replacementString .. after
end
local function rawWrapString(str, substr, Start, End, color, isBold)
local r, g, b = math.ceil(color.R*255), math.ceil(color.G*255), math.ceil(color.B*255)
local colorStrStart = ''
local bold = ''
local afterbold = ''
if isBold then
bold = ''
afterbold = ''
end
return replace(str, Start, End, colorStrStart .. bold .. substr .. afterbold .. '')
end
local function iterateThruAndReplace(str, pattern, replacement)
local cleanStr, replacements = string.gsub(str, pattern, replacement)
return cleanStr
end
local function trimWhitespace(str)
return string.gsub(str, '^%s*(.-)%s*$', '%1')
end
local function getTrueTokenData(i, tokenData, scanData)
local prevTokenData = scanData[i - 1]
local nextTokenData = scanData[i + 1]
local token = tokenData.token
if token == "iden" then
local prevIndexData = scanData[i - 2]
if prevTokenData and prevTokenData.src == "." then
token = "lprop"
end
if nextTokenData and (nextTokenData.src:match("^%(") or nextTokenData.src:match("^{") or nextTokenData.src:match("^\"") or nextTokenData.src:match("^\'") or nextTokenData.src:match("%[%[")) then
if prevTokenData and prevTokenData.src == "." then
token = "lmethod"
else
token = "method"
end
end
end
local tokenText = trimWhitespace(tokenData.src)
local trueToken = tokenNameTypes[tokenText]
if trueToken then
token = trueToken
end
return token
end
local function escapeRich(code)
local cleanCode = iterateThruAndReplace(code, "&", "&")
cleanCode = iterateThruAndReplace(cleanCode, "<", "<")
cleanCode = iterateThruAndReplace(cleanCode, ">", ">")
cleanCode = iterateThruAndReplace(cleanCode, "\"", """)
cleanCode = iterateThruAndReplace(cleanCode, "\'", "'")
return cleanCode
end
local function tabsToSpaces(code)
return iterateThruAndReplace(code, "\9", " ")
end
local function spacesToTabs(code)
return iterateThruAndReplace(code, " ", "\9")
end
local function colorify(code, theme)
theme = theme or DefaultTheme
local highlight = code
local size = code:len()
local increase = 0
local scanData = Lexer.run(code)
for i, tokenData in scanData do
local token = tokenData.token
token = getTrueTokenData(i, tokenData, scanData)
local colorData = theme[token]
if colorData then
local trim = tokenData.trim
local src = tokenData.src
local End = trim - 1 + increase
local Start = trim - src:len() + increase
local color = colorData.color
local isBold = colorData.isBold
local escapedToken = escapeRich(src)
highlight = rawWrapString(highlight, escapedToken, Start, End, color, isBold)
increase = math.abs(highlight:len() - size)
end
end
return highlight
end
local registerEditEvent = true
local function rawEditCodeField(scriptEditor, text)
registerEditEvent = false
scriptEditor.Background.CodeField.Text = text
task.defer(function()
registerEditEvent = true
end)
end
local function fixCodeFieldLines(scriptEditor, originalSize)
local codeField = scriptEditor.Background.CodeField
local newCode = codeField.Text
if scriptEditor.VisibleLines > originalSize then
for i = originalSize + 1, scriptEditor.VisibleLines do
local line = scriptEditor.SourceData.Code[scriptEditor.LineFocused + i - 1]
if i > scriptEditor.SourceData.Size then
break
end
if not line then
break
end
newCode ..= "\n" .. line
end
elseif scriptEditor.VisibleLines < originalSize then
local lines = string.split(newCode, "\n")
local replacementText = lines[1] or ""
for i = 2, scriptEditor.VisibleLines do
if i + scriptEditor.LineFocused - 1 > scriptEditor.SourceData.Size then
break
end
replacementText = replacementText .. "\n" .. (lines[i] or "")
end
newCode = replacementText
end
return newCode
end
local function declareCurrentTextSize(scriptEditor, textWidth)
local codeField = scriptEditor.Background.CodeField
local richOverlayContainer = scriptEditor.Background.RichOverlayContainer
local textHeight = textWidth*2
local genericSize = codeField.Size
local currentWidth = codeField.AbsoluteSize.X
local currentHeight = codeField.AbsoluteSize.Y
local newSize = UDim2.new(1, genericSize.X.Offset - currentWidth%textWidth + 1, 1, -10 - currentHeight%textHeight)
codeField.Size = newSize
richOverlayContainer.Size = newSize
local originalSize = scriptEditor.VisibleLines
recountVisibleLines(scriptEditor)
codeField.Text = fixCodeFieldLines(scriptEditor, originalSize)
end
local function moveShiftContainer(scriptEditor)
local background = scriptEditor.Background
local codeField = background.CodeField
local shiftContainer = background.RichOverlayContainer.ShiftContainer
scriptEditor.ScrollingShift = GetTextBoxScrolling.UpdateShift(codeField, codeField.CursorPosition, scriptEditor.ScrollingShift)
shiftContainer.Position = UDim2.new(0, -scriptEditor.ScrollingShift, 0, 0)
end
local function updateLines(scriptEditor)
local lineNumbers = {}
local visibleCodeLines = {}
table.move(scriptEditor.SourceData.Code, scriptEditor.LineFocused, scriptEditor.LineFocused + scriptEditor.VisibleLines, 1, visibleCodeLines)
for i = 1, scriptEditor.VisibleLines + 1 do
local line = scriptEditor.SourceData.Code[scriptEditor.LineFocused + i - 1]
if not line then
break
end
table.insert(lineNumbers, i + scriptEditor.LineFocused - 1)
end
local visibleCodeSegment = table.concat(visibleCodeLines, "\n")
scriptEditor.Background.LineNumberContainer.LineNumber.Text = table.concat(lineNumbers, "\n")
scriptEditor.Background.RichOverlayContainer.ShiftContainer.RichOverlayLabel.Text = colorify(tabsToSpaces(visibleCodeSegment), scriptEditor.Theme)
end
local function onCodeFieldEdit(scriptEditor)
if not registerEditEvent then
registerEditEvent = true
return
end
local background = scriptEditor.Background
local codeField = background.CodeField
local newText = codeField.Text
local lines = string.split(newText, "\n")
local finalRawCode = newText
task.defer(function()
local lineNumberWidth = 6*math.ceil(math.log10(#scriptEditor.SourceData.Code + .1))
background.LineNumberContainer.Size = UDim2.new(0, lineNumberWidth + 3, 1, -10)
background.LineNumberBackground.Size = UDim2.new(0, lineNumberWidth + 6, 1, 0)
codeField.Position = UDim2.new(0, lineNumberWidth + 9, 0, 5)
codeField.Size = UDim2.new(1, -(lineNumberWidth + 9 + 5), 1, -10)
background.RichOverlayContainer.Position = UDim2.new(0, lineNumberWidth + 9, 0, 5)
background.RichOverlayContainer.Size = UDim2.new(1, -(lineNumberWidth + 9 + 5), 1, -10)
declareCurrentTextSize(scriptEditor, 7)
end)
local luaArray = string.split(newText, "\n")
scriptEditor.SourceData:Write(scriptEditor.LineFocused, scriptEditor.VisibleLines, luaArray)
scriptEditor.RawSource = scriptEditor.SourceData:ToString()
updateLines(scriptEditor)
local originalSize = scriptEditor.VisibleLines
finalRawCode = fixCodeFieldLines(scriptEditor, #lines)
local data = {
Code = finalRawCode,
Gain = finalRawCode:len() - scriptEditor.DisplayCode:len(),
Cursor = 0,
Selection = 0,
}
AutoIndent.OnScriptChange(scriptEditor, data)
AutoWrap.OnScriptChange(scriptEditor, data)
finalRawCode = data.Code
if finalRawCode ~= newText then
local prevPosition = codeField.CursorPosition
codeField.CursorPosition = -1
codeField.Text = finalRawCode
codeField.CursorPosition = prevPosition + data.Cursor
else
scriptEditor.OnEdit:Fire(scriptEditor.RawSource)
end
scriptEditor.DisplayCode = finalRawCode
end
local function repack(t, separator)
local str = t[1]
for i = 2, #t do
if not t[i] then
end
str = str .. separator .. t[i]
end
return str
end
function recountVisibleLines(scriptEditor)
local visibleLines = math.floor(scriptEditor.Background.CodeField.AbsoluteSize.Y/14)
scriptEditor.VisibleLines = visibleLines
end
function getLastSelectedString(scriptEditor)
local codeField = scriptEditor.Background.CodeField
scriptEditor._LastCursorPosition = codeField.CursorPosition
scriptEditor._LastSelectionStart = codeField.SelectionStart
local start = math.min(scriptEditor._LastCursorPosition, scriptEditor._LastSelectionStart)
local finish = math.max(scriptEditor._LastCursorPosition, scriptEditor._LastSelectionStart, 1) - 1
if start == -1 then
start = finish + 1
end
scriptEditor._LastSelectedString = string.sub(codeField.Text, start, finish)
scriptEditor._LastPreviousChar = string.sub(codeField.Text, start - 1, start - 1)
end
function ScriptEditor:ApplyTheme(ThemeData)
ThemeData = ThemeData or self.Theme
self.Theme = ThemeData
local background = self.Background
if #background:GetChildren() == 0 then
return
end
background.BackgroundTransparency = 1
background.LineNumberBackground.BackgroundTransparency = 1
background.CodeField.TextColor3 = ThemeData.text
Storage.LineNumber.TextColor3 = ThemeData.text
Storage.RichOverlayLabel.TextColor3 = ThemeData.text
for _, lineLabel: Instance in pairs(background.LineNumberContainer:GetChildren()) do
if lineLabel:IsA("TextLabel") then
lineLabel.TextColor3 = ThemeData.text
end
end
for _, lineLabel: Instance in pairs(background.RichOverlayContainer.ShiftContainer:GetChildren()) do
if lineLabel:IsA("TextLabel") then
lineLabel.TextColor3 = ThemeData.text
end
end
self:JumpTo()
end
function ScriptEditor:ApplyStudioTheme(studio: Studio)
local theme = {
background = studio["Background Color"],
text = studio["Text Color"],
sidebar = studio["Script Editor Scrollbar Background Color"],
}
for ThemePropertyName, highlightData in HighlightColorsInfo do
theme[ThemePropertyName] = {
color = studio[highlightData[1]],
isBold = highlightData[2]
}
end
self:ApplyTheme(theme)
end
function ScriptEditor:JumpTo(lineNumber: number)
lineNumber = lineNumber or self.LineFocused
self.LineFocused = lineNumber
local lines = string.split(self.RawSource, "\n")
local displayCode = ""
for i = lineNumber, lineNumber + self.VisibleLines - 1 do
local line = lines[i]
if line then
displayCode = displayCode .. line .. "\n"
end
end
displayCode = string.sub(displayCode, 1, displayCode:len() - 1)
displayCode = displayCode
if self.Background.CodeField.Text == displayCode then
onCodeFieldEdit(self)
else
self.Background.CodeField.Text = displayCode
end
end
function ScriptEditor:SetStringAsync(str: string, lineNumber: number)
self.Background.CodeField.Visible = false
self.Background.RichOverlayContainer.Visible = false
self.Background.LineNumberContainer.Visible = false
task.delay(1/30, function()
recountVisibleLines(self)
self.RawSource = str
self.SourceData = LuaTable.FromString(str)
self.Background.CodeField.Visible = true
self.Background.RichOverlayContainer.Visible = true
self.Background.LineNumberContainer.Visible = true
self:JumpTo(lineNumber or 1)
end)
end
function ScriptEditor:SetScriptAsync(scriptObject: LuaSourceContainer, lineNumber: number)
self:SetStringAsync(scriptObject.Source, lineNumber)
end
function ScriptEditor:Unload()
self:SetStringAsync("")
end
function ScriptEditor:ReadOnly(allowEditing: boolean?)
self.Background.CodeField.TextEditable = allowEditing or false
end
function ScriptEditor.Embed(frame: GuiBase2d)
local background = InitGui.init()
background.Parent = frame
local scriptEditor = {
LineFocused = 1,
RawSource = "",
SourceData = LuaTable.FromString(""),
DisplayCode = "",
Background = background,
VisibleLines = 1,
OutputScript = nil,
AutoWrapEnabled = false,
ScrollingShift = 0,
Destroyed = false,
OnEdit = SignalModule.new(),
Theme = DefaultTheme,
}
Storage.LineNumber:Clone().Parent = background.LineNumberContainer
Storage.RichOverlayLabel:Clone().Parent = background.RichOverlayContainer.ShiftContainer
setmetatable(scriptEditor, ScriptEditor)
local codeField = background.CodeField
codeField:GetPropertyChangedSignal("Text"):Connect(function()
onCodeFieldEdit(scriptEditor)
end)
codeField:GetPropertyChangedSignal("CursorPosition"):Connect(function()
task.defer(moveShiftContainer, scriptEditor)
end)
background:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
local originalSize = scriptEditor.VisibleLines
recountVisibleLines(scriptEditor)
codeField.Text = fixCodeFieldLines(scriptEditor, originalSize)
updateLines(scriptEditor)
task.defer(moveShiftContainer, scriptEditor)
end)
codeField.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseWheel then
local roundZ = math.round(input.Position.Z)
local newLineFocused = math.clamp(scriptEditor.LineFocused - roundZ*3, 1, #string.split(scriptEditor.RawSource, "\n"))
codeField:ReleaseFocus()
scriptEditor:JumpTo(newLineFocused)
end
end)
AutoWrap.OnScriptEditorInstantiation(scriptEditor)
scriptEditor:SetStringAsync("")
return scriptEditor
end
function ScriptEditor:Relocate(newFrame: GuiBase2d)
local background = self.Background
background.Parent = newFrame
end
function ScriptEditor:Destroy()
if self.Destroyed then
return
end
self.Destroyed = true
self.Background:Destroy()
self.OnEdit:DisconnectAll()
end
local HttpService: HttpService = game:GetService("HttpService")
local ImGui = {} :: ImGui
ImGui.TemplateConfig = {
colorDark = { -- Dear, ImGui default dark
TextColor = Color3.fromRGB(255, 255, 255),
TextTransparency = 0,
TextDisabledColor = Color3.fromRGB(128, 128, 128),
TextDisabledTransparency = 0,
-- Dear ImGui uses 110, 110, 125
-- The Roblox window selection highlight is 67, 191, 254
BorderColor = Color3.fromRGB(110, 110, 125),
BorderActiveColor = Color3.fromRGB(160, 160, 175), -- does not exist in Dear ImGui
-- BorderTransparency will be problematic for non UIStroke border implimentations
-- is not implimented because of this
BorderTransparency = 0.5,
BorderActiveTransparency = 0.3,
WindowBgColor = Color3.fromRGB(15, 15, 15),
WindowBgTransparency = 0.06,
PopupBgColor = Color3.fromRGB(20, 20, 20),
PopupBgTransparency = 0.06,
ScrollbarGrabColor = Color3.fromRGB(128, 128, 128),
ScrollbarGrabTransparency = 0,
TitleBgColor = Color3.fromRGB(10, 10, 10),
TitleBgTransparency = 0,
TitleBgActiveColor = Color3.fromRGB(41, 74, 122),
TitleBgActiveTransparency = 0,
TitleBgCollapsedColor = Color3.fromRGB(0, 0, 0),
TitleBgCollapsedTransparency = 0.5,
MenubarBgColor = Color3.fromRGB(36, 36, 36),
MenubarBgTransparency = 0,
FrameBgColor = Color3.fromRGB(41, 74, 122),
FrameBgTransparency = 0.46,
FrameBgHoveredColor = Color3.fromRGB(66, 150, 250),
FrameBgHoveredTransparency = 0.46,
FrameBgActiveColor = Color3.fromRGB(66, 150, 250),
FrameBgActiveTransparency = 0.33,
ButtonColor = Color3.fromRGB(66, 150, 250),
ButtonTransparency = 0.6,
ButtonHoveredColor = Color3.fromRGB(66, 150, 250),
ButtonHoveredTransparency = 0,
ButtonActiveColor = Color3.fromRGB(15, 135, 250),
ButtonActiveTransparency = 0,
ImageColor = Color3.fromRGB(255, 255, 255),
ImageTransparency = 0,
SliderGrabColor = Color3.fromRGB(66, 150, 250),
SliderGrabTransparency = 0,
SliderGrabActiveColor = Color3.fromRGB(117, 138, 204),
SliderGrabActiveTransparency = 0,
HeaderColor = Color3.fromRGB(66, 150, 250),
HeaderTransparency = 0.69,
HeaderHoveredColor = Color3.fromRGB(66, 150, 250),
HeaderHoveredTransparency = 0.2,
HeaderActiveColor = Color3.fromRGB(66, 150, 250),
HeaderActiveTransparency = 0,
TabColor = Color3.fromRGB(46, 89, 148),
TabTransparency = 0.14,
TabHoveredColor = Color3.fromRGB(66, 150, 250),
TabHoveredTransparency = 0.2,
TabActiveColor = Color3.fromRGB(51, 105, 173),
TabActiveTransparency = 0,
SelectionImageObjectColor = Color3.fromRGB(255, 255, 255),
SelectionImageObjectTransparency = 0.8,
SelectionImageObjectBorderColor = Color3.fromRGB(255, 255, 255),
SelectionImageObjectBorderTransparency = 0,
TableBorderStrongColor = Color3.fromRGB(79, 79, 89),
TableBorderStrongTransparency = 0,
TableBorderLightColor = Color3.fromRGB(59, 59, 64),
TableBorderLightTransparency = 0,
TableRowBgColor = Color3.fromRGB(0, 0, 0),
TableRowBgTransparency = 1,
TableRowBgAltColor = Color3.fromRGB(255, 255, 255),
TableRowBgAltTransparency = 0.94,
NavWindowingHighlightColor = Color3.fromRGB(255, 255, 255),
NavWindowingHighlightTransparency = 0.3,
NavWindowingDimBgColor = Color3.fromRGB(204, 204, 204),
NavWindowingDimBgTransparency = 0.65,
SeparatorColor = Color3.fromRGB(110, 110, 128),
SeparatorTransparency = 0.5,
CheckMarkColor = Color3.fromRGB(66, 150, 250),
CheckMarkTransparency = 0,
PlotHistogramColor = Color3.fromRGB(230, 179, 0),
PlotHistogramTransparency = 0,
PlotHistogramHoveredColor = Color3.fromRGB(255, 153, 0),
PlotHistogramHoveredTransparency = 0,
},
colorLight = { -- Dear, ImGui default light
TextColor = Color3.fromRGB(0, 0, 0),
TextTransparency = 0,
TextDisabledColor = Color3.fromRGB(153, 153, 153),
TextDisabledTransparency = 0,
BorderColor = Color3.fromRGB(64, 64, 64),
BorderActiveColor = Color3.fromRGB(64, 64, 64),
BorderTransparency = 0.5,
BorderActiveTransparency = 0.2,
WindowBgColor = Color3.fromRGB(240, 240, 240),
WindowBgTransparency = 0,
PopupBgColor = Color3.fromRGB(255, 255, 255),
PopupBgTransparency = 0.02,
TitleBgColor = Color3.fromRGB(245, 245, 245),
TitleBgTransparency = 0,
TitleBgActiveColor = Color3.fromRGB(209, 209, 209),
TitleBgActiveTransparency = 0,
TitleBgCollapsedColor = Color3.fromRGB(255, 255, 255),
TitleBgCollapsedTransparency = 0.5,
MenubarBgColor = Color3.fromRGB(219, 219, 219),
MenubarBgTransparency = 0,
ScrollbarGrabColor = Color3.fromRGB(96, 96, 96),
ScrollbarGrabTransparency = 0,
FrameBgColor = Color3.fromRGB(255, 255, 255),
FrameBgTransparency = 0.6,
FrameBgHoveredColor = Color3.fromRGB(66, 150, 250),
FrameBgHoveredTransparency = 0.6,
FrameBgActiveColor = Color3.fromRGB(66, 150, 250),
FrameBgActiveTransparency = 0.33,
ButtonColor = Color3.fromRGB(66, 150, 250),
ButtonTransparency = 0.6,
ButtonHoveredColor = Color3.fromRGB(66, 150, 250),
ButtonHoveredTransparency = 0,
ButtonActiveColor = Color3.fromRGB(15, 135, 250),
ButtonActiveTransparency = 0,
ImageColor = Color3.fromRGB(255, 255, 255),
ImageTransparency = 0,
HeaderColor = Color3.fromRGB(66, 150, 250),
HeaderTransparency = 0.31,
HeaderHoveredColor = Color3.fromRGB(66, 150, 250),
HeaderHoveredTransparency = 0.2,
HeaderActiveColor = Color3.fromRGB(66, 150, 250),
HeaderActiveTransparency = 0,
TabColor = Color3.fromRGB(195, 203, 213),
TabTransparency = 0.07,
TabHoveredColor = Color3.fromRGB(66, 150, 250),
TabHoveredTransparency = 0.2,
TabActiveColor = Color3.fromRGB(152, 186, 255),
TabActiveTransparency = 0,
SliderGrabColor = Color3.fromRGB(61, 133, 224),
SliderGrabTransparency = 0,
SliderGrabActiveColor = Color3.fromRGB(66, 150, 250),
SliderGrabActiveTransparency = 0,
SelectionImageObjectColor = Color3.fromRGB(0, 0, 0),
SelectionImageObjectTransparency = 0.8,
SelectionImageObjectBorderColor = Color3.fromRGB(0, 0, 0),
SelectionImageObjectBorderTransparency = 0,
TableBorderStrongColor = Color3.fromRGB(145, 145, 163),
TableBorderStrongTransparency = 0,
TableBorderLightColor = Color3.fromRGB(173, 173, 189),
TableBorderLightTransparency = 0,
TableRowBgColor = Color3.fromRGB(0, 0, 0),
TableRowBgTransparency = 1,
TableRowBgAltColor = Color3.fromRGB(77, 77, 77),
TableRowBgAltTransparency = 0.91,
NavWindowingHighlightColor = Color3.fromRGB(179, 179, 179),
NavWindowingHighlightTransparency = 0.3,
NavWindowingDimBgColor = Color3.fromRGB(51, 51, 51),
NavWindowingDimBgTransparency = 0.8,
SeparatorColor = Color3.fromRGB(99, 99, 99),
SeparatorTransparency = 0.38,
CheckMarkColor = Color3.fromRGB(66, 150, 250),
CheckMarkTransparency = 0,
PlotHistogramColor = Color3.fromRGB(230, 179, 0),
PlotHistogramTransparency = 0,
PlotHistogramHoveredColor = Color3.fromRGB(255, 153, 0),
PlotHistogramHoveredTransparency = 0,
},
sizeDefault = { -- Dear, ImGui default
ItemWidth = UDim.new(1, 0),
ContentWidth = UDim.new(0.65, 0),
ContentHeight = UDim.new(0, 0),
WindowPadding = Vector2.new(8, 8),
WindowResizePadding = Vector2.new(6, 6),
FramePadding = Vector2.new(4, 3),
ItemSpacing = Vector2.new(8, 4),
ItemInnerSpacing = Vector2.new(4, 4),
CellPadding = Vector2.new(4, 2),
DisplaySafeAreaPadding = Vector2.new(0, 0),
SeparatorTextPadding = Vector2.new(20, 3),
IndentSpacing = 21,
TextFont = Font.fromEnum(Enum.Font.Code),
TextSize = 13,
FrameBorderSize = 0,
FrameRounding = 0,
GrabRounding = 0,
WindowRounding = 0, -- these don't actually work but it's nice to have them.
WindowBorderSize = 1,
WindowTitleAlign = Enum.LeftRight.Left,
PopupBorderSize = 1,
PopupRounding = 0,
ScrollbarSize = 7,
GrabMinSize = 10,
SeparatorTextBorderSize = 3,
ImageBorderSize = 2,
},
sizeClear = { -- easier to read and manuveure
ItemWidth = UDim.new(1, 0),
ContentWidth = UDim.new(0.65, 0),
ContentHeight = UDim.new(0, 0),
WindowPadding = Vector2.new(12, 8),
WindowResizePadding = Vector2.new(8, 8),
FramePadding = Vector2.new(6, 4),
ItemSpacing = Vector2.new(8, 8),
ItemInnerSpacing = Vector2.new(8, 8),
CellPadding = Vector2.new(4, 4),
DisplaySafeAreaPadding = Vector2.new(8, 8),
SeparatorTextPadding = Vector2.new(24, 6),
IndentSpacing = 25,
TextFont = Font.fromEnum(Enum.Font.Ubuntu),
TextSize = 15,
FrameBorderSize = 1,
FrameRounding = 4,
GrabRounding = 4,
WindowRounding = 4,
WindowBorderSize = 1,
WindowTitleAlign = Enum.LeftRight.Center,
PopupBorderSize = 1,
PopupRounding = 4,
ScrollbarSize = 9,
GrabMinSize = 14,
SeparatorTextBorderSize = 4,
ImageBorderSize = 4,
},
utilityDefault = {
UseScreenGUIs = true,
IgnoreGuiInset = false,
Parent = nil,
RichText = false,
TextWrapped = false,
DisplayOrderOffset = 127,
ZIndexOffset = 0,
MouseDoubleClickTime = 0.30, -- Time for a double-click, in seconds.
MouseDoubleClickMaxDist = 6.0, -- Distance threshold to stay in to validate a double-click, in pixels.
HoverColor = Color3.fromRGB(255, 255, 0),
HoverTransparency = 0.1,
},
}
for i, v in ImGui.TemplateConfig.colorDark do
if typeof(v) == "Color3" then
local h, s, v = Color3.toHSV(v)
ImGui.TemplateConfig.colorDark[i] = Color3.fromHSV(1, s, v)
--print(Color3.fromHSV(0.139222, 1, 1))
end
end
local function Internal(ImGui: ImGui): Internal
local Internal = {} :: Internal
Internal._version = [[ 2.3.1 ]]
Internal._started = false
Internal._shutdown = false
Internal._cycleTick = 0
Internal._globalRefreshRequested = false
Internal._localRefreshActive = false
Internal._widgets = {}
Internal._stackIndex = 1
Internal._rootInstance = nil
Internal._rootWidget = {
ID = "R",
type = "Root",
Instance = Internal._rootInstance,
ZIndex = 0,
ZOffset = 0,
}
Internal._lastWidget = Internal._rootWidget
Internal._rootConfig = {}
Internal._config = Internal._rootConfig
Internal._IDStack = { "R" }
Internal._usedIDs = {}
Internal._pushedId = nil
Internal._nextWidgetId = nil
Internal._states = {}
Internal._postCycleCallbacks = {}
Internal._connectedFunctions = {}
Internal._connections = {}
Internal._initFunctions = {}
Internal._fullErrorTracebacks = game:GetService("RunService"):IsStudio()
Internal._cycleCoroutine = coroutine.create(function()
while Internal._started do
for _, callback: () -> string in Internal._connectedFunctions do
--debug.profilebegin("ImGui/Connection")
local status: boolean, _error: string = pcall(callback)
--debug.profileend()
if not status then
Internal._stackIndex = 1
coroutine.yield(false, _error)
end
end
coroutine.yield(true)
end
end)
local StateClass = {}
StateClass.__index = StateClass
function StateClass:get(): T
return self.value
end
function StateClass:set(newValue: T): T
if newValue == self.value then
return self.value
end
self.value = newValue
for _, thisWidget: Widget in self.ConnectedWidgets do
Internal._widgets[thisWidget.type].UpdateState(thisWidget)
end
for _, callback in self.ConnectedFunctions do
callback(newValue)
end
return self.value
end
function StateClass:onChange(callback: (newValue: T) -> ()): () -> ()
local connectionIndex: number = #self.ConnectedFunctions + 1
self.ConnectedFunctions[connectionIndex] = callback
return function()
self.ConnectedFunctions[connectionIndex] = nil
end
end
Internal.StateClass = StateClass
function Internal._cycle()
if ImGui.Disabled then
return
end
Internal._rootWidget.lastCycleTick = Internal._cycleTick
if Internal._rootInstance == nil or Internal._rootInstance.Parent == nil then
ImGui.ForceRefresh()
end
for _, widget: Widget in Internal._lastVDOM do
if widget.lastCycleTick ~= Internal._cycleTick and (widget.lastCycleTick ~= -1) then
Internal._DiscardWidget(widget)
end
end
setmetatable(Internal._lastVDOM, { __mode = "kv" })
Internal._lastVDOM = Internal._VDOM
Internal._VDOM = Internal._generateEmptyVDOM()
task.spawn(function()
for _, callback: () -> () in Internal._postCycleCallbacks do
callback()
end
end)
if Internal._globalRefreshRequested then
Internal._generateSelectionImageObject()
Internal._globalRefreshRequested = false
for _, widget: Widget in Internal._lastVDOM do
Internal._DiscardWidget(widget)
end
Internal._generateRootInstance()
Internal._lastVDOM = Internal._generateEmptyVDOM()
end
Internal._cycleTick += 1
table.clear(Internal._usedIDs)
local compatibleParent: boolean = (Internal.parentInstance:IsA("GuiBase2d") or Internal.parentInstance:IsA("CoreGui") or Internal.parentInstance:IsA("PluginGui") or Internal.parentInstance:IsA("PlayerGui"))
if compatibleParent == false then
error("ImGui Parent Instance cant contain GUI")
end
if Internal._fullErrorTracebacks then
for _, callback: () -> () in Internal._connectedFunctions do
callback()
end
else
local coroutineStatus = coroutine.status(Internal._cycleCoroutine)
if coroutineStatus == "suspended" then
local _, success, result = coroutine.resume(Internal._cycleCoroutine)
if success == false then
error(result, 0)
end
elseif coroutineStatus == "running" then
error("ImGui cycleCoroutine took to long to yield. Connected functions should not yield.")
else
error("unrecoverable state")
end
end
if Internal._stackIndex ~= 1 then
Internal._stackIndex = 1
--error("Callback has too few calls to ImGui.End()", 0)
end
end
function Internal._NoOp() end
function Internal.WidgetConstructor(type: string, widgetClass: WidgetClass)
local Fields: { [string]: { [string]: { string } } } = {
All = {
Required = {
"Generate",
"Discard",
"Update",
"Args",
"Events",
"hasChildren",
"hasState",
},
Optional = {},
},
IfState = {
Required = {
"GenerateState",
"UpdateState",
},
Optional = {},
},
IfChildren = {
Required = {
"ChildAdded",
},
Optional = {
"ChildDiscarded",
},
},
}
local thisWidget = {} :: WidgetClass
for _, field: string in Fields.All.Required do
assert(widgetClass[field] ~= nil, `field {field} is missing from widget {type}, it is required for all widgets`)
thisWidget[field] = widgetClass[field]
end
for _, field: string in Fields.All.Optional do
if widgetClass[field] == nil then
thisWidget[field] = Internal._NoOp
else
thisWidget[field] = widgetClass[field]
end
end
if widgetClass.hasState then
for _, field: string in Fields.IfState.Required do
assert(widgetClass[field] ~= nil, `field {field} is missing from widget {type}, it is required for all widgets with state`)
thisWidget[field] = widgetClass[field]
end
for _, field: string in Fields.IfState.Optional do
if widgetClass[field] == nil then
thisWidget[field] = Internal._NoOp
else
thisWidget[field] = widgetClass[field]
end
end
end
if widgetClass.hasChildren then
for _, field: string in Fields.IfChildren.Required do
assert(widgetClass[field] ~= nil, `field {field} is missing from widget {type}, it is required for all widgets with children`)
thisWidget[field] = widgetClass[field]
end
for _, field: string in Fields.IfChildren.Optional do
if widgetClass[field] == nil then
thisWidget[field] = Internal._NoOp
else
thisWidget[field] = widgetClass[field]
end
end
end
Internal._widgets[type] = thisWidget
ImGui.Args[type] = thisWidget.Args
local ArgNames: { [number]: string } = {}
for index: string, argument: number in thisWidget.Args do
ArgNames[argument] = index
end
thisWidget.ArgNames = ArgNames
for index: string, _ in thisWidget.Events do
if ImGui.Events[index] == nil then
ImGui.Events[index] = function()
return Internal._EventCall(Internal._lastWidget, index)
end
end
end
end
function Internal._Insert(widgetType: string, args: WidgetArguments?, states: WidgetStates?): Widget
local ID: ID = Internal._getID(3)
local thisWidgetClass: WidgetClass = Internal._widgets[widgetType]
if Internal._VDOM[ID] then
return Internal._ContinueWidget(ID, widgetType)
end
local arguments: Arguments = {} :: Arguments
if args ~= nil then
if type(args) ~= "table" then
args = { args }
end
for index: number, argument: Argument in args do
arguments[thisWidgetClass.ArgNames[index]] = argument
end
end
table.freeze(arguments)
local lastWidget: Widget? = Internal._lastVDOM[ID]
if lastWidget and widgetType == lastWidget.type then
if Internal._localRefreshActive then
Internal._DiscardWidget(lastWidget)
lastWidget = nil
end
end
local thisWidget: Widget = if lastWidget == nil then Internal._GenNewWidget(widgetType, arguments, states, ID) else lastWidget
local parentWidget: ParentWidget = thisWidget.parentWidget
if thisWidget.type ~= "Window" and thisWidget.type ~= "Tooltip" then
if thisWidget.ZIndex ~= parentWidget.ZOffset then
parentWidget.ZUpdate = true
end
if parentWidget.ZUpdate then
thisWidget.ZIndex = parentWidget.ZOffset
if thisWidget.Instance then
thisWidget.Instance.ZIndex = thisWidget.ZIndex
thisWidget.Instance.LayoutOrder = thisWidget.ZIndex
end
end
end
if Internal._deepCompare(thisWidget.providedArguments, arguments) == false then
thisWidget.arguments = Internal._deepCopy(arguments)
thisWidget.providedArguments = arguments
thisWidgetClass.Update(thisWidget)
end
thisWidget.lastCycleTick = Internal._cycleTick
parentWidget.ZOffset += 1
if thisWidgetClass.hasChildren then
local thisParent = thisWidget :: ParentWidget
thisParent.ZOffset = 0
thisParent.ZUpdate = false
Internal._stackIndex += 1
Internal._IDStack[Internal._stackIndex] = thisWidget.ID
end
Internal._VDOM[ID] = thisWidget
Internal._lastWidget = thisWidget
return thisWidget
end
function Internal._GenNewWidget(widgetType: string, arguments: Arguments, states: WidgetStates?, ID: ID): Widget
local parentId: ID = Internal._IDStack[Internal._stackIndex]
local parentWidget: ParentWidget = Internal._VDOM[parentId]
local thisWidgetClass: WidgetClass = Internal._widgets[widgetType]
local thisWidget = {} :: Widget
setmetatable(thisWidget, thisWidget)
thisWidget.ID = ID
thisWidget.type = widgetType
thisWidget.parentWidget = parentWidget
thisWidget.trackedEvents = {}
thisWidget.UID = HttpService:GenerateGUID(false):sub(0, 8)
thisWidget.ZIndex = parentWidget.ZOffset
thisWidget.Instance = thisWidgetClass.Generate(thisWidget)
parentWidget = thisWidget.parentWidget
if Internal._config.Parent then
thisWidget.Instance.Parent = Internal._config.Parent
else
thisWidget.Instance.Parent = Internal._widgets[parentWidget.type].ChildAdded(parentWidget, thisWidget)
end
thisWidget.providedArguments = arguments
thisWidget.arguments = Internal._deepCopy(arguments)
thisWidgetClass.Update(thisWidget)
local eventMTParent
if thisWidgetClass.hasState then
local stateWidget = thisWidget :: StateWidget
if states then
for index: string, state: State in states do
if not (type(state) == "table" and getmetatable(state :: any) == Internal.StateClass) then
states[index] = Internal._widgetState(stateWidget, index, state)
end
end
stateWidget.state = states
for _, state: State in states do
state.ConnectedWidgets[stateWidget.ID] = stateWidget
end
else
stateWidget.state = {}
end
thisWidgetClass.GenerateState(stateWidget)
thisWidgetClass.UpdateState(stateWidget)
stateWidget.stateMT = {}
setmetatable(stateWidget.state, stateWidget.stateMT)
stateWidget.__index = stateWidget.state
eventMTParent = stateWidget.stateMT
else
eventMTParent = thisWidget
end
eventMTParent.__index = function(_, eventName: string)
return function()
return Internal._EventCall(thisWidget, eventName)
end
end
return thisWidget
end
function Internal._ContinueWidget(ID: ID, widgetType: string): Widget
local thisWidgetClass: WidgetClass = Internal._widgets[widgetType]
local thisWidget: Widget = Internal._VDOM[ID]
if thisWidgetClass.hasChildren then
Internal._stackIndex += 1
Internal._IDStack[Internal._stackIndex] = thisWidget.ID
end
Internal._lastWidget = thisWidget
return thisWidget
end
function Internal._DiscardWidget(widgetToDiscard: Widget)
local widgetParent = widgetToDiscard.parentWidget
if widgetParent then
Internal._widgets[widgetParent.type].ChildDiscarded(widgetParent, widgetToDiscard)
end
Internal._widgets[widgetToDiscard.type].Discard(widgetToDiscard)
widgetToDiscard.lastCycleTick = -1
end
function Internal._widgetState(thisWidget: StateWidget, stateName: string, initialValue: any): State
local ID: ID = thisWidget.ID .. stateName
if Internal._states[ID] then
Internal._states[ID].ConnectedWidgets[thisWidget.ID] = thisWidget
return Internal._states[ID]
else
Internal._states[ID] = {
ID = ID,
value = initialValue,
ConnectedWidgets = { [thisWidget.ID] = thisWidget },
ConnectedFunctions = {},
}
setmetatable(Internal._states[ID], Internal.StateClass)
return Internal._states[ID]
end
end
function Internal._EventCall(thisWidget: Widget, eventName: string): boolean
local Events: Events = Internal._widgets[thisWidget.type].Events
local Event: Event = Events[eventName]
assert(Event ~= nil, `widget {thisWidget.type} has no event of name {eventName}`)
if thisWidget.trackedEvents[eventName] == nil then
Event.Init(thisWidget)
thisWidget.trackedEvents[eventName] = true
end
return Event.Get(thisWidget)
end
function Internal._GetParentWidget(): ParentWidget
return Internal._VDOM[Internal._IDStack[Internal._stackIndex]]
end
function Internal._generateEmptyVDOM(): { [ID]: Widget }
return {
["R"] = Internal._rootWidget,
}
end
function Internal._generateRootInstance()
Internal._rootInstance = Internal._widgets["Root"].Generate(Internal._widgets["Root"])
Internal._rootInstance.Parent = Internal.parentInstance
Internal._rootWidget.Instance = Internal._rootInstance
end
function Internal._generateSelectionImageObject()
if Internal.SelectionImageObject then
Internal.SelectionImageObject:Destroy()
end
local SelectionImageObject: Frame = Instance.new("Frame")
SelectionImageObject.Position = UDim2.fromOffset(-1, -1)
SelectionImageObject.Size = UDim2.new(1, 2, 1, 2)
SelectionImageObject.BackgroundColor3 = Internal._config.SelectionImageObjectColor
SelectionImageObject.BackgroundTransparency = Internal._config.SelectionImageObjectTransparency
SelectionImageObject.BorderSizePixel = 0
local UIStroke: UIStroke = Instance.new("UIStroke")
UIStroke.Thickness = 1
UIStroke.Color = Internal._config.SelectionImageObjectBorderColor
UIStroke.Transparency = Internal._config.SelectionImageObjectBorderTransparency
UIStroke.LineJoinMode = Enum.LineJoinMode.Round
UIStroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
UIStroke.Parent = SelectionImageObject
local Rounding: UICorner = Instance.new("UICorner")
Rounding.CornerRadius = UDim.new(0, 2)
Rounding.Parent = SelectionImageObject
Internal.SelectionImageObject = SelectionImageObject
end
function Internal._getID(levelsToIgnore: number): ID
if Internal._nextWidgetId then
local ID: ID = Internal._nextWidgetId
Internal._nextWidgetId = nil
return ID
end
local i: number = 1 + (levelsToIgnore or 1)
local ID: ID = ""
local levelInfo: number = debug.info(i, "l")
while levelInfo ~= -1 and levelInfo ~= nil do
ID ..= "+" .. levelInfo
i += 1
levelInfo = debug.info(i, "l")
end
if Internal._usedIDs[ID] then
Internal._usedIDs[ID] += 1
else
Internal._usedIDs[ID] = 1
end
local discriminator = if Internal._pushedId then Internal._pushedId else Internal._usedIDs[ID]
return ID .. ":" .. discriminator
end
function Internal._deepCompare(t1: {}, t2: {}): boolean
for i, v1 in t1 do
local v2 = t2[i]
if type(v1) == "table" then
if v2 and type(v2) == "table" then
if Internal._deepCompare(v1, v2) == false then
return false
end
else
return false
end
else
if type(v1) ~= type(v2) or v1 ~= v2 then
return false
end
end
end
return true
end
function Internal._deepCopy(t: {}): {}
local copy: {} = table.clone(t)
for k: any, v: any in pairs(t) do
if type(v) == "table" then
copy[k] = Internal._deepCopy(v)
end
end
return copy
end
Internal._lastVDOM = Internal._generateEmptyVDOM()
Internal._VDOM = Internal._generateEmptyVDOM()
ImGui.Internal = Internal
ImGui._config = Internal._config
return Internal
end
local Internal: Internal = Internal(ImGui)
ImGui.Disabled = false
ImGui.Args = {}
ImGui.Events = {}
function ImGui.Init(parentInstance: Instance?, eventConnection: (RBXScriptSignal | () -> () | false)?): ImGui
assert(Internal._started == false, "ImGui.Init can only be called once.")
assert(Internal._shutdown == false, "ImGui.Init cannot be called once shutdown.")
if parentInstance == nil then
parentInstance = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")
end
if eventConnection == nil then
eventConnection = game:GetService("RunService").Heartbeat
end
Internal.parentInstance = parentInstance :: Instance
Internal._started = true
Internal._generateRootInstance()
Internal._generateSelectionImageObject()
for _, callback: () -> () in Internal._initFunctions do
callback()
end
task.spawn(function()
if typeof(eventConnection) == "function" then
while Internal._started do
eventConnection()
Internal._cycle()
end
elseif eventConnection ~= nil and eventConnection ~= false then
Internal._eventConnection = eventConnection:Connect(function()
Internal._cycle()
end)
end
end)
return ImGui
end
function ImGui.Shutdown()
Internal._started = false
Internal._shutdown = true
if Internal._eventConnection then
Internal._eventConnection:Disconnect()
end
Internal._eventConnection = nil
if Internal._rootWidget then
if Internal._rootWidget.Instance then
Internal._widgets["Root"].Discard(Internal._rootWidget)
end
Internal._rootInstance = nil
end
if Internal.SelectionImageObject then
Internal.SelectionImageObject:Destroy()
end
for _, connection: RBXScriptConnection in Internal._connections do
connection:Disconnect()
end
end
function ImGui:Connect(callback: () -> ()): () -> ()
if Internal._started == false then
warn("ImGui:Connect() was called before calling ImGui.Init(), the connected function will never run")
end
local connectionIndex: number = #Internal._connectedFunctions + 1
Internal._connectedFunctions[connectionIndex] = callback
return function()
Internal._connectedFunctions[connectionIndex] = nil
end
end
function ImGui.Append(userInstance: GuiObject)
local parentWidget: ParentWidget = Internal._GetParentWidget()
local widgetInstanceParent: GuiObject
if Internal._config.Parent then
widgetInstanceParent = Internal._config.Parent :: any
else
widgetInstanceParent = Internal._widgets[parentWidget.type].ChildAdded(parentWidget, { type = "userInstance" } :: Widget)
end
userInstance.Parent = widgetInstanceParent
end
function ImGui.End()
if Internal._stackIndex == 1 then
error("Callback has too many calls to ImGui.End()", 2)
end
Internal._IDStack[Internal._stackIndex] = nil
Internal._stackIndex -= 1
end
function ImGui.ForceRefresh()
Internal._globalRefreshRequested = true
end
function ImGui.UpdateGlobalConfig(deltaStyle: { [string]: any })
for index, style in deltaStyle do
Internal._rootConfig[index] = style
end
ImGui.ForceRefresh()
end
function ImGui.PushConfig(deltaStyle: { [string]: any })
local ID = ImGui.State(-1)
if ID.value == -1 then
ID:set(deltaStyle)
else
if Internal._deepCompare(ID:get(), deltaStyle) == false then
Internal._localRefreshActive = true
ID:set(deltaStyle)
end
end
Internal._config = setmetatable(deltaStyle, {
__index = Internal._config,
}) :: any
end
function ImGui.PopConfig()
Internal._localRefreshActive = false
Internal._config = getmetatable(Internal._config :: any).__index
end
ImGui.UpdateGlobalConfig(ImGui.TemplateConfig.colorDark)
ImGui.UpdateGlobalConfig(ImGui.TemplateConfig.sizeDefault)
ImGui.UpdateGlobalConfig(ImGui.TemplateConfig.utilityDefault)
Internal._globalRefreshRequested = false
function ImGui.PushId(ID: ID)
assert(typeof(ID) == "string", "ImGui expected ImGui.PushId id to PushId to be a string.")
Internal._pushedId = tostring(ID)
end
function ImGui.PopId()
Internal._pushedId = nil
end
function ImGui.SetNextWidgetID(ID: ID)
Internal._nextWidgetId = ID
end
function ImGui.State(initialValue: T): State
local ID: ID = Internal._getID(2)
if Internal._states[ID] then
return Internal._states[ID]
end
Internal._states[ID] = {
ID = ID,
value = initialValue,
ConnectedWidgets = {},
ConnectedFunctions = {},
} :: any
setmetatable(Internal._states[ID], Internal.StateClass)
return Internal._states[ID]
end
function ImGui.WeakState(initialValue: T): State
local ID: ID = Internal._getID(2)
if Internal._states[ID] then
if next(Internal._states[ID].ConnectedWidgets) == nil then
Internal._states[ID] = nil
else
return Internal._states[ID]
end
end
Internal._states[ID] = {
ID = ID,
value = initialValue,
ConnectedWidgets = {},
ConnectedFunctions = {},
} :: any
setmetatable(Internal._states[ID], Internal.StateClass)
return Internal._states[ID]
end
function ImGui.VariableState(variable: T, callback: (T) -> ()): State
local ID: ID = Internal._getID(2)
local state: State? = Internal._states[ID]
if state then
if variable ~= state.value then
state:set(variable)
end
return state
end
local newState = {
ID = ID,
value = variable,
ConnectedWidgets = {},
ConnectedFunctions = {},
} :: State
setmetatable(newState, Internal.StateClass)
Internal._states[ID] = newState
newState:onChange(callback)
return newState
end
function ImGui.TableState(tab: { [K]: V }, key: K, callback: ((newValue: V) -> false?)?): State
local value: V = tab[key]
local ID: ID = Internal._getID(2)
local state: State? = Internal._states[ID]
if state then
if value ~= state.value then
state:set(value)
end
return state
end
local newState = {
ID = ID,
value = value,
ConnectedWidgets = {},
ConnectedFunctions = {},
} :: State
setmetatable(newState, Internal.StateClass)
Internal._states[ID] = newState
newState:onChange(function()
if callback ~= nil then
if callback(newState.value) then
tab[key] = newState.value
end
else
tab[key] = newState.value
end
end)
return newState
end
function ImGui.ComputedState(firstState: State, onChangeCallback: (firstValue: T) -> U): State
local ID: ID = Internal._getID(2)
if Internal._states[ID] then
return Internal._states[ID]
else
Internal._states[ID] = {
ID = ID,
value = onChangeCallback(firstState.value),
ConnectedWidgets = {},
ConnectedFunctions = {},
} :: State
firstState:onChange(function(newValue: T)
Internal._states[ID]:set(onChangeCallback(newValue))
end)
setmetatable(Internal._states[ID], Internal.StateClass)
return Internal._states[ID]
end
end
ImGui.ShowDemoWindow = require(game.ReplicatedStorage.ImGui.demoWindow)(ImGui)
local widgets = {} :: WidgetUtility
local function Widgets(ImGui: Internal)
widgets.GuiService = game:GetService("GuiService")
widgets.RunService = game:GetService("RunService")
widgets.UserInputService = game:GetService("UserInputService")
widgets.ContextActionService = game:GetService("ContextActionService")
widgets.TextService = game:GetService("TextService")
widgets.ICONS = {
RIGHT_POINTING_TRIANGLE = "rbxasset://textures/DeveloperFramework/button_arrow_right.png",
DOWN_POINTING_TRIANGLE = "rbxasset://textures/DeveloperFramework/button_arrow_down.png",
MULTIPLICATION_SIGN = "rbxasset://textures/AnimationEditor/icon_close.png",
BOTTOM_RIGHT_CORNER = "rbxasset://textures/ui/InspectMenu/gr-item-selector-triangle.png",
CHECK_MARK = "rbxasset://textures/AnimationEditor/icon_checkmark.png",
ALPHA_BACKGROUND_TEXTURE = "rbxasset://textures/meshPartFallback.png",
UNKNOWN_TEXTURE = "rbxasset://textures/ui/GuiImagePlaceholder.png",
}
widgets.IS_STUDIO = widgets.RunService:IsStudio()
function widgets.getTime()
if widgets.IS_STUDIO then
return os.clock()
else
return time()
end
end
widgets.GuiOffset = Vector2.zero
widgets.MouseOffset = if ImGui._config.IgnoreGuiInset then Vector2.zero else widgets.GuiService:GetGuiInset()
local connection: RBXScriptConnection = widgets.GuiService:GetPropertyChangedSignal("TopbarInset"):Once(function()
widgets.MouseOffset = if ImGui._config.IgnoreGuiInset then Vector2.zero else widgets.GuiService:GetGuiInset()
end)
task.delay(3, function()
connection:Disconnect()
end)
do
function widgets.getMouseLocation(): Vector2
return widgets.UserInputService:GetMouseLocation() - widgets.MouseOffset
end
function widgets.isPosInsideRect(pos: Vector2, rectMin: Vector2, rectMax: Vector2): boolean
return pos.X > rectMin.X and pos.X < rectMax.X and pos.Y > rectMin.Y and pos.Y < rectMax.Y
end
function widgets.findBestWindowPosForPopup(refPos: Vector2, size: Vector2, outerMin: Vector2, outerMax: Vector2): Vector2
local CURSOR_OFFSET_DIST: number = 20
if refPos.X + size.X + CURSOR_OFFSET_DIST > outerMax.X then
if refPos.Y + size.Y + CURSOR_OFFSET_DIST > outerMax.Y then
refPos += Vector2.new(0, -(CURSOR_OFFSET_DIST + size.Y))
else
refPos += Vector2.new(0, CURSOR_OFFSET_DIST)
end
else
refPos += Vector2.new(CURSOR_OFFSET_DIST)
end
local clampedPos: Vector2 = Vector2.new(math.max(math.min(refPos.X + size.X, outerMax.X) - size.X, outerMin.X), math.max(math.min(refPos.Y + size.Y, outerMax.Y) - size.Y, outerMin.Y))
return clampedPos
end
function widgets.getScreenSizeForWindow(thisWidget: Widget): Vector2
if thisWidget.Instance:IsA("GuiBase2d") then
return thisWidget.Instance.AbsoluteSize
else
local rootParent = thisWidget.Instance.Parent
if rootParent:IsA("GuiBase2d") then
return rootParent.AbsoluteSize
else
if rootParent.Parent:IsA("GuiBase2d") then
return rootParent.AbsoluteSize
else
return workspace.CurrentCamera.ViewportSize
end
end
end
end
function widgets.extend(superClass: WidgetClass, subClass: WidgetClass): WidgetClass
local newClass: WidgetClass = table.clone(superClass)
for index: unknown, value: any in subClass do
newClass[index] = value
end
return newClass
end
function widgets.UIPadding(Parent: GuiObject, PxPadding: Vector2): UIPadding
local UIPaddingInstance: UIPadding = Instance.new("UIPadding")
UIPaddingInstance.PaddingLeft = UDim.new(0, PxPadding.X)
UIPaddingInstance.PaddingRight = UDim.new(0, PxPadding.X)
UIPaddingInstance.PaddingTop = UDim.new(0, PxPadding.Y)
UIPaddingInstance.PaddingBottom = UDim.new(0, PxPadding.Y)
UIPaddingInstance.Parent = Parent
return UIPaddingInstance
end
function widgets.UIListLayout(Parent: GuiObject, FillDirection: Enum.FillDirection, Padding: UDim): UIListLayout
local UIListLayoutInstance: UIListLayout = Instance.new("UIListLayout")
UIListLayoutInstance.SortOrder = Enum.SortOrder.LayoutOrder
UIListLayoutInstance.Padding = Padding
UIListLayoutInstance.FillDirection = FillDirection
UIListLayoutInstance.Parent = Parent
return UIListLayoutInstance
end
function widgets.UIStroke(Parent: GuiObject, Thickness: number, Color: Color3, Transparency: number): UIStroke
local UIStrokeInstance: UIStroke = Instance.new("UIStroke")
UIStrokeInstance.Thickness = Thickness
UIStrokeInstance.Color = Color
UIStrokeInstance.Transparency = Transparency
UIStrokeInstance.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
UIStrokeInstance.LineJoinMode = Enum.LineJoinMode.Round
UIStrokeInstance.Parent = Parent
return UIStrokeInstance
end
function widgets.UICorner(Parent: GuiObject, PxRounding: number?): UICorner
local UICornerInstance: UICorner = Instance.new("UICorner")
UICornerInstance.CornerRadius = UDim.new(PxRounding and 0 or 1, PxRounding or 0)
UICornerInstance.Parent = Parent
return UICornerInstance
end
function widgets.UISizeConstraint(Parent: GuiObject, MinSize: Vector2?, MaxSize: Vector2?): UISizeConstraint
local UISizeConstraintInstance: UISizeConstraint = Instance.new("UISizeConstraint")
UISizeConstraintInstance.MinSize = MinSize or UISizeConstraintInstance.MinSize
UISizeConstraintInstance.MaxSize = MaxSize or UISizeConstraintInstance.MaxSize
UISizeConstraintInstance.Parent = Parent
return UISizeConstraintInstance
end
function widgets.applyTextStyle(thisInstance: TextLabel & TextButton & TextBox)
thisInstance.FontFace = ImGui._config.TextFont
thisInstance.TextSize = ImGui._config.TextSize
thisInstance.TextColor3 = ImGui._config.TextColor
thisInstance.TextTransparency = ImGui._config.TextTransparency
thisInstance.TextXAlignment = Enum.TextXAlignment.Left
thisInstance.TextYAlignment = Enum.TextYAlignment.Center
thisInstance.RichText = ImGui._config.RichText
thisInstance.TextWrapped = ImGui._config.TextWrapped
thisInstance.AutoLocalize = false
end
function widgets.applyInteractionHighlights(Property: string, Button: GuiButton, Highlightee: GuiObject, Colors: { [string]: any })
local exitedButton: boolean = false
widgets.applyMouseEnter(Button, function()
Highlightee[Property .. "Color3"] = Colors.HoveredColor
Highlightee[Property .. "Transparency"] = Colors.HoveredTransparency
exitedButton = false
end)
widgets.applyMouseLeave(Button, function()
Highlightee[Property .. "Color3"] = Colors.Color
Highlightee[Property .. "Transparency"] = Colors.Transparency
exitedButton = true
end)
widgets.applyInputBegan(Button, function(input: InputObject)
if not (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Gamepad1) then
return
end
Highlightee[Property .. "Color3"] = Colors.ActiveColor
Highlightee[Property .. "Transparency"] = Colors.ActiveTransparency
end)
widgets.applyInputEnded(Button, function(input: InputObject)
if not (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Gamepad1) or exitedButton then
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
Highlightee[Property .. "Color3"] = Colors.HoveredColor
Highlightee[Property .. "Transparency"] = Colors.HoveredTransparency
end
if input.UserInputType == Enum.UserInputType.Gamepad1 then
Highlightee[Property .. "Color3"] = Colors.Color
Highlightee[Property .. "Transparency"] = Colors.Transparency
end
end)
Button.SelectionImageObject = ImGui.SelectionImageObject
end
function widgets.applyInteractionHighlightsWithMultiHighlightee(Property: string, Button: GuiButton, Highlightees: { { GuiObject | { [string]: Color3 | number } } })
local exitedButton: boolean = false
widgets.applyMouseEnter(Button, function()
for _, Highlightee in Highlightees do
Highlightee[1][Property .. "Color3"] = Highlightee[2].HoveredColor
Highlightee[1][Property .. "Transparency"] = Highlightee[2].HoveredTransparency
exitedButton = false
end
end)
widgets.applyMouseLeave(Button, function()
for _, Highlightee in Highlightees do
Highlightee[1][Property .. "Color3"] = Highlightee[2].Color
Highlightee[1][Property .. "Transparency"] = Highlightee[2].Transparency
exitedButton = true
end
end)
widgets.applyInputBegan(Button, function(input: InputObject)
if not (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Gamepad1) then
return
end
for _, Highlightee in Highlightees do
Highlightee[1][Property .. "Color3"] = Highlightee[2].ActiveColor
Highlightee[1][Property .. "Transparency"] = Highlightee[2].ActiveTransparency
end
end)
widgets.applyInputEnded(Button, function(input: InputObject)
if not (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Gamepad1) or exitedButton then
return
end
for _, Highlightee in Highlightees do
if input.UserInputType == Enum.UserInputType.MouseButton1 then
Highlightee[1][Property .. "Color3"] = Highlightee[2].HoveredColor
Highlightee[1][Property .. "Transparency"] = Highlightee[2].HoveredTransparency
end
if input.UserInputType == Enum.UserInputType.Gamepad1 then
Highlightee[1][Property .. "Color3"] = Highlightee[2].Color
Highlightee[1][Property .. "Transparency"] = Highlightee[2].Transparency
end
end
end)
Button.SelectionImageObject = ImGui.SelectionImageObject
end
function widgets.applyFrameStyle(thisInstance: GuiObject, noPadding: boolean?, noCorner: boolean?)
local FrameBorderSize: number = ImGui._config.FrameBorderSize
local FrameRounding: number = ImGui._config.FrameRounding
thisInstance.BorderSizePixel = 0
if FrameBorderSize > 0 then
widgets.UIStroke(thisInstance, FrameBorderSize, ImGui._config.BorderColor, ImGui._config.BorderTransparency)
end
if FrameRounding > 0 and not noCorner then
widgets.UICorner(thisInstance, FrameRounding)
end
if not noPadding then
widgets.UIPadding(thisInstance, ImGui._config.FramePadding)
end
end
function widgets.applyButtonClick(thisInstance: GuiButton, callback: () -> ())
thisInstance.MouseButton1Click:Connect(function()
callback()
end)
end
function widgets.applyButtonDown(thisInstance: GuiButton, callback: (x: number, y: number) -> ())
thisInstance.MouseButton1Down:Connect(function(...)
callback(...)
end)
end
function widgets.applyMouseEnter(thisInstance: GuiObject, callback: () -> ())
thisInstance.MouseEnter:Connect(function(...)
callback(...)
end)
end
function widgets.applyMouseLeave(thisInstance: GuiObject, callback: () -> ())
thisInstance.MouseLeave:Connect(function(...)
callback(...)
end)
end
function widgets.applyInputBegan(thisInstance: GuiButton, callback: (input: InputObject) -> ())
thisInstance.InputBegan:Connect(function(...)
callback(...)
end)
end
function widgets.applyInputEnded(thisInstance: GuiButton, callback: (input: InputObject) -> ())
thisInstance.InputEnded:Connect(function(...)
callback(...)
end)
end
function widgets.discardState(thisWidget: StateWidget)
for _, state: State in thisWidget.state do
state.ConnectedWidgets[thisWidget.ID] = nil
end
end
function widgets.registerEvent(event: string, callback: (...any) -> ())
table.insert(ImGui._initFunctions, function()
table.insert(ImGui._connections, widgets.UserInputService[event]:Connect(callback))
end)
end
end
widgets.EVENTS = {
hover = function(pathToHovered: (thisWidget: Widget) -> GuiObject)
return {
["Init"] = function(thisWidget: Widget & Hovered)
local hoveredGuiObject: GuiObject = pathToHovered(thisWidget)
widgets.applyMouseEnter(hoveredGuiObject, function()
thisWidget.isHoveredEvent = true
end)
widgets.applyMouseLeave(hoveredGuiObject, function()
thisWidget.isHoveredEvent = false
end)
thisWidget.isHoveredEvent = false
end,
["Get"] = function(thisWidget: Widget & Hovered): boolean
return thisWidget.isHoveredEvent
end,
}
end,
click = function(pathToClicked: (thisWidget: Widget) -> GuiButton)
return {
["Init"] = function(thisWidget: Widget & Clicked)
local clickedGuiObject: GuiButton = pathToClicked(thisWidget)
thisWidget.lastClickedTick = -1
widgets.applyButtonClick(clickedGuiObject, function()
thisWidget.lastClickedTick = ImGui._cycleTick + 1
end)
end,
["Get"] = function(thisWidget: Widget & Clicked): boolean
return thisWidget.lastClickedTick == ImGui._cycleTick
end,
}
end,
rightClick = function(pathToClicked: (thisWidget: Widget) -> GuiButton)
return {
["Init"] = function(thisWidget: Widget & RightClicked)
local clickedGuiObject: GuiButton = pathToClicked(thisWidget)
thisWidget.lastRightClickedTick = -1
clickedGuiObject.MouseButton2Click:Connect(function()
thisWidget.lastRightClickedTick = ImGui._cycleTick + 1
end)
end,
["Get"] = function(thisWidget: Widget & RightClicked): boolean
return thisWidget.lastRightClickedTick == ImGui._cycleTick
end,
}
end,
doubleClick = function(pathToClicked: (thisWidget: Widget) -> GuiButton)
return {
["Init"] = function(thisWidget: Widget & DoubleClicked)
local clickedGuiObject: GuiButton = pathToClicked(thisWidget)
thisWidget.lastClickedTime = -1
thisWidget.lastClickedPosition = Vector2.zero
thisWidget.lastDoubleClickedTick = -1
widgets.applyButtonDown(clickedGuiObject, function(x: number, y: number)
local currentTime: number = widgets.getTime()
local isTimeValid: boolean = currentTime - thisWidget.lastClickedTime < ImGui._config.MouseDoubleClickTime
if isTimeValid and (Vector2.new(x, y) - thisWidget.lastClickedPosition).Magnitude < ImGui._config.MouseDoubleClickMaxDist then
thisWidget.lastDoubleClickedTick = ImGui._cycleTick + 1
else
thisWidget.lastClickedTime = currentTime
thisWidget.lastClickedPosition = Vector2.new(x, y)
end
end)
end,
["Get"] = function(thisWidget: Widget & DoubleClicked): boolean
return thisWidget.lastDoubleClickedTick == ImGui._cycleTick
end,
}
end,
ctrlClick = function(pathToClicked: (thisWidget: Widget) -> GuiButton)
return {
["Init"] = function(thisWidget: Widget & CtrlClicked)
local clickedGuiObject: GuiButton = pathToClicked(thisWidget)
thisWidget.lastCtrlClickedTick = -1
widgets.applyButtonClick(clickedGuiObject, function()
if widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl) then
thisWidget.lastCtrlClickedTick = ImGui._cycleTick + 1
end
end)
end,
["Get"] = function(thisWidget: Widget & CtrlClicked): boolean
return thisWidget.lastCtrlClickedTick == ImGui._cycleTick
end,
}
end,
}
ImGui._utility = widgets
widgets.ELEMENTS = {
Root = function(ImGui: Internal, widgets: WidgetUtility)
local NumNonWindowChildren: number = 0
--stylua: ignore
ImGui.WidgetConstructor("Root", {
hasState = false,
hasChildren = true,
Args = {},
Events = {},
Generate = function(_thisWidget: Root)
local Root: Folder = Instance.new("Folder")
Root.Name = "ImGui_Root"
local PseudoWindowScreenGui
if ImGui._config.UseScreenGUIs then
PseudoWindowScreenGui = Instance.new("ScreenGui")
PseudoWindowScreenGui.ResetOnSpawn = false
PseudoWindowScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
PseudoWindowScreenGui.DisplayOrder = ImGui._config.DisplayOrderOffset
PseudoWindowScreenGui.IgnoreGuiInset = ImGui._config.IgnoreGuiInset
else
PseudoWindowScreenGui = Instance.new("Frame")
PseudoWindowScreenGui.AnchorPoint = Vector2.new(0.5, 0.5)
PseudoWindowScreenGui.Position = UDim2.new(0.5, 0, 0.5, 0)
PseudoWindowScreenGui.Size = UDim2.new(1, 0, 1, 0)
PseudoWindowScreenGui.BackgroundTransparency = 1
PseudoWindowScreenGui.ZIndex = ImGui._config.DisplayOrderOffset
end
PseudoWindowScreenGui.Name = "PseudoWindowScreenGui"
PseudoWindowScreenGui.Parent = Root
local PopupScreenGui
if ImGui._config.UseScreenGUIs then
PopupScreenGui = Instance.new("ScreenGui")
PopupScreenGui.ResetOnSpawn = false
PopupScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
PopupScreenGui.DisplayOrder = ImGui._config.DisplayOrderOffset + 1024 -- room for 1024 regular windows before overlap
PopupScreenGui.IgnoreGuiInset = ImGui._config.IgnoreGuiInset
else
PopupScreenGui = Instance.new("Frame")
PopupScreenGui.AnchorPoint = Vector2.new(0.5, 0.5)
PopupScreenGui.Position = UDim2.new(0.5, 0, 0.5, 0)
PopupScreenGui.Size = UDim2.new(1, 0, 1, 0)
PopupScreenGui.BackgroundTransparency = 1
PopupScreenGui.ZIndex = ImGui._config.DisplayOrderOffset + 1024
end
PopupScreenGui.Name = "PopupScreenGui"
PopupScreenGui.Parent = Root
local TooltipContainer: Frame = Instance.new("Frame")
TooltipContainer.Name = "TooltipContainer"
TooltipContainer.AutomaticSize = Enum.AutomaticSize.XY
TooltipContainer.Size = UDim2.fromOffset(0, 0)
TooltipContainer.BackgroundTransparency = 1
TooltipContainer.BorderSizePixel = 0
widgets.UIListLayout(TooltipContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.PopupBorderSize))
TooltipContainer.Parent = PopupScreenGui
local MenuBarContainer: Frame = Instance.new("Frame")
MenuBarContainer.Name = "MenuBarContainer"
MenuBarContainer.AutomaticSize = Enum.AutomaticSize.Y
MenuBarContainer.Size = UDim2.fromScale(1, 0)
MenuBarContainer.BackgroundTransparency = 1
MenuBarContainer.BorderSizePixel = 0
MenuBarContainer.Parent = PopupScreenGui
local PseudoWindow: Frame = Instance.new("Frame")
PseudoWindow.Name = "PseudoWindow"
PseudoWindow.Size = UDim2.new(0, 0, 0, 0)
PseudoWindow.Position = UDim2.fromOffset(0, 22)
PseudoWindow.AutomaticSize = Enum.AutomaticSize.XY
PseudoWindow.BackgroundTransparency = ImGui._config.WindowBgTransparency
PseudoWindow.BackgroundColor3 = ImGui._config.WindowBgColor
PseudoWindow.BorderSizePixel = ImGui._config.WindowBorderSize
PseudoWindow.BorderColor3 = ImGui._config.BorderColor
PseudoWindow.Selectable = false
PseudoWindow.SelectionGroup = true
PseudoWindow.SelectionBehaviorUp = Enum.SelectionBehavior.Stop
PseudoWindow.SelectionBehaviorDown = Enum.SelectionBehavior.Stop
PseudoWindow.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop
PseudoWindow.SelectionBehaviorRight = Enum.SelectionBehavior.Stop
PseudoWindow.Visible = false
widgets.UIPadding(PseudoWindow, ImGui._config.WindowPadding)
widgets.UIListLayout(PseudoWindow, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
PseudoWindow.Parent = PseudoWindowScreenGui
return Root
end,
Update = function(thisWidget: Root)
if NumNonWindowChildren > 0 then
local Root = thisWidget.Instance :: any
local PseudoWindowScreenGui = Root.PseudoWindowScreenGui :: any
local PseudoWindow: Frame = PseudoWindowScreenGui.PseudoWindow
PseudoWindow.Visible = true
end
end,
Discard = function(thisWidget: Root)
NumNonWindowChildren = 0
thisWidget.Instance:Destroy()
end,
ChildAdded = function(thisWidget: Root, thisChild: Widget)
local Root = thisWidget.Instance :: any
if thisChild.type == "Window" then
return thisWidget.Instance
elseif thisChild.type == "Tooltip" then
return Root.PopupScreenGui.TooltipContainer
elseif thisChild.type == "MenuBar" then
return Root.PopupScreenGui.MenuBarContainer
else
local PseudoWindowScreenGui = Root.PseudoWindowScreenGui :: any
local PseudoWindow: Frame = PseudoWindowScreenGui.PseudoWindow
NumNonWindowChildren += 1
PseudoWindow.Visible = true
return PseudoWindow
end
end,
ChildDiscarded = function(thisWidget: Root, thisChild: Widget)
if thisChild.type ~= "Window" and thisChild.type ~= "Tooltip" and thisChild.type ~= "MenuBar" then
NumNonWindowChildren -= 1
if NumNonWindowChildren == 0 then
local Root = thisWidget.Instance :: any
local PseudoWindowScreenGui = Root.PseudoWindowScreenGui :: any
local PseudoWindow: Frame = PseudoWindowScreenGui.PseudoWindow
PseudoWindow.Visible = false
end
end
end,
} :: WidgetClass)
end,
Window = function(ImGui: Internal, widgets: WidgetUtility)
local function relocateTooltips()
if ImGui._rootInstance == nil then
return
end
local PopupScreenGui = ImGui._rootInstance:FindFirstChild("PopupScreenGui")
local TooltipContainer = PopupScreenGui.TooltipContainer
local mouseLocation: Vector2 = widgets.getMouseLocation()
local newPosition: Vector2 = widgets.findBestWindowPosForPopup(mouseLocation, TooltipContainer.AbsoluteSize, ImGui._config.DisplaySafeAreaPadding, PopupScreenGui.AbsoluteSize)
TooltipContainer.Position = UDim2.fromOffset(newPosition.X, newPosition.Y)
end
widgets.registerEvent("InputChanged", function()
if not ImGui._started then
return
end
relocateTooltips()
end)
--stylua: ignore
ImGui.WidgetConstructor("Tooltip", {
hasState = false,
hasChildren = false,
Args = {
["Text"] = 1,
},
Events = {},
Generate = function(thisWidget: Tooltip)
thisWidget.parentWidget = ImGui._rootWidget -- only allow root as parent
local Tooltip: Frame = Instance.new("Frame")
Tooltip.Name = "ImGui_Tooltip"
Tooltip.Size = UDim2.new(ImGui._config.ContentWidth, UDim.new(0, 0))
Tooltip.AutomaticSize = Enum.AutomaticSize.Y
Tooltip.BorderSizePixel = 0
Tooltip.BackgroundTransparency = 1
Tooltip.ZIndex = thisWidget.ZIndex + 1
Tooltip.LayoutOrder = thisWidget.ZIndex + 1
local TooltipText: TextLabel = Instance.new("TextLabel")
TooltipText.Name = "TooltipText"
TooltipText.Size = UDim2.fromOffset(0, 0)
TooltipText.AutomaticSize = Enum.AutomaticSize.XY
TooltipText.BackgroundColor3 = ImGui._config.PopupBgColor
TooltipText.BackgroundTransparency = ImGui._config.PopupBgTransparency
TooltipText.BorderSizePixel = ImGui._config.PopupBorderSize
TooltipText.TextWrapped = ImGui._config.TextWrapped
widgets.applyTextStyle(TooltipText)
widgets.UIStroke(TooltipText, ImGui._config.WindowBorderSize, ImGui._config.BorderActiveColor, ImGui._config.BorderActiveTransparency)
widgets.UIPadding(TooltipText, ImGui._config.WindowPadding)
if ImGui._config.PopupRounding > 0 then
widgets.UICorner(TooltipText, ImGui._config.PopupRounding)
end
TooltipText.Parent = Tooltip
return Tooltip
end,
Update = function(thisWidget: Tooltip)
local Tooltip = thisWidget.Instance :: Frame
local TooltipText: TextLabel = Tooltip.TooltipText
if thisWidget.arguments.Text == nil then
error("ImGui.Text Text Argument is required", 5)
end
TooltipText.Text = thisWidget.arguments.Text
relocateTooltips()
end,
Discard = function(thisWidget: Tooltip)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
local windowDisplayOrder: number = 0 -- incremental count which is used for determining focused windows ZIndex
local dragWindow: Window? -- window being dragged, may be nil
local isDragging: boolean = false
local moveDeltaCursorPosition: Vector2 -- cursor offset from drag origin (top left of window)
local resizeWindow: Window? -- window being resized, may be nil
local isResizing = false
local isInsideResize = false -- is cursor inside of the focused window resize outer padding
local isInsideWindow = false -- is cursor inside of the focused window
local resizeFromTopBottom: Enum.TopBottom = Enum.TopBottom.Top
local resizeFromLeftRight: Enum.LeftRight = Enum.LeftRight.Left
local lastCursorPosition: Vector2
local focusedWindow: Window? -- window with focus, may be nil
local anyFocusedWindow: boolean = false -- is there any focused window?
local windowWidgets: { [ID]: Window } = {} -- array of widget objects of type window
local function quickSwapWindows()
-- ctrl + tab swapping functionality
if ImGui._config.UseScreenGUIs == false then
return
end
local lowest: number = 0xFFFF
local lowestWidget: Window
for _, widget: Window in windowWidgets do
if widget.state.isOpened.value and not widget.arguments.NoNav then
if widget.Instance:IsA("ScreenGui") then
local value: number = widget.Instance.DisplayOrder
if value < lowest then
lowest = value
lowestWidget = widget
end
end
end
end
if not lowestWidget then
return
end
if lowestWidget.state.isUncollapsed.value == false then
lowestWidget.state.isUncollapsed:set(true)
end
ImGui.SetFocusedWindow(lowestWidget)
end
local function fitSizeToWindowBounds(thisWidget: Window, intentedSize: Vector2): Vector2
local windowSize: Vector2 = Vector2.new(thisWidget.state.position.value.X, thisWidget.state.position.value.Y)
local minWindowSize: number = (ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y) * 2
local usableSize: Vector2 = widgets.getScreenSizeForWindow(thisWidget)
local safeAreaPadding: Vector2 = Vector2.new(ImGui._config.WindowBorderSize + ImGui._config.DisplaySafeAreaPadding.X, ImGui._config.WindowBorderSize + ImGui._config.DisplaySafeAreaPadding.Y)
local maxWindowSize: Vector2 = (usableSize - windowSize - safeAreaPadding)
return Vector2.new(math.clamp(intentedSize.X, minWindowSize, math.max(maxWindowSize.X, minWindowSize)), math.clamp(intentedSize.Y, minWindowSize, math.max(maxWindowSize.Y, minWindowSize)))
end
local function fitPositionToWindowBounds(thisWidget: Window, intendedPosition: Vector2): Vector2
local thisWidgetInstance = thisWidget.Instance
local usableSize: Vector2 = widgets.getScreenSizeForWindow(thisWidget)
local safeAreaPadding: Vector2 = Vector2.new(ImGui._config.WindowBorderSize + ImGui._config.DisplaySafeAreaPadding.X, ImGui._config.WindowBorderSize + ImGui._config.DisplaySafeAreaPadding.Y)
return Vector2.new(
math.clamp(intendedPosition.X, safeAreaPadding.X, math.max(safeAreaPadding.X, usableSize.X - thisWidgetInstance.WindowButton.AbsoluteSize.X - safeAreaPadding.X)),
math.clamp(intendedPosition.Y, safeAreaPadding.Y, math.max(safeAreaPadding.Y, usableSize.Y - thisWidgetInstance.WindowButton.AbsoluteSize.Y - safeAreaPadding.Y))
)
end
ImGui.SetFocusedWindow = function(thisWidget: Window?)
if focusedWindow == thisWidget then
return
end
if anyFocusedWindow and focusedWindow ~= nil then
if windowWidgets[focusedWindow.ID] then
local Window = focusedWindow.Instance :: Frame
local WindowButton = Window.WindowButton :: TextButton
local Content = WindowButton.Content :: Frame
local TitleBar: Frame = Content.TitleBar
-- update appearance to unfocus
if focusedWindow.state.isUncollapsed.value then
TitleBar.BackgroundColor3 = ImGui._config.TitleBgColor
TitleBar.BackgroundTransparency = ImGui._config.TitleBgTransparency
else
TitleBar.BackgroundColor3 = ImGui._config.TitleBgCollapsedColor
TitleBar.BackgroundTransparency = ImGui._config.TitleBgCollapsedTransparency
end
WindowButton.UIStroke.Color = ImGui._config.BorderColor
end
anyFocusedWindow = false
focusedWindow = nil
end
if thisWidget ~= nil then
-- update appearance to focus
anyFocusedWindow = true
focusedWindow = thisWidget
local Window = thisWidget.Instance :: Frame
local WindowButton = Window.WindowButton :: TextButton
local Content = WindowButton.Content :: Frame
local TitleBar: Frame = Content.TitleBar
TitleBar.BackgroundColor3 = ImGui._config.TitleBgActiveColor
TitleBar.BackgroundTransparency = ImGui._config.TitleBgActiveTransparency
WindowButton.UIStroke.Color = ImGui._config.BorderActiveColor
windowDisplayOrder += 1
if thisWidget.usesScreenGuis then
Window.DisplayOrder = windowDisplayOrder + ImGui._config.DisplayOrderOffset
else
Window.ZIndex = windowDisplayOrder + ImGui._config.DisplayOrderOffset
end
if thisWidget.state.isUncollapsed.value == false then
thisWidget.state.isUncollapsed:set(true)
end
local firstSelectedObject: GuiObject? = widgets.GuiService.SelectedObject
if firstSelectedObject then
if TitleBar.Visible then
widgets.GuiService:Select(TitleBar)
else
widgets.GuiService:Select(thisWidget.ChildContainer)
end
end
end
end
widgets.registerEvent("InputBegan", function(input: InputObject)
if not ImGui._started then
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local inWindow: boolean = false
local position: Vector2 = widgets.getMouseLocation()
for _, window: Window in windowWidgets do
local Window = window.Instance :: Instance
if not Window then
continue
end
local WindowButton = Window.WindowButton :: TextButton
local ResizeBorder: TextButton = WindowButton.ResizeBorder
if ResizeBorder and widgets.isPosInsideRect(position, ResizeBorder.AbsolutePosition, ResizeBorder.AbsolutePosition + ResizeBorder.AbsoluteSize) then
inWindow = true
break
end
end
if not inWindow then
ImGui.SetFocusedWindow(nil)
end
end
if input.KeyCode == Enum.KeyCode.Tab and (widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)) then
quickSwapWindows()
end
if input.UserInputType == Enum.UserInputType.MouseButton1 and isInsideResize and not isInsideWindow and anyFocusedWindow and focusedWindow then
local midWindow: Vector2 = focusedWindow.state.position.value + (focusedWindow.state.size.value / 2)
local cursorPosition: Vector2 = widgets.getMouseLocation() - midWindow
-- check which axis its closest to, then check which side is closest with math.sign
if math.abs(cursorPosition.X) * focusedWindow.state.size.value.Y >= math.abs(cursorPosition.Y) * focusedWindow.state.size.value.X then
resizeFromTopBottom = Enum.TopBottom.Center
resizeFromLeftRight = if math.sign(cursorPosition.X) == -1 then Enum.LeftRight.Left else Enum.LeftRight.Right
else
resizeFromLeftRight = Enum.LeftRight.Center
resizeFromTopBottom = if math.sign(cursorPosition.Y) == -1 then Enum.TopBottom.Top else Enum.TopBottom.Bottom
end
isResizing = true
resizeWindow = focusedWindow
end
end)
widgets.registerEvent("TouchTapInWorld", function(_, gameProcessedEvent: boolean)
if not ImGui._started then
return
end
if not gameProcessedEvent then
ImGui.SetFocusedWindow(nil)
end
end)
widgets.registerEvent("InputChanged", function(input: InputObject)
if not ImGui._started then
return
end
if isDragging and dragWindow then
local mouseLocation: Vector2
if input.UserInputType == Enum.UserInputType.Touch then
local location: Vector3 = input.Position
mouseLocation = Vector2.new(location.X, location.Y)
else
mouseLocation = widgets.getMouseLocation()
end
local Window = dragWindow.Instance :: Frame
local dragInstance: TextButton = Window.WindowButton
local intendedPosition: Vector2 = mouseLocation - moveDeltaCursorPosition
local newPos: Vector2 = fitPositionToWindowBounds(dragWindow, intendedPosition)
-- state shouldnt be used like this, but calling :set would run the entire UpdateState function for the window, which is slow.
dragInstance.Position = UDim2.fromOffset(newPos.X, newPos.Y)
dragWindow.state.position.value = newPos
end
if isResizing and resizeWindow and resizeWindow.arguments.NoResize ~= true then
local Window = resizeWindow.Instance :: Frame
local resizeInstance: TextButton = Window.WindowButton
local windowPosition: Vector2 = Vector2.new(resizeInstance.Position.X.Offset, resizeInstance.Position.Y.Offset)
local windowSize: Vector2 = Vector2.new(resizeInstance.Size.X.Offset, resizeInstance.Size.Y.Offset)
local mouseDelta: Vector2 | Vector3
if input.UserInputType == Enum.UserInputType.Touch then
mouseDelta = input.Delta
else
mouseDelta = widgets.getMouseLocation() - lastCursorPosition
end
local intendedPosition: Vector2 = windowPosition + Vector2.new(if resizeFromLeftRight == Enum.LeftRight.Left then mouseDelta.X else 0, if resizeFromTopBottom == Enum.TopBottom.Top then mouseDelta.Y else 0)
local intendedSize: Vector2 = windowSize
+ Vector2.new(
if resizeFromLeftRight == Enum.LeftRight.Left then -mouseDelta.X elseif resizeFromLeftRight == Enum.LeftRight.Right then mouseDelta.X else 0,
if resizeFromTopBottom == Enum.TopBottom.Top then -mouseDelta.Y elseif resizeFromTopBottom == Enum.TopBottom.Bottom then mouseDelta.Y else 0
)
local newSize: Vector2 = fitSizeToWindowBounds(resizeWindow, intendedSize)
local newPosition: Vector2 = fitPositionToWindowBounds(resizeWindow, intendedPosition)
resizeInstance.Size = UDim2.fromOffset(newSize.X, newSize.Y)
resizeWindow.state.size.value = newSize
resizeInstance.Position = UDim2.fromOffset(newPosition.X, newPosition.Y)
resizeWindow.state.position.value = newPosition
end
lastCursorPosition = widgets.getMouseLocation()
end)
widgets.registerEvent("InputEnded", function(input, _)
if not ImGui._started then
return
end
if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) and isDragging and dragWindow then
local Window = dragWindow.Instance :: Frame
local dragInstance: TextButton = Window.WindowButton
isDragging = false
dragWindow.state.position:set(Vector2.new(dragInstance.Position.X.Offset, dragInstance.Position.Y.Offset))
end
if (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) and isResizing and resizeWindow then
local Window = resizeWindow.Instance :: Instance
isResizing = false
resizeWindow.state.size:set(Window.WindowButton.AbsoluteSize)
end
if input.KeyCode == Enum.KeyCode.ButtonX then
quickSwapWindows()
end
end)
--stylua: ignore
ImGui.WidgetConstructor("Window", {
hasState = true,
hasChildren = true,
Args = {
["Title"] = 1,
["NoTitleBar"] = 2,
["NoBackground"] = 3,
["NoCollapse"] = 4,
["NoClose"] = 5,
["NoMove"] = 6,
["NoScrollbar"] = 7,
["NoResize"] = 8,
["NoNav"] = 9,
["NoMenu"] = 10,
},
Events = {
["closed"] = {
["Init"] = function(_thisWidget: Window) end,
["Get"] = function(thisWidget: Window)
return thisWidget.lastClosedTick == ImGui._cycleTick
end,
},
["opened"] = {
["Init"] = function(_thisWidget: Window) end,
["Get"] = function(thisWidget: Window)
return thisWidget.lastOpenedTick == ImGui._cycleTick
end,
},
["collapsed"] = {
["Init"] = function(_thisWidget: Window) end,
["Get"] = function(thisWidget: Window)
return thisWidget.lastCollapsedTick == ImGui._cycleTick
end,
},
["uncollapsed"] = {
["Init"] = function(_thisWidget: Window) end,
["Get"] = function(thisWidget: Window)
return thisWidget.lastUncollapsedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
local Window = thisWidget.Instance :: Frame
return Window.WindowButton
end),
},
Generate = function(thisWidget: Window)
thisWidget.parentWidget = ImGui._rootWidget -- only allow root as parent
thisWidget.usesScreenGuis = ImGui._config.UseScreenGUIs
windowWidgets[thisWidget.ID] = thisWidget
local Window
if thisWidget.usesScreenGuis then
Window = Instance.new("ScreenGui")
Window.ResetOnSpawn = false
Window.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
Window.DisplayOrder = ImGui._config.DisplayOrderOffset
Window.IgnoreGuiInset = ImGui._config.IgnoreGuiInset
else
Window = Instance.new("Frame")
Window.AnchorPoint = Vector2.new(0.5, 0.5)
Window.Position = UDim2.new(0.5, 0, 0.5, 0)
Window.Size = UDim2.new(1, 0, 1, 0)
Window.BackgroundTransparency = 1
Window.ZIndex = ImGui._config.DisplayOrderOffset
end
Window.Name = "ImGui_Window"
local WindowButton: TextButton = Instance.new("TextButton")
WindowButton.Name = "WindowButton"
WindowButton.Size = UDim2.fromOffset(0, 0)
WindowButton.BackgroundTransparency = 1
WindowButton.BorderSizePixel = 0
WindowButton.Text = ""
WindowButton.ClipsDescendants = false
WindowButton.AutoButtonColor = false
WindowButton.Selectable = false
WindowButton.SelectionImageObject = ImGui.SelectionImageObject
WindowButton.SelectionGroup = true
WindowButton.SelectionBehaviorUp = Enum.SelectionBehavior.Stop
WindowButton.SelectionBehaviorDown = Enum.SelectionBehavior.Stop
WindowButton.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop
WindowButton.SelectionBehaviorRight = Enum.SelectionBehavior.Stop
widgets.UIStroke(WindowButton, ImGui._config.WindowBorderSize, ImGui._config.BorderColor, ImGui._config.BorderTransparency)
WindowButton.Parent = Window
widgets.applyInputBegan(WindowButton, function(input: InputObject)
if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Keyboard then
return
end
if thisWidget.state.isUncollapsed.value then
ImGui.SetFocusedWindow(thisWidget)
end
if not thisWidget.arguments.NoMove and input.UserInputType == Enum.UserInputType.MouseButton1 then
dragWindow = thisWidget
isDragging = true
moveDeltaCursorPosition = widgets.getMouseLocation() - thisWidget.state.position.value
end
end)
local Content: Frame = Instance.new("Frame")
Content.Name = "Content"
Content.AnchorPoint = Vector2.new(0.5, 0.5)
Content.Position = UDim2.fromScale(0.5, 0.5)
Content.Size = UDim2.fromScale(1, 1)
Content.BackgroundTransparency = 1
Content.ClipsDescendants = true
Content.Parent = WindowButton
local UIListLayout: UIListLayout = widgets.UIListLayout(Content, Enum.FillDirection.Vertical, UDim.new(0, 0))
UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
local ChildContainer: ScrollingFrame = Instance.new("ScrollingFrame")
ChildContainer.Name = "WindowContainer"
ChildContainer.Size = UDim2.fromScale(1, 1)
ChildContainer.BackgroundColor3 = ImGui._config.WindowBgColor
ChildContainer.BackgroundTransparency = ImGui._config.WindowBgTransparency
ChildContainer.BorderSizePixel = 0
ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y
ChildContainer.ScrollBarImageTransparency = ImGui._config.ScrollbarGrabTransparency
ChildContainer.ScrollBarImageColor3 = ImGui._config.ScrollbarGrabColor
ChildContainer.CanvasSize = UDim2.fromScale(0, 0)
ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ChildContainer.LayoutOrder = thisWidget.ZIndex + 0xFFFF
ChildContainer.ClipsDescendants = true
widgets.UIPadding(ChildContainer, ImGui._config.WindowPadding)
ChildContainer.Parent = Content
local UIFlexItem: UIFlexItem = Instance.new("UIFlexItem")
UIFlexItem.FlexMode = Enum.UIFlexMode.Fill
UIFlexItem.ItemLineAlignment = Enum.ItemLineAlignment.End
UIFlexItem.Parent = ChildContainer
ChildContainer:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
-- "wrong" use of state here, for optimization
thisWidget.state.scrollDistance.value = ChildContainer.CanvasPosition.Y
end)
widgets.applyInputBegan(ChildContainer, function(input: InputObject)
if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Keyboard then
return
end
if thisWidget.state.isUncollapsed.value then
ImGui.SetFocusedWindow(thisWidget)
end
end)
local TerminatingFrame: Frame = Instance.new("Frame")
TerminatingFrame.Name = "TerminatingFrame"
TerminatingFrame.Size = UDim2.fromOffset(0, ImGui._config.WindowPadding.Y + ImGui._config.FramePadding.Y)
TerminatingFrame.BackgroundTransparency = 1
TerminatingFrame.BorderSizePixel = 0
TerminatingFrame.LayoutOrder = 0x7FFFFFF0
local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
ChildContainerUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
TerminatingFrame.Parent = ChildContainer
local TitleBar: Frame = Instance.new("Frame")
TitleBar.Name = "TitleBar"
TitleBar.AutomaticSize = Enum.AutomaticSize.Y
TitleBar.Size = UDim2.fromScale(1, 0)
TitleBar.BorderSizePixel = 0
TitleBar.ClipsDescendants = true
TitleBar.Parent = Content
widgets.UIPadding(TitleBar, Vector2.new(ImGui._config.FramePadding.X))
widgets.UIListLayout(TitleBar, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X)).VerticalAlignment = Enum.VerticalAlignment.Center
widgets.applyInputBegan(TitleBar, function(input: InputObject)
if input.UserInputType == Enum.UserInputType.Touch then
if not thisWidget.arguments.NoMove then
dragWindow = thisWidget
isDragging = true
local location: Vector3 = input.Position
moveDeltaCursorPosition = Vector2.new(location.X, location.Y) - thisWidget.state.position.value
end
end
end)
local TitleButtonSize: number = ImGui._config.TextSize + ((ImGui._config.FramePadding.Y - 1) * 2)
local CollapseButton: TextButton = Instance.new("TextButton")
CollapseButton.Name = "CollapseButton"
CollapseButton.AnchorPoint = Vector2.new(0, 0.5)
CollapseButton.Size = UDim2.fromOffset(TitleButtonSize, TitleButtonSize)
CollapseButton.Position = UDim2.new(0, 0, 0.5, 0)
CollapseButton.AutomaticSize = Enum.AutomaticSize.None
CollapseButton.BackgroundTransparency = 1
CollapseButton.BorderSizePixel = 0
CollapseButton.AutoButtonColor = false
CollapseButton.Text = ""
widgets.UICorner(CollapseButton)
CollapseButton.Parent = TitleBar
widgets.applyButtonClick(CollapseButton, function()
thisWidget.state.isUncollapsed:set(not thisWidget.state.isUncollapsed.value)
end)
widgets.applyInteractionHighlights("Background", CollapseButton, CollapseButton, {
Color = ImGui._config.ButtonColor,
Transparency = 1,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonActiveColor,
ActiveTransparency = ImGui._config.ButtonActiveTransparency,
})
local CollapseArrow: ImageLabel = Instance.new("ImageLabel")
CollapseArrow.Name = "Arrow"
CollapseArrow.AnchorPoint = Vector2.new(0.5, 0.5)
CollapseArrow.Size = UDim2.fromOffset(math.floor(0.7 * TitleButtonSize), math.floor(0.7 * TitleButtonSize))
CollapseArrow.Position = UDim2.fromScale(0.5, 0.5)
CollapseArrow.BackgroundTransparency = 1
CollapseArrow.BorderSizePixel = 0
CollapseArrow.Image = widgets.ICONS.MULTIPLICATION_SIGN
CollapseArrow.ImageColor3 = ImGui._config.TextColor
CollapseArrow.ImageTransparency = ImGui._config.TextTransparency
CollapseArrow.Parent = CollapseButton
local CloseButton: TextButton = Instance.new("TextButton")
CloseButton.Name = "CloseButton"
CloseButton.AnchorPoint = Vector2.new(1, 0.5)
CloseButton.Size = UDim2.fromOffset(TitleButtonSize, TitleButtonSize)
CloseButton.Position = UDim2.new(1, 0, 0.5, 0)
CloseButton.AutomaticSize = Enum.AutomaticSize.None
CloseButton.BackgroundTransparency = 1
CloseButton.BorderSizePixel = 0
CloseButton.Text = ""
CloseButton.LayoutOrder = 2
CloseButton.AutoButtonColor = false
widgets.UICorner(CloseButton)
widgets.applyButtonClick(CloseButton, function()
thisWidget.state.isOpened:set(false)
end)
widgets.applyInteractionHighlights("Background", CloseButton, CloseButton, {
Color = ImGui._config.ButtonColor,
Transparency = 1,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonActiveColor,
ActiveTransparency = ImGui._config.ButtonActiveTransparency,
})
CloseButton.Parent = TitleBar
local CloseIcon: ImageLabel = Instance.new("ImageLabel")
CloseIcon.Name = "Icon"
CloseIcon.AnchorPoint = Vector2.new(0.5, 0.5)
CloseIcon.Size = UDim2.fromOffset(math.floor(0.7 * TitleButtonSize), math.floor(0.7 * TitleButtonSize))
CloseIcon.Position = UDim2.fromScale(0.5, 0.5)
CloseIcon.BackgroundTransparency = 1
CloseIcon.BorderSizePixel = 0
CloseIcon.Image = widgets.ICONS.MULTIPLICATION_SIGN
CloseIcon.ImageColor3 = ImGui._config.TextColor
CloseIcon.ImageTransparency = ImGui._config.TextTransparency
CloseIcon.Parent = CloseButton
-- allowing fractional titlebar title location dosent seem useful, as opposed to Enum.LeftRight.
local Title: TextLabel = Instance.new("TextLabel")
Title.Name = "Title"
Title.AutomaticSize = Enum.AutomaticSize.XY
Title.BorderSizePixel = 0
Title.BackgroundTransparency = 1
Title.LayoutOrder = 1
Title.ClipsDescendants = true
widgets.UIPadding(Title, Vector2.new(0, ImGui._config.FramePadding.Y))
widgets.applyTextStyle(Title)
Title.TextXAlignment = Enum.TextXAlignment[ImGui._config.WindowTitleAlign.Name] :: Enum.TextXAlignment
local TitleFlexItem: UIFlexItem = Instance.new("UIFlexItem")
TitleFlexItem.FlexMode = Enum.UIFlexMode.Fill
TitleFlexItem.ItemLineAlignment = Enum.ItemLineAlignment.Center
TitleFlexItem.Parent = Title
Title.Parent = TitleBar
local ResizeButtonSize: number = ImGui._config.TextSize + ImGui._config.FramePadding.X
local ResizeGrip = Instance.new("ImageButton")
ResizeGrip.Name = "ResizeGrip"
ResizeGrip.AnchorPoint = Vector2.one
ResizeGrip.Size = UDim2.fromOffset(ResizeButtonSize, ResizeButtonSize)
ResizeGrip.Position = UDim2.fromScale(1, 1)
ResizeGrip.Rotation = 90
ResizeGrip.AutoButtonColor = false
ResizeGrip.BorderSizePixel = 0
ResizeGrip.BackgroundTransparency = 1
ResizeGrip.Image = widgets.ICONS.BOTTOM_RIGHT_CORNER
ResizeGrip.ImageColor3 = ImGui._config.ButtonColor
ResizeGrip.ImageTransparency = ImGui._config.ButtonTransparency
ResizeGrip.Selectable = false
ResizeGrip.ZIndex = 3
ResizeGrip.Parent = WindowButton
widgets.applyInteractionHighlights("Image", ResizeGrip, ResizeGrip, {
Color = ImGui._config.ButtonColor,
Transparency = ImGui._config.ButtonTransparency,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonActiveColor,
ActiveTransparency = ImGui._config.ButtonActiveTransparency,
})
widgets.applyButtonDown(ResizeGrip, function()
if not anyFocusedWindow or not (focusedWindow == thisWidget) then
ImGui.SetFocusedWindow(thisWidget)
-- mitigating wrong focus when clicking on buttons inside of a window without clicking the window itself
end
isResizing = true
resizeFromTopBottom = Enum.TopBottom.Bottom
resizeFromLeftRight = Enum.LeftRight.Right
resizeWindow = thisWidget
end)
local ResizeBorder: Frame = Instance.new("Frame")
ResizeBorder.Name = "ResizeBorder"
ResizeBorder.Size = UDim2.new(1, ImGui._config.WindowResizePadding.X * 2, 1, ImGui._config.WindowResizePadding.Y * 2)
ResizeBorder.Position = UDim2.fromOffset(-ImGui._config.WindowResizePadding.X, -ImGui._config.WindowResizePadding.Y)
ResizeBorder.BackgroundTransparency = 1
ResizeBorder.BorderSizePixel = 0
ResizeBorder.Active = true
ResizeBorder.Selectable = false
ResizeBorder.ClipsDescendants = false
ResizeBorder.Parent = WindowButton
widgets.applyMouseEnter(ResizeBorder, function()
if focusedWindow == thisWidget then
isInsideResize = true
end
end)
widgets.applyMouseLeave(ResizeBorder, function()
if focusedWindow == thisWidget then
isInsideResize = false
end
end)
widgets.applyMouseEnter(WindowButton, function()
if focusedWindow == thisWidget then
isInsideWindow = true
end
end)
widgets.applyMouseLeave(WindowButton, function()
if focusedWindow == thisWidget then
isInsideWindow = false
end
end)
thisWidget.ChildContainer = ChildContainer
return Window
end,
Update = function(thisWidget: Window)
local Window = thisWidget.Instance :: GuiObject
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
local WindowButton = Window.WindowButton :: TextButton
local Content = WindowButton.Content :: Frame
local TitleBar = Content.TitleBar :: Frame
local Title: TextLabel = TitleBar.Title
local MenuBar: Frame? = Content:FindFirstChild("MenuBar")
local ResizeGrip: TextButton = WindowButton.ResizeGrip
if thisWidget.arguments.NoResize ~= true then
ResizeGrip.Visible = true
else
ResizeGrip.Visible = false
end
if thisWidget.arguments.NoScrollbar then
ChildContainer.ScrollBarThickness = 0
else
ChildContainer.ScrollBarThickness = ImGui._config.ScrollbarSize
end
if thisWidget.arguments.NoTitleBar then
TitleBar.Visible = false
else
TitleBar.Visible = true
end
if MenuBar then
if thisWidget.arguments.NoMenu then
MenuBar.Visible = false
else
MenuBar.Visible = true
end
end
if thisWidget.arguments.NoBackground then
ChildContainer.BackgroundTransparency = 1
else
ChildContainer.BackgroundTransparency = ImGui._config.WindowBgTransparency
end
-- TitleBar buttons
if thisWidget.arguments.NoCollapse then
TitleBar.CollapseButton.Visible = false
else
TitleBar.CollapseButton.Visible = true
end
if thisWidget.arguments.NoClose then
TitleBar.CloseButton.Visible = false
else
TitleBar.CloseButton.Visible = true
end
Title.Text = thisWidget.arguments.Title or ""
end,
Discard = function(thisWidget: Window)
if focusedWindow == thisWidget then
focusedWindow = nil
anyFocusedWindow = false
end
if dragWindow == thisWidget then
dragWindow = nil
isDragging = false
end
if resizeWindow == thisWidget then
resizeWindow = nil
isResizing = false
end
windowWidgets[thisWidget.ID] = nil
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
ChildAdded = function(thisWidget: Window, thisChid: Widget)
local Window = thisWidget.Instance :: Frame
local WindowButton = Window.WindowButton :: TextButton
local Content = WindowButton.Content :: Frame
if thisChid.type == "MenuBar" then
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
thisChid.Instance.ZIndex = ChildContainer.ZIndex + 1
thisChid.Instance.LayoutOrder = ChildContainer.LayoutOrder - 1
return Content
end
return thisWidget.ChildContainer
end,
UpdateState = function(thisWidget: Window)
local stateSize: Vector2 = thisWidget.state.size.value
local statePosition: Vector2 = thisWidget.state.position.value
local stateIsUncollapsed: boolean = thisWidget.state.isUncollapsed.value
local stateIsOpened: boolean = thisWidget.state.isOpened.value
local stateScrollDistance: number = thisWidget.state.scrollDistance.value
local Window = thisWidget.Instance :: Frame
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
local WindowButton = Window.WindowButton :: TextButton
local Content = WindowButton.Content :: Frame
local TitleBar = Content.TitleBar :: Frame
local MenuBar: Frame? = Content:FindFirstChild("MenuBar")
local ResizeGrip: TextButton = WindowButton.ResizeGrip
WindowButton.Size = UDim2.fromOffset(stateSize.X, stateSize.Y)
WindowButton.Position = UDim2.fromOffset(statePosition.X, statePosition.Y)
if stateIsOpened then
if thisWidget.usesScreenGuis then
Window.Enabled = true
WindowButton.Visible = true
else
Window.Visible = true
WindowButton.Visible = true
end
thisWidget.lastOpenedTick = ImGui._cycleTick + 1
else
if thisWidget.usesScreenGuis then
Window.Enabled = false
WindowButton.Visible = false
else
Window.Visible = false
WindowButton.Visible = false
end
thisWidget.lastClosedTick = ImGui._cycleTick + 1
end
if stateIsUncollapsed then
TitleBar.CollapseButton.Arrow.Image = widgets.ICONS.DOWN_POINTING_TRIANGLE
if MenuBar then
MenuBar.Visible = not thisWidget.arguments.NoMenu
end
ChildContainer.Visible = true
if thisWidget.arguments.NoResize ~= true then
ResizeGrip.Visible = true
end
WindowButton.AutomaticSize = Enum.AutomaticSize.None
thisWidget.lastUncollapsedTick = ImGui._cycleTick + 1
else
local collapsedHeight: number = TitleBar.AbsoluteSize.Y -- ImGui._config.TextSize + ImGui._config.FramePadding.Y * 2
TitleBar.CollapseButton.Arrow.Image = widgets.ICONS.RIGHT_POINTING_TRIANGLE
if MenuBar then
MenuBar.Visible = false
end
ChildContainer.Visible = false
ResizeGrip.Visible = false
WindowButton.Size = UDim2.fromOffset(stateSize.X, collapsedHeight)
thisWidget.lastCollapsedTick = ImGui._cycleTick + 1
end
if stateIsOpened and stateIsUncollapsed then
ImGui.SetFocusedWindow(thisWidget)
else
TitleBar.BackgroundColor3 = ImGui._config.TitleBgCollapsedColor
TitleBar.BackgroundTransparency = ImGui._config.TitleBgCollapsedTransparency
WindowButton.UIStroke.Color = ImGui._config.BorderColor
ImGui.SetFocusedWindow(nil)
end
-- cant update canvasPosition in this cycle because scrollingframe isint ready to be changed
if stateScrollDistance and stateScrollDistance ~= 0 then
local callbackIndex: number = #ImGui._postCycleCallbacks + 1
local desiredCycleTick: number = ImGui._cycleTick + 1
ImGui._postCycleCallbacks[callbackIndex] = function()
if ImGui._cycleTick >= desiredCycleTick then
if thisWidget.lastCycleTick ~= -1 then
ChildContainer.CanvasPosition = Vector2.new(0, stateScrollDistance)
end
ImGui._postCycleCallbacks[callbackIndex] = nil
end
end
end
end,
GenerateState = function(thisWidget: Window)
if thisWidget.state.size == nil then
thisWidget.state.size = ImGui._widgetState(thisWidget, "size", Vector2.new(400, 300))
end
if thisWidget.state.position == nil then
thisWidget.state.position = ImGui._widgetState(thisWidget, "position", if anyFocusedWindow and focusedWindow then focusedWindow.state.position.value + Vector2.new(15, 45) else Vector2.new(150, 250))
end
thisWidget.state.position.value = fitPositionToWindowBounds(thisWidget, thisWidget.state.position.value)
thisWidget.state.size.value = fitSizeToWindowBounds(thisWidget, thisWidget.state.size.value)
if thisWidget.state.isUncollapsed == nil then
thisWidget.state.isUncollapsed = ImGui._widgetState(thisWidget, "isUncollapsed", true)
end
if thisWidget.state.isOpened == nil then
thisWidget.state.isOpened = ImGui._widgetState(thisWidget, "isOpened", true)
end
if thisWidget.state.scrollDistance == nil then
thisWidget.state.scrollDistance = ImGui._widgetState(thisWidget, "scrollDistance", 0)
end
end,
} :: WidgetClass)
end,
Menu = function(ImGui: Internal, widgets: WidgetUtility)
local AnyMenuOpen: boolean = false
local ActiveMenu: Menu? = nil
local MenuStack: { Menu } = {}
local function EmptyMenuStack(menuIndex: number?)
for index = #MenuStack, menuIndex and menuIndex + 1 or 1, -1 do
local widget: Menu = MenuStack[index]
widget.state.isOpened:set(false)
widget.Instance.BackgroundColor3 = ImGui._config.HeaderColor
widget.Instance.BackgroundTransparency = 1
table.remove(MenuStack, index)
end
if #MenuStack == 0 then
AnyMenuOpen = false
ActiveMenu = nil
end
end
local function UpdateChildContainerTransform(thisWidget: Menu)
local submenu: boolean = thisWidget.parentWidget.type == "Menu"
local Menu = thisWidget.Instance :: Frame
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
ChildContainer.Size = UDim2.fromOffset(Menu.AbsoluteSize.X, 0)
if ChildContainer.Parent == nil then
return
end
local menuPosition: Vector2 = Menu.AbsolutePosition - widgets.GuiOffset
local menuSize: Vector2 = Menu.AbsoluteSize
local containerSize: Vector2 = ChildContainer.AbsoluteSize
local borderSize: number = ImGui._config.PopupBorderSize
local screenSize: Vector2 = ChildContainer.Parent.AbsoluteSize
local x: number = menuPosition.X
local y: number
local anchor: Vector2 = Vector2.zero
if submenu then
if menuPosition.X + containerSize.X > screenSize.X then
anchor = Vector2.xAxis
else
x = menuPosition.X + menuSize.X
end
end
if menuPosition.Y + containerSize.Y > screenSize.Y then
-- too low.
y = menuPosition.Y - borderSize + (submenu and menuSize.Y or 0)
anchor += Vector2.yAxis
else
y = menuPosition.Y + borderSize + (submenu and 0 or menuSize.Y)
end
ChildContainer.Position = UDim2.fromOffset(x, y)
ChildContainer.AnchorPoint = anchor
end
widgets.registerEvent("InputBegan", function(inputObject: InputObject)
if not ImGui._started then
return
end
if inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 and inputObject.UserInputType ~= Enum.UserInputType.MouseButton2 then
return
end
if AnyMenuOpen == false then
return
end
if ActiveMenu == nil then
return
end
-- this only checks if we clicked outside all the menus. If we clicked in any menu, then the hover function handles this.
local isInMenu: boolean = false
local MouseLocation: Vector2 = widgets.getMouseLocation()
for _, menu: Menu in MenuStack do
for _, container: GuiObject in { menu.ChildContainer, menu.Instance } do
local rectMin: Vector2 = container.AbsolutePosition - widgets.GuiOffset
local rectMax: Vector2 = rectMin + container.AbsoluteSize
if widgets.isPosInsideRect(MouseLocation, rectMin, rectMax) then
isInMenu = true
break
end
end
if isInMenu then
break
end
end
if not isInMenu then
EmptyMenuStack()
end
end)
--stylua: ignore
ImGui.WidgetConstructor("MenuBar", {
hasState = false,
hasChildren = true,
Args = {},
Events = {},
Generate = function(thisWidget: MenuBar)
local MenuBar: Frame = Instance.new("Frame")
MenuBar.Name = "MenuBar"
MenuBar.Size = UDim2.fromScale(1, 0)
MenuBar.AutomaticSize = Enum.AutomaticSize.Y
MenuBar.BackgroundColor3 = ImGui._config.MenubarBgColor
MenuBar.BackgroundTransparency = ImGui._config.MenubarBgTransparency
MenuBar.BorderSizePixel = 0
MenuBar.LayoutOrder = thisWidget.ZIndex
MenuBar.ClipsDescendants = true
widgets.UIPadding(MenuBar, Vector2.new(ImGui._config.WindowPadding.X, 1))
widgets.UIListLayout(MenuBar, Enum.FillDirection.Horizontal, UDim.new()).VerticalAlignment = Enum.VerticalAlignment.Center
widgets.applyFrameStyle(MenuBar, true, true)
return MenuBar
end,
Update = function()
end,
ChildAdded = function(thisWidget: MenuBar, _thisChild: Widget)
return thisWidget.Instance
end,
Discard = function(thisWidget: MenuBar)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
--stylua: ignore
ImGui.WidgetConstructor("Menu", {
hasState = true,
hasChildren = true,
Args = {
["Text"] = 1,
},
Events = {
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
["opened"] = {
["Init"] = function(_thisWidget: Menu) end,
["Get"] = function(thisWidget: Menu)
return thisWidget.lastOpenedTick == ImGui._cycleTick
end,
},
["closed"] = {
["Init"] = function(_thisWidget: Menu) end,
["Get"] = function(thisWidget: Menu)
return thisWidget.lastClosedTick == ImGui._cycleTick
end,
},
},
Generate = function(thisWidget: Menu)
local Menu: TextButton
thisWidget.ButtonColors = {
Color = ImGui._config.HeaderColor,
Transparency = 1,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderHoveredColor,
ActiveTransparency = ImGui._config.HeaderHoveredTransparency,
}
if thisWidget.parentWidget.type == "Menu" then
-- this Menu is a sub-Menu
Menu = Instance.new("TextButton")
Menu.Name = "Menu"
Menu.BackgroundColor3 = ImGui._config.HeaderColor
Menu.BackgroundTransparency = 1
Menu.BorderSizePixel = 0
Menu.Size = UDim2.fromScale(1, 0)
Menu.Text = ""
Menu.AutomaticSize = Enum.AutomaticSize.Y
Menu.LayoutOrder = thisWidget.ZIndex
Menu.AutoButtonColor = false
local UIPadding = widgets.UIPadding(Menu, ImGui._config.FramePadding)
UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1)
widgets.UIListLayout(Menu, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X)).VerticalAlignment = Enum.VerticalAlignment.Center
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Menu
local frameSize: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local padding: number = math.round(0.2 * frameSize)
local iconSize: number = frameSize - 2 * padding
local Icon: ImageLabel = Instance.new("ImageLabel")
Icon.Name = "Icon"
Icon.Size = UDim2.fromOffset(iconSize, iconSize)
Icon.BackgroundTransparency = 1
Icon.BorderSizePixel = 0
Icon.ImageColor3 = ImGui._config.TextColor
Icon.ImageTransparency = ImGui._config.TextTransparency
Icon.Image = widgets.ICONS.RIGHT_POINTING_TRIANGLE
Icon.LayoutOrder = 1
Icon.Parent = Menu
else
Menu = Instance.new("TextButton")
Menu.Name = "Menu"
Menu.AutomaticSize = Enum.AutomaticSize.XY
Menu.Size = UDim2.fromScale(0, 0)
Menu.BackgroundColor3 = ImGui._config.HeaderColor
Menu.BackgroundTransparency = 1
Menu.BorderSizePixel = 0
Menu.Text = ""
Menu.LayoutOrder = thisWidget.ZIndex
Menu.AutoButtonColor = false
Menu.ClipsDescendants = true
widgets.applyTextStyle(Menu)
widgets.UIPadding(Menu, Vector2.new(ImGui._config.ItemSpacing.X, ImGui._config.FramePadding.Y))
end
widgets.applyInteractionHighlights("Background", Menu, Menu, thisWidget.ButtonColors)
widgets.applyButtonClick(Menu, function()
local openMenu: boolean = if #MenuStack <= 1 then not thisWidget.state.isOpened.value else true
thisWidget.state.isOpened:set(openMenu)
AnyMenuOpen = openMenu
ActiveMenu = openMenu and thisWidget or nil
-- the hovering should handle all of the menus after the first one.
if #MenuStack <= 1 then
if openMenu then
table.insert(MenuStack, thisWidget)
else
table.remove(MenuStack)
end
end
end)
widgets.applyMouseEnter(Menu, function()
if AnyMenuOpen and ActiveMenu and ActiveMenu ~= thisWidget then
local parentMenu = thisWidget.parentWidget :: Menu
local parentIndex: number? = table.find(MenuStack, parentMenu)
EmptyMenuStack(parentIndex)
thisWidget.state.isOpened:set(true)
ActiveMenu = thisWidget
AnyMenuOpen = true
table.insert(MenuStack, thisWidget)
end
end)
local ChildContainer: ScrollingFrame = Instance.new("ScrollingFrame")
ChildContainer.Name = "MenuContainer"
ChildContainer.BackgroundColor3 = ImGui._config.PopupBgColor
ChildContainer.BackgroundTransparency = ImGui._config.PopupBgTransparency
ChildContainer.BorderSizePixel = 0
ChildContainer.Size = UDim2.fromOffset(0, 0)
ChildContainer.AutomaticSize = Enum.AutomaticSize.XY
ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y
ChildContainer.ScrollBarImageTransparency = ImGui._config.ScrollbarGrabTransparency
ChildContainer.ScrollBarImageColor3 = ImGui._config.ScrollbarGrabColor
ChildContainer.ScrollBarThickness = ImGui._config.ScrollbarSize
ChildContainer.CanvasSize = UDim2.fromScale(0, 0)
ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ChildContainer.ZIndex = 6
ChildContainer.LayoutOrder = 6
ChildContainer.ClipsDescendants = true
-- Unfortunatley, ScrollingFrame does not work with UICorner
-- if ImGui._config.PopupRounding > 0 then
-- widgets.UICorner(ChildContainer, ImGui._config.PopupRounding)
-- end
widgets.UIStroke(ChildContainer, ImGui._config.WindowBorderSize, ImGui._config.BorderColor, ImGui._config.BorderTransparency)
widgets.UIPadding(ChildContainer, Vector2.new(2, ImGui._config.WindowPadding.Y - ImGui._config.ItemSpacing.Y))
local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, 1))
ChildContainerUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
local RootPopupScreenGui = ImGui._rootInstance and ImGui._rootInstance:FindFirstChild("PopupScreenGui") :: GuiObject
ChildContainer.Parent = RootPopupScreenGui
thisWidget.ChildContainer = ChildContainer
return Menu
end,
Update = function(thisWidget: Menu)
local Menu = thisWidget.Instance :: TextButton
local TextLabel: TextLabel
if thisWidget.parentWidget.type == "Menu" then
TextLabel = Menu.TextLabel
else
TextLabel = Menu
end
TextLabel.Text = thisWidget.arguments.Text or "Menu"
end,
ChildAdded = function(thisWidget: Menu, _thisChild: Widget)
UpdateChildContainerTransform(thisWidget)
return thisWidget.ChildContainer
end,
ChildDiscarded = function(thisWidget: Menu, _thisChild: Widget)
UpdateChildContainerTransform(thisWidget)
end,
GenerateState = function(thisWidget: Menu)
if thisWidget.state.isOpened == nil then
thisWidget.state.isOpened = ImGui._widgetState(thisWidget, "isOpened", false)
end
end,
UpdateState = function(thisWidget: Menu)
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
if thisWidget.state.isOpened.value then
thisWidget.lastOpenedTick = ImGui._cycleTick + 1
thisWidget.ButtonColors.Transparency = ImGui._config.HeaderTransparency
ChildContainer.Visible = true
UpdateChildContainerTransform(thisWidget)
else
thisWidget.lastClosedTick = ImGui._cycleTick + 1
thisWidget.ButtonColors.Transparency = 1
ChildContainer.Visible = false
end
end,
Discard = function(thisWidget: Menu)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
} :: WidgetClass)
--stylua: ignore
ImGui.WidgetConstructor("MenuItem", {
hasState = false,
hasChildren = false,
Args = {
Text = 1,
KeyCode = 2,
ModifierKey = 3,
},
Events = {
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: MenuItem)
local MenuItem: TextButton = Instance.new("TextButton")
MenuItem.Name = "MenuItem"
MenuItem.BackgroundTransparency = 1
MenuItem.BorderSizePixel = 0
MenuItem.Size = UDim2.fromScale(1, 0)
MenuItem.Text = ""
MenuItem.AutomaticSize = Enum.AutomaticSize.Y
MenuItem.LayoutOrder = thisWidget.ZIndex
MenuItem.AutoButtonColor = false
local UIPadding = widgets.UIPadding(MenuItem, ImGui._config.FramePadding)
UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1)
widgets.UIListLayout(MenuItem, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
widgets.applyInteractionHighlights("Background", MenuItem, MenuItem, {
Color = ImGui._config.HeaderColor,
Transparency = 1,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderHoveredColor,
ActiveTransparency = ImGui._config.HeaderHoveredTransparency,
})
widgets.applyButtonClick(MenuItem, function()
EmptyMenuStack()
end)
widgets.applyMouseEnter(MenuItem, function()
local parentMenu = thisWidget.parentWidget :: Menu
if AnyMenuOpen and ActiveMenu and ActiveMenu ~= parentMenu then
local parentIndex: number? = table.find(MenuStack, parentMenu)
EmptyMenuStack(parentIndex)
ActiveMenu = parentMenu
AnyMenuOpen = true
end
end)
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = MenuItem
local Shortcut: TextLabel = Instance.new("TextLabel")
Shortcut.Name = "Shortcut"
Shortcut.BackgroundTransparency = 1
Shortcut.BorderSizePixel = 0
Shortcut.LayoutOrder = 1
Shortcut.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(Shortcut)
Shortcut.Text = ""
Shortcut.TextColor3 = ImGui._config.TextDisabledColor
Shortcut.TextTransparency = ImGui._config.TextDisabledTransparency
Shortcut.Parent = MenuItem
return MenuItem
end,
Update = function(thisWidget: MenuItem)
local MenuItem = thisWidget.Instance :: TextButton
local TextLabel: TextLabel = MenuItem.TextLabel
local Shortcut: TextLabel = MenuItem.Shortcut
TextLabel.Text = thisWidget.arguments.Text
if thisWidget.arguments.KeyCode then
if thisWidget.arguments.ModifierKey then
Shortcut.Text = thisWidget.arguments.ModifierKey.Name .. " + " .. thisWidget.arguments.KeyCode.Name
else
Shortcut.Text = thisWidget.arguments.KeyCode.Name
end
end
end,
Discard = function(thisWidget: MenuItem)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
--stylua: ignore
ImGui.WidgetConstructor("MenuToggle", {
hasState = true,
hasChildren = false,
Args = {
Text = 1,
KeyCode = 2,
ModifierKey = 3,
},
Events = {
["checked"] = {
["Init"] = function(_thisWidget: MenuToggle) end,
["Get"] = function(thisWidget: MenuToggle): boolean
return thisWidget.lastCheckedTick == ImGui._cycleTick
end,
},
["unchecked"] = {
["Init"] = function(_thisWidget: MenuToggle) end,
["Get"] = function(thisWidget: MenuToggle): boolean
return thisWidget.lastUncheckedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: MenuToggle)
local MenuItem: TextButton = Instance.new("TextButton")
MenuItem.Name = "MenuItem"
MenuItem.BackgroundTransparency = 1
MenuItem.BorderSizePixel = 0
MenuItem.Size = UDim2.fromScale(1, 0)
MenuItem.Text = ""
MenuItem.AutomaticSize = Enum.AutomaticSize.Y
MenuItem.LayoutOrder = thisWidget.ZIndex
MenuItem.AutoButtonColor = false
local UIPadding = widgets.UIPadding(MenuItem, ImGui._config.FramePadding)
UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1)
widgets.UIListLayout(MenuItem, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X)).VerticalAlignment = Enum.VerticalAlignment.Center
widgets.applyInteractionHighlights("Background", MenuItem, MenuItem, {
Color = ImGui._config.HeaderColor,
Transparency = 1,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderHoveredColor,
ActiveTransparency = ImGui._config.HeaderHoveredTransparency,
})
widgets.applyButtonClick(MenuItem, function()
local wasChecked: boolean = thisWidget.state.isChecked.value
thisWidget.state.isChecked:set(not wasChecked)
EmptyMenuStack()
end)
widgets.applyMouseEnter(MenuItem, function()
local parentMenu = thisWidget.parentWidget :: Menu
if AnyMenuOpen and ActiveMenu and ActiveMenu ~= parentMenu then
local parentIndex: number? = table.find(MenuStack, parentMenu)
EmptyMenuStack(parentIndex)
ActiveMenu = parentMenu
AnyMenuOpen = true
end
end)
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = MenuItem
local Shortcut: TextLabel = Instance.new("TextLabel")
Shortcut.Name = "Shortcut"
Shortcut.BackgroundTransparency = 1
Shortcut.BorderSizePixel = 0
Shortcut.LayoutOrder = 1
Shortcut.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(Shortcut)
Shortcut.Text = ""
Shortcut.TextColor3 = ImGui._config.TextDisabledColor
Shortcut.TextTransparency = ImGui._config.TextDisabledTransparency
Shortcut.Parent = MenuItem
local frameSize: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local padding: number = math.round(0.2 * frameSize)
local iconSize: number = frameSize - 2 * padding
local Icon: ImageLabel = Instance.new("ImageLabel")
Icon.Name = "Icon"
Icon.Size = UDim2.fromOffset(iconSize, iconSize)
Icon.BackgroundTransparency = 1
Icon.BorderSizePixel = 0
Icon.ImageColor3 = ImGui._config.TextColor
Icon.ImageTransparency = ImGui._config.TextTransparency
Icon.Image = widgets.ICONS.CHECK_MARK
Icon.LayoutOrder = 2
Icon.Parent = MenuItem
return MenuItem
end,
GenerateState = function(thisWidget: MenuToggle)
if thisWidget.state.isChecked == nil then
thisWidget.state.isChecked = ImGui._widgetState(thisWidget, "isChecked", false)
end
end,
Update = function(thisWidget: MenuToggle)
local MenuItem = thisWidget.Instance :: TextButton
local TextLabel: TextLabel = MenuItem.TextLabel
local Shortcut: TextLabel = MenuItem.Shortcut
TextLabel.Text = thisWidget.arguments.Text
if thisWidget.arguments.KeyCode then
if thisWidget.arguments.ModifierKey then
Shortcut.Text = thisWidget.arguments.ModifierKey.Name .. " + " .. thisWidget.arguments.KeyCode.Name
else
Shortcut.Text = thisWidget.arguments.KeyCode.Name
end
end
end,
UpdateState = function(thisWidget: MenuToggle)
local MenuItem = thisWidget.Instance :: TextButton
local Icon: ImageLabel = MenuItem.Icon
if thisWidget.state.isChecked.value then
Icon.Image = widgets.ICONS.CHECK_MARK
thisWidget.lastCheckedTick = ImGui._cycleTick + 1
else
Icon.Image = ""
thisWidget.lastUncheckedTick = ImGui._cycleTick + 1
end
end,
Discard = function(thisWidget: MenuToggle)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
} :: WidgetClass)
end,
Format = function(ImGui: Internal, widgets: WidgetUtility)
ImGui.WidgetConstructor("Separator", {
hasState = false,
hasChildren = false,
Args = {},
Events = {},
Generate = function(thisWidget: Separator)
local Separator: Frame = Instance.new("Frame")
Separator.Name = "ImGui_Separator"
Separator.BackgroundColor3 = ImGui._config.SeparatorColor
Separator.BackgroundTransparency = ImGui._config.SeparatorTransparency
Separator.BorderSizePixel = 0
if thisWidget.parentWidget.type == "SameLine" then
Separator.Size = UDim2.new(0, 1, 1, 0)
else
Separator.Size = UDim2.new(1, 0, 0, 1)
end
Separator.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(Separator, Enum.FillDirection.Vertical, UDim.new(0, 0))
return Separator
end,
Update = function(_thisWidget: Separator) end,
Discard = function(thisWidget: Separator)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
ImGui.WidgetConstructor("Indent", {
hasState = false,
hasChildren = true,
Args = {
["Width"] = 1,
},
Events = {},
Generate = function(thisWidget: Indent)
local Indent: Frame = Instance.new("Frame")
Indent.Name = "ImGui_Indent"
Indent.BackgroundTransparency = 1
Indent.BorderSizePixel = 0
Indent.Size = UDim2.fromScale(1, 0)
Indent.AutomaticSize = Enum.AutomaticSize.Y
Indent.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(Indent, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
widgets.UIPadding(Indent, Vector2.zero)
return Indent
end,
Update = function(thisWidget: Indent)
local Indent = thisWidget.Instance :: Frame
local indentWidth: number
if thisWidget.arguments.Width then
indentWidth = thisWidget.arguments.Width
else
indentWidth = ImGui._config.IndentSpacing
end
Indent.UIPadding.PaddingLeft = UDim.new(0, indentWidth)
end,
Discard = function(thisWidget: Indent)
thisWidget.Instance:Destroy()
end,
ChildAdded = function(thisWidget: Indent, _thisChild: Widget)
return thisWidget.Instance
end,
} :: WidgetClass)
ImGui.WidgetConstructor("SameLine", {
hasState = false,
hasChildren = true,
Args = {
["Width"] = 1,
["VerticalAlignment"] = 2,
["HorizontalAlignment"] = 3,
},
Events = {},
Generate = function(thisWidget: SameLine)
local SameLine: Frame = Instance.new("Frame")
SameLine.Name = "ImGui_SameLine"
SameLine.BackgroundTransparency = 1
SameLine.BorderSizePixel = 0
SameLine.Size = UDim2.fromScale(1, 0)
SameLine.AutomaticSize = Enum.AutomaticSize.Y
SameLine.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(SameLine, Enum.FillDirection.Horizontal, UDim.new(0, 0))
return SameLine
end,
Update = function(thisWidget: SameLine)
local Sameline = thisWidget.Instance :: Frame
local uiListLayout: UIListLayout = Sameline.UIListLayout
local itemWidth: number
if thisWidget.arguments.Width then
itemWidth = thisWidget.arguments.Width
else
itemWidth = ImGui._config.ItemSpacing.X
end
uiListLayout.Padding = UDim.new(0, itemWidth)
if thisWidget.arguments.VerticalAlignment then
uiListLayout.VerticalAlignment = thisWidget.arguments.VerticalAlignment
else
uiListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
end
if thisWidget.arguments.HorizontalAlignment then
uiListLayout.HorizontalAlignment = thisWidget.arguments.HorizontalAlignment
else
uiListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left
end
end,
Discard = function(thisWidget: SameLine)
thisWidget.Instance:Destroy()
end,
ChildAdded = function(thisWidget: SameLine, _thisChild: Widget)
return thisWidget.Instance
end,
} :: WidgetClass)
ImGui.WidgetConstructor("Group", {
hasState = false,
hasChildren = true,
Args = {},
Events = {},
Generate = function(thisWidget: Group)
local Group: Frame = Instance.new("Frame")
Group.Name = "ImGui_Group"
Group.AutomaticSize = Enum.AutomaticSize.XY
Group.Size = UDim2.fromOffset(0, 0)
Group.BackgroundTransparency = 1
Group.BorderSizePixel = 0
Group.LayoutOrder = thisWidget.ZIndex
Group.ClipsDescendants = false
widgets.UIListLayout(Group, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
return Group
end,
Update = function(_thisWidget: Group) end,
Discard = function(thisWidget: Group)
thisWidget.Instance:Destroy()
end,
ChildAdded = function(thisWidget: Group, _thisChild: Widget)
return thisWidget.Instance
end,
} :: WidgetClass)
end,
Text = function(ImGui: Internal, widgets: WidgetUtility)
--stylua: ignore
ImGui.WidgetConstructor("Text", {
hasState = false,
hasChildren = false,
Args = {
["Text"] = 1,
["Wrapped"] = 2,
["Color"] = 3,
["RichText"] = 4,
},
Events = {
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Text)
local Text: TextLabel = Instance.new("TextLabel")
Text.Name = "ImGui_Text"
Text.Size = UDim2.fromOffset(0, 0)
Text.BackgroundTransparency = 1
Text.BorderSizePixel = 0
Text.LayoutOrder = thisWidget.ZIndex
Text.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(Text)
widgets.UIPadding(Text, Vector2.new(0, 2))
return Text
end,
Update = function(thisWidget: Text)
local Text = thisWidget.Instance :: TextLabel
if thisWidget.arguments.Text == nil then
error("ImGui.Text Text Argument is required", 5)
end
if thisWidget.arguments.Wrapped ~= nil then
Text.TextWrapped = thisWidget.arguments.Wrapped
else
Text.TextWrapped = ImGui._config.TextWrapped
end
if thisWidget.arguments.Color then
Text.TextColor3 = thisWidget.arguments.Color
else
Text.TextColor3 = ImGui._config.TextColor
end
if thisWidget.arguments.RichText ~= nil then
Text.RichText = thisWidget.arguments.RichText
else
Text.RichText = ImGui._config.RichText
end
Text.Text = thisWidget.arguments.Text
end,
Discard = function(thisWidget: Text)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
--stylua: ignore
ImGui.WidgetConstructor("SeparatorText", {
hasState = false,
hasChildren = false,
Args = {
["Text"] = 1,
},
Events = {
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: SeparatorText)
local SeparatorText = Instance.new("Frame")
SeparatorText.Name = "ImGui_SeparatorText"
SeparatorText.Size = UDim2.fromScale(1, 0)
SeparatorText.BackgroundTransparency = 1
SeparatorText.BorderSizePixel = 0
SeparatorText.AutomaticSize = Enum.AutomaticSize.Y
SeparatorText.LayoutOrder = thisWidget.ZIndex
SeparatorText.ClipsDescendants = true
widgets.UIPadding(SeparatorText, Vector2.new(0, ImGui._config.SeparatorTextPadding.Y))
widgets.UIListLayout(SeparatorText, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemSpacing.X))
SeparatorText.UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = SeparatorText
local Left: Frame = Instance.new("Frame")
Left.Name = "Left"
Left.AnchorPoint = Vector2.new(1, 0.5)
Left.BackgroundColor3 = ImGui._config.SeparatorColor
Left.BackgroundTransparency = ImGui._config.SeparatorTransparency
Left.BorderSizePixel = 0
Left.Size = UDim2.fromOffset(ImGui._config.SeparatorTextPadding.X - ImGui._config.ItemSpacing.X, ImGui._config.SeparatorTextBorderSize)
Left.Parent = SeparatorText
local Right: Frame = Instance.new("Frame")
Right.Name = "Right"
Right.AnchorPoint = Vector2.new(1, 0.5)
Right.BackgroundColor3 = ImGui._config.SeparatorColor
Right.BackgroundTransparency = ImGui._config.SeparatorTransparency
Right.BorderSizePixel = 0
Right.Size = UDim2.new(1, 0, 0, ImGui._config.SeparatorTextBorderSize)
Right.LayoutOrder = 2
Right.Parent = SeparatorText
return SeparatorText
end,
Update = function(thisWidget: SeparatorText)
local SeparatorText = thisWidget.Instance :: Frame
local TextLabel: TextLabel = SeparatorText.TextLabel
if thisWidget.arguments.Text == nil then
error("ImGui.Text Text Argument is required", 5)
end
TextLabel.Text = thisWidget.arguments.Text
end,
Discard = function(thisWidget: SeparatorText)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass)
end,
Button = function(ImGui: Internal, widgets: WidgetUtility)
local abstractButton = {
hasState = false,
hasChildren = false,
Args = {
["Text"] = 1,
["Size"] = 2,
},
Events = {
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
return thisWidget.Instance
end),
["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Button)
local Button: TextButton = Instance.new("TextButton")
Button.Size = UDim2.fromOffset(0, 0)
Button.BackgroundColor3 = ImGui._config.ButtonColor
Button.BackgroundTransparency = ImGui._config.ButtonTransparency
Button.AutoButtonColor = false
Button.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(Button)
Button.TextXAlignment = Enum.TextXAlignment.Center
widgets.applyFrameStyle(Button)
widgets.applyInteractionHighlights("Background", Button, Button, {
Color = ImGui._config.ButtonColor,
Transparency = ImGui._config.ButtonTransparency,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonActiveColor,
ActiveTransparency = ImGui._config.ButtonActiveTransparency,
})
Button.ZIndex = thisWidget.ZIndex
Button.LayoutOrder = thisWidget.ZIndex
return Button
end,
Update = function(thisWidget: Button)
local Button = thisWidget.Instance :: TextButton
Button.Text = thisWidget.arguments.Text or "Button"
Button.Size = thisWidget.arguments.Size or UDim2.fromOffset(0, 0)
end,
Discard = function(thisWidget: Button)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass
widgets.abstractButton = abstractButton
ImGui.WidgetConstructor("Button", widgets.extend(abstractButton, {
Generate = function(thisWidget: Button)
local Button: TextButton = abstractButton.Generate(thisWidget)
Button.Name = "ImGui_Button"
return Button
end,
} :: WidgetClass)
)
ImGui.WidgetConstructor("SmallButton", widgets.extend(abstractButton, {
Generate = function(thisWidget: Button)
local SmallButton = abstractButton.Generate(thisWidget) :: TextButton
SmallButton.Name = "ImGui_SmallButton"
local uiPadding: UIPadding = SmallButton.UIPadding
uiPadding.PaddingLeft = UDim.new(0, 2)
uiPadding.PaddingRight = UDim.new(0, 2)
uiPadding.PaddingTop = UDim.new(0, 0)
uiPadding.PaddingBottom = UDim.new(0, 0)
return SmallButton
end,
} :: WidgetClass)
)
end,
Checkbox = function(ImGui: Internal, widgets: WidgetUtility)
ImGui.WidgetConstructor("Checkbox", {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
},
Events = {
["checked"] = {
["Init"] = function(_thisWidget: Checkbox) end,
["Get"] = function(thisWidget: Checkbox): boolean
return thisWidget.lastCheckedTick == ImGui._cycleTick
end,
},
["unchecked"] = {
["Init"] = function(_thisWidget: Checkbox) end,
["Get"] = function(thisWidget: Checkbox): boolean
return thisWidget.lastUncheckedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Checkbox)
local Checkbox: TextButton = Instance.new("TextButton")
Checkbox.Name = "ImGui_Checkbox"
Checkbox.AutomaticSize = Enum.AutomaticSize.XY
Checkbox.Size = UDim2.fromOffset(0, 0)
Checkbox.BackgroundTransparency = 1
Checkbox.BorderSizePixel = 0
Checkbox.Text = ""
Checkbox.AutoButtonColor = false
Checkbox.ZIndex = thisWidget.ZIndex
Checkbox.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(Checkbox, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local checkboxSize: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local Box: Frame = Instance.new("Frame")
Box.Name = "Box"
Box.Size = UDim2.fromOffset(checkboxSize, checkboxSize)
Box.BackgroundColor3 = ImGui._config.FrameBgColor
Box.BackgroundTransparency = ImGui._config.FrameBgTransparency
widgets.applyFrameStyle(Box, true)
widgets.UIPadding(Box, Vector2.new(math.floor(checkboxSize / 10), math.floor(checkboxSize / 10)))
widgets.applyInteractionHighlights("Background", Checkbox, Box, {
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
})
Box.Parent = Checkbox
local Checkmark: ImageLabel = Instance.new("ImageLabel")
Checkmark.Name = "Checkmark"
Checkmark.Size = UDim2.fromScale(1, 1)
Checkmark.BackgroundTransparency = 1
Checkmark.ImageColor3 = ImGui._config.CheckMarkColor
Checkmark.ImageTransparency = ImGui._config.CheckMarkTransparency
Checkmark.ScaleType = Enum.ScaleType.Fit
Checkmark.Parent = Box
widgets.applyButtonClick(Checkbox, function()
local wasChecked: boolean = thisWidget.state.isChecked.value
thisWidget.state.isChecked:set(not wasChecked)
end)
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Checkbox
return Checkbox
end,
Update = function(thisWidget: Checkbox)
local Checkbox = thisWidget.Instance :: TextButton
Checkbox.TextLabel.Text = thisWidget.arguments.Text or "Checkbox"
end,
Discard = function(thisWidget: Checkbox)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: Checkbox)
if thisWidget.state.isChecked == nil then
thisWidget.state.isChecked = ImGui._widgetState(thisWidget, "checked", false)
end
end,
UpdateState = function(thisWidget: Checkbox)
local Checkbox = thisWidget.Instance :: TextButton
local Box = Checkbox.Box :: Frame
local Checkmark: ImageLabel = Box.Checkmark
if thisWidget.state.isChecked.value then
Checkmark.Image = widgets.ICONS.CHECK_MARK
thisWidget.lastCheckedTick = ImGui._cycleTick + 1
else
Checkmark.Image = ""
thisWidget.lastUncheckedTick = ImGui._cycleTick + 1
end
end,
} :: WidgetClass)
end,
RadioButton = function(ImGui: Internal, widgets: WidgetUtility)
--stylua: ignore
ImGui.WidgetConstructor("RadioButton", {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Index"] = 2,
},
Events = {
["selected"] = {
["Init"] = function(_thisWidget: RadioButton) end,
["Get"] = function(thisWidget: RadioButton)
return thisWidget.lastSelectedTick == ImGui._cycleTick
end,
},
["unselected"] = {
["Init"] = function(_thisWidget: RadioButton) end,
["Get"] = function(thisWidget: RadioButton)
return thisWidget.lastUnselectedTick == ImGui._cycleTick
end,
},
["active"] = {
["Init"] = function(_thisWidget: RadioButton) end,
["Get"] = function(thisWidget: RadioButton)
return thisWidget.state.index.value == thisWidget.arguments.Index
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: RadioButton)
local RadioButton: TextButton = Instance.new("TextButton")
RadioButton.Name = "ImGui_RadioButton"
RadioButton.AutomaticSize = Enum.AutomaticSize.XY
RadioButton.Size = UDim2.fromOffset(0, 0)
RadioButton.BackgroundTransparency = 1
RadioButton.BorderSizePixel = 0
RadioButton.Text = ""
RadioButton.LayoutOrder = thisWidget.ZIndex
RadioButton.AutoButtonColor = false
RadioButton.ZIndex = thisWidget.ZIndex
RadioButton.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(RadioButton, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local buttonSize: number = ImGui._config.TextSize + 2 * (ImGui._config.FramePadding.Y - 1)
local Button: Frame = Instance.new("Frame")
Button.Name = "Button"
Button.Size = UDim2.fromOffset(buttonSize, buttonSize)
Button.Parent = RadioButton
Button.BackgroundColor3 = ImGui._config.FrameBgColor
Button.BackgroundTransparency = ImGui._config.FrameBgTransparency
widgets.UICorner(Button)
widgets.UIPadding(Button, Vector2.new(math.max(1, math.floor(buttonSize / 5)), math.max(1, math.floor(buttonSize / 5))))
local Circle: Frame = Instance.new("Frame")
Circle.Name = "Circle"
Circle.Size = UDim2.fromScale(1, 1)
Circle.Parent = Button
Circle.BackgroundColor3 = ImGui._config.CheckMarkColor
Circle.BackgroundTransparency = ImGui._config.CheckMarkTransparency
widgets.UICorner(Circle)
widgets.applyInteractionHighlights("Background", RadioButton, Button, {
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
})
widgets.applyButtonClick(RadioButton, function()
thisWidget.state.index:set(thisWidget.arguments.Index)
end)
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = RadioButton
return RadioButton
end,
Update = function(thisWidget: RadioButton)
local RadioButton = thisWidget.Instance :: TextButton
local TextLabel: TextLabel = RadioButton.TextLabel
TextLabel.Text = thisWidget.arguments.Text or "Radio Button"
if thisWidget.state then
ImGui._widgets[thisWidget.type].UpdateState(thisWidget)
end
end,
Discard = function(thisWidget: RadioButton)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: RadioButton)
if thisWidget.state.index == nil then
thisWidget.state.index = ImGui._widgetState(thisWidget, "index", thisWidget.arguments.Index)
end
end,
UpdateState = function(thisWidget: RadioButton)
local RadioButton = thisWidget.Instance :: TextButton
local Button = RadioButton.Button :: Frame
local Circle: Frame = Button.Circle
if thisWidget.state.index.value == thisWidget.arguments.Index then
-- only need to hide the circle
Circle.BackgroundTransparency = ImGui._config.CheckMarkTransparency
thisWidget.lastSelectedTick = ImGui._cycleTick + 1
else
Circle.BackgroundTransparency = 1
thisWidget.lastUnselectedTick = ImGui._cycleTick + 1
end
end,
} :: WidgetClass)
end,
Image = function(ImGui: Internal, widgets: WidgetUtility)
local abstractImage = {
hasState = false,
hasChildren = false,
Args = {
["Image"] = 1,
["Size"] = 2,
["Rect"] = 3,
["ScaleType"] = 4,
["ResampleMode"] = 5,
["TileSize"] = 6,
["SliceCenter"] = 7,
["SliceScale"] = 8,
},
Discard = function(thisWidget: Image)
thisWidget.Instance:Destroy()
end,
} :: WidgetClass
ImGui.WidgetConstructor("Image", widgets.extend(abstractImage, {
Events = {
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Image)
local Image: ImageLabel = Instance.new("ImageLabel")
Image.Name = "ImGui_Image"
Image.BackgroundTransparency = 1
Image.BorderSizePixel = 0
Image.ImageColor3 = ImGui._config.ImageColor
Image.ImageTransparency = ImGui._config.ImageTransparency
Image.LayoutOrder = thisWidget.ZIndex
widgets.applyFrameStyle(Image, true)
return Image
end,
Update = function(thisWidget: Image)
local Image = thisWidget.Instance :: ImageLabel
Image.Image = thisWidget.arguments.Image or widgets.ICONS.UNKNOWN_TEXTURE
Image.Size = thisWidget.arguments.Size
if thisWidget.arguments.ScaleType then
Image.ScaleType = thisWidget.arguments.ScaleType
if thisWidget.arguments.ScaleType == Enum.ScaleType.Tile and thisWidget.arguments.TileSize then
Image.TileSize = thisWidget.arguments.TileSize
elseif thisWidget.arguments.ScaleType == Enum.ScaleType.Slice then
if thisWidget.arguments.SliceCenter then
Image.SliceCenter = thisWidget.arguments.SliceCenter
end
if thisWidget.arguments.SliceScale then
Image.SliceScale = thisWidget.arguments.SliceScale
end
end
end
if thisWidget.arguments.Rect then
Image.ImageRectOffset = thisWidget.arguments.Rect.Min
Image.ImageRectSize = Vector2.new(thisWidget.arguments.Rect.Width, thisWidget.arguments.Rect.Height)
end
if thisWidget.arguments.ResampleMode then
Image.ResampleMode = thisWidget.arguments.ResampleMode
end
end,
} :: WidgetClass)
)
ImGui.WidgetConstructor("ImageButton", widgets.extend(abstractImage, {
Events = {
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
return thisWidget.Instance
end),
["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: ImageButton)
local Button: ImageButton = Instance.new("ImageButton")
Button.Name = "ImGui_ImageButton"
Button.AutomaticSize = Enum.AutomaticSize.XY
Button.BackgroundColor3 = ImGui._config.FrameBgColor
Button.BackgroundTransparency = ImGui._config.FrameBgTransparency
Button.BorderSizePixel = 0
Button.Image = ""
Button.ImageTransparency = 1
Button.LayoutOrder = thisWidget.ZIndex
Button.AutoButtonColor = false
widgets.applyFrameStyle(Button, true)
widgets.UIPadding(Button, Vector2.new(ImGui._config.ImageBorderSize, ImGui._config.ImageBorderSize))
local Image: ImageLabel = Instance.new("ImageLabel")
Image.Name = "ImageLabel"
Image.BackgroundTransparency = 1
Image.BorderSizePixel = 0
Image.ImageColor3 = ImGui._config.ImageColor
Image.ImageTransparency = ImGui._config.ImageTransparency
Image.Parent = Button
widgets.applyInteractionHighlights("Background", Button, Button, {
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
})
return Button
end,
Update = function(thisWidget: ImageButton)
local Button = thisWidget.Instance :: TextButton
local Image: ImageLabel = Button.ImageLabel
Image.Image = thisWidget.arguments.Image or widgets.ICONS.UNKNOWN_TEXTURE
Image.Size = thisWidget.arguments.Size
if thisWidget.arguments.ScaleType then
Image.ScaleType = thisWidget.arguments.ScaleType
if thisWidget.arguments.ScaleType == Enum.ScaleType.Tile and thisWidget.arguments.TileSize then
Image.TileSize = thisWidget.arguments.TileSize
elseif thisWidget.arguments.ScaleType == Enum.ScaleType.Slice then
if thisWidget.arguments.SliceCenter then
Image.SliceCenter = thisWidget.arguments.SliceCenter
end
if thisWidget.arguments.SliceScale then
Image.SliceScale = thisWidget.arguments.SliceScale
end
end
end
if thisWidget.arguments.Rect then
Image.ImageRectOffset = thisWidget.arguments.Rect.Min
Image.ImageRectSize = Vector2.new(thisWidget.arguments.Rect.Width, thisWidget.arguments.Rect.Height)
end
if thisWidget.arguments.ResampleMode then
Image.ResampleMode = thisWidget.arguments.ResampleMode
end
end,
} :: WidgetClass)
)
end,
Tree = function(ImGui: Internal, widgets: WidgetUtility)
local abstractTree = {
hasState = true,
hasChildren = true,
Events = {
["collapsed"] = {
["Init"] = function(_thisWidget: CollapsingHeader) end,
["Get"] = function(thisWidget: CollapsingHeader)
return thisWidget.lastCollapsedTick == ImGui._cycleTick
end,
},
["uncollapsed"] = {
["Init"] = function(_thisWidget: CollapsingHeader) end,
["Get"] = function(thisWidget: CollapsingHeader)
return thisWidget.lastUncollapsedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget)
return thisWidget.Instance
end),
},
Discard = function(thisWidget: CollapsingHeader)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
ChildAdded = function(thisWidget: CollapsingHeader, _thisChild: Widget)
local ChildContainer: Frame = thisWidget.ChildContainer :: Frame
ChildContainer.Visible = thisWidget.state.isUncollapsed.value
return ChildContainer
end,
UpdateState = function(thisWidget: CollapsingHeader)
local isUncollapsed: boolean = thisWidget.state.isUncollapsed.value
local Tree = thisWidget.Instance :: Frame
local ChildContainer = thisWidget.ChildContainer :: Frame
local Header = Tree.Header :: Frame
local Button = Header.Button :: TextButton
local Arrow: ImageLabel = Button.Arrow
Arrow.Image = (isUncollapsed and widgets.ICONS.DOWN_POINTING_TRIANGLE or widgets.ICONS.RIGHT_POINTING_TRIANGLE)
if isUncollapsed then
thisWidget.lastUncollapsedTick = ImGui._cycleTick + 1
else
thisWidget.lastCollapsedTick = ImGui._cycleTick + 1
end
ChildContainer.Visible = isUncollapsed
end,
GenerateState = function(thisWidget: CollapsingHeader)
if thisWidget.state.isUncollapsed == nil then
thisWidget.state.isUncollapsed = ImGui._widgetState(thisWidget, "isUncollapsed", false)
end
end,
} :: WidgetClass
--stylua: ignore
ImGui.WidgetConstructor(
"Tree",
widgets.extend(abstractTree, {
Args = {
["Text"] = 1,
["SpanAvailWidth"] = 2,
["NoIndent"] = 3,
},
Generate = function(thisWidget: Tree)
local Tree: Frame = Instance.new("Frame")
Tree.Name = "ImGui_Tree"
Tree.Size = UDim2.new(ImGui._config.ItemWidth, UDim.new(0, 0))
Tree.AutomaticSize = Enum.AutomaticSize.Y
Tree.BackgroundTransparency = 1
Tree.BorderSizePixel = 0
Tree.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(Tree, Enum.FillDirection.Vertical, UDim.new(0, 0))
local ChildContainer: Frame = Instance.new("Frame")
ChildContainer.Name = "TreeContainer"
ChildContainer.Size = UDim2.fromScale(1, 0)
ChildContainer.AutomaticSize = Enum.AutomaticSize.Y
ChildContainer.BackgroundTransparency = 1
ChildContainer.BorderSizePixel = 0
ChildContainer.LayoutOrder = 1
ChildContainer.Visible = false
-- ChildContainer.ClipsDescendants = true
widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
local ChildContainerPadding: UIPadding = widgets.UIPadding(ChildContainer, Vector2.zero)
ChildContainerPadding.PaddingTop = UDim.new(0, ImGui._config.ItemSpacing.Y)
ChildContainer.Parent = Tree
local Header: Frame = Instance.new("Frame")
Header.Name = "Header"
Header.Size = UDim2.fromScale(1, 0)
Header.AutomaticSize = Enum.AutomaticSize.Y
Header.BackgroundTransparency = 1
Header.BorderSizePixel = 0
Header.Parent = Tree
local Button: TextButton = Instance.new("TextButton")
Button.Name = "Button"
Button.BackgroundTransparency = 1
Button.BorderSizePixel = 0
Button.Text = ""
Button.AutoButtonColor = false
widgets.applyInteractionHighlights("Background", Button, Header, {
Color = Color3.fromRGB(0, 0, 0),
Transparency = 1,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderActiveColor,
ActiveTransparency = ImGui._config.HeaderActiveTransparency,
})
local ButtonPadding: UIPadding = widgets.UIPadding(Button, Vector2.zero)
ButtonPadding.PaddingLeft = UDim.new(0, ImGui._config.FramePadding.X)
local ButtonUIListLayout: UIListLayout = widgets.UIListLayout(Button, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.FramePadding.X))
ButtonUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
Button.Parent = Header
local Arrow: ImageLabel = Instance.new("ImageLabel")
Arrow.Name = "Arrow"
Arrow.Size = UDim2.fromOffset(ImGui._config.TextSize, math.floor(ImGui._config.TextSize * 0.7))
Arrow.BackgroundTransparency = 1
Arrow.BorderSizePixel = 0
Arrow.ImageColor3 = ImGui._config.TextColor
Arrow.ImageTransparency = ImGui._config.TextTransparency
Arrow.ScaleType = Enum.ScaleType.Fit
Arrow.Parent = Button
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.Size = UDim2.fromOffset(0, 0)
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
local TextPadding: UIPadding = widgets.UIPadding(TextLabel, Vector2.zero)
TextPadding.PaddingRight = UDim.new(0, 21)
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Button
widgets.applyButtonClick(Button, function()
thisWidget.state.isUncollapsed:set(not thisWidget.state.isUncollapsed.value)
end)
thisWidget.ChildContainer = ChildContainer
return Tree
end,
Update = function(thisWidget: Tree)
local Tree = thisWidget.Instance :: Frame
local ChildContainer = thisWidget.ChildContainer :: Frame
local Header = Tree.Header :: Frame
local Button = Header.Button :: TextButton
local TextLabel: TextLabel = Button.TextLabel
local Padding: UIPadding = ChildContainer.UIPadding
TextLabel.Text = thisWidget.arguments.Text or "Tree"
if thisWidget.arguments.SpanAvailWidth then
Button.AutomaticSize = Enum.AutomaticSize.Y
Button.Size = UDim2.fromScale(1, 0)
else
Button.AutomaticSize = Enum.AutomaticSize.XY
Button.Size = UDim2.fromScale(0, 0)
end
if thisWidget.arguments.NoIndent then
Padding.PaddingLeft = UDim.new(0, 0)
else
Padding.PaddingLeft = UDim.new(0, ImGui._config.IndentSpacing)
end
end,
})
)
--stylua: ignore
ImGui.WidgetConstructor(
"CollapsingHeader",
widgets.extend(abstractTree, {
Args = {
["Text"] = 1,
},
Generate = function(thisWidget: CollapsingHeader)
local CollapsingHeader: Frame = Instance.new("Frame")
CollapsingHeader.Name = "ImGui_CollapsingHeader"
CollapsingHeader.Size = UDim2.new(ImGui._config.ItemWidth, UDim.new(0, 0))
CollapsingHeader.AutomaticSize = Enum.AutomaticSize.Y
CollapsingHeader.BackgroundTransparency = 1
CollapsingHeader.BorderSizePixel = 0
CollapsingHeader.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(CollapsingHeader, Enum.FillDirection.Vertical, UDim.new(0, 0))
local ChildContainer: Frame = Instance.new("Frame")
ChildContainer.Name = "CollapsingHeaderContainer"
ChildContainer.Size = UDim2.fromScale(1, 0)
ChildContainer.AutomaticSize = Enum.AutomaticSize.Y
ChildContainer.BackgroundTransparency = 1
ChildContainer.BorderSizePixel = 0
ChildContainer.LayoutOrder = 1
ChildContainer.Visible = false
-- ChildContainer.ClipsDescendants = true
widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
local ChildContainerPadding: UIPadding = widgets.UIPadding(ChildContainer, Vector2.zero)
ChildContainerPadding.PaddingTop = UDim.new(0, ImGui._config.ItemSpacing.Y)
ChildContainer.Parent = CollapsingHeader
local Header: Frame = Instance.new("Frame")
Header.Name = "Header"
Header.Size = UDim2.fromScale(1, 0)
Header.AutomaticSize = Enum.AutomaticSize.Y
Header.BackgroundTransparency = 1
Header.BorderSizePixel = 0
Header.Parent = CollapsingHeader
local Button = Instance.new("TextButton")
Button.Name = "Button"
Button.Size = UDim2.new(1, 0, 0, 0)
Button.AutomaticSize = Enum.AutomaticSize.Y
Button.BackgroundColor3 = ImGui._config.HeaderColor
Button.BackgroundTransparency = ImGui._config.HeaderTransparency
Button.BorderSizePixel = 0
Button.Text = ""
Button.AutoButtonColor = false
Button.ClipsDescendants = true
widgets.UIPadding(Button, ImGui._config.FramePadding) -- we add a custom padding because it extends on both sides
widgets.applyFrameStyle(Button, true)
local ButtonUIListLayout: UIListLayout = widgets.UIListLayout(Button, Enum.FillDirection.Horizontal, UDim.new(0, 2 * ImGui._config.FramePadding.X))
ButtonUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
widgets.applyInteractionHighlights("Background", Button, Button, {
Color = ImGui._config.HeaderColor,
Transparency = ImGui._config.HeaderTransparency,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderActiveColor,
ActiveTransparency = ImGui._config.HeaderActiveTransparency,
})
Button.Parent = Header
local Arrow: ImageLabel = Instance.new("ImageLabel")
Arrow.Name = "Arrow"
Arrow.Size = UDim2.fromOffset(ImGui._config.TextSize, math.ceil(ImGui._config.TextSize * 0.8))
Arrow.AutomaticSize = Enum.AutomaticSize.Y
Arrow.BackgroundTransparency = 1
Arrow.BorderSizePixel = 0
Arrow.ImageColor3 = ImGui._config.TextColor
Arrow.ImageTransparency = ImGui._config.TextTransparency
Arrow.ScaleType = Enum.ScaleType.Fit
Arrow.Parent = Button
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.Size = UDim2.fromOffset(0, 0)
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
local TextPadding: UIPadding = widgets.UIPadding(TextLabel, Vector2.zero)
TextPadding.PaddingRight = UDim.new(0, 21)
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Button
widgets.applyButtonClick(Button, function()
thisWidget.state.isUncollapsed:set(not thisWidget.state.isUncollapsed.value)
end)
thisWidget.ChildContainer = ChildContainer
return CollapsingHeader
end,
Update = function(thisWidget: CollapsingHeader)
local Tree = thisWidget.Instance :: Frame
local Header = Tree.Header :: Frame
local Button = Header.Button :: TextButton
local TextLabel: TextLabel = Button.TextLabel
TextLabel.Text = thisWidget.arguments.Text or "Collapsing Header"
end,
})
)
end,
Input = function(ImGui: Internal, widgets: WidgetUtility)
local numberChanged = {
["Init"] = function(_thisWidget: Widget) end,
["Get"] = function(thisWidget: Input)
return thisWidget.lastNumberChangedTick == ImGui._cycleTick
end,
}
local function getValueByIndex(value: T, index: number, arguments: any): number
local t: string = typeof(value)
local v = value :: any
if t == "number" then
return v
elseif t == "Vector2" then
if index == 1 then
return v.X
elseif index == 2 then
return v.Y
end
elseif t == "Vector3" then
if index == 1 then
return v.X
elseif index == 2 then
return v.Y
elseif index == 3 then
return v.Z
end
elseif t == "UDim" then
if index == 1 then
return v.Scale
elseif index == 2 then
return v.Offset
end
elseif t == "UDim2" then
if index == 1 then
return v.X.Scale
elseif index == 2 then
return v.X.Offset
elseif index == 3 then
return v.Y.Scale
elseif index == 4 then
return v.Y.Offset
end
elseif t == "Color3" then
local color: { number } = arguments.UseHSV and { v:ToHSV() } or { v.R, v.G, v.B }
if index == 1 then
return color[1]
elseif index == 2 then
return color[2]
elseif index == 3 then
return color[3]
end
elseif t == "Rect" then
if index == 1 then
return v.Min.X
elseif index == 2 then
return v.Min.Y
elseif index == 3 then
return v.Max.X
elseif index == 4 then
return v.Max.Y
end
elseif t == "table" then
return v[index]
end
error(`Incorrect datatype or value: {value} {typeof(value)} {index}`)
end
local function updateValueByIndex(value: T, index: number, newValue: number, arguments: Arguments): T
if typeof(value) == "number" then
return newValue :: any
elseif typeof(value) == "Vector2" then
if index == 1 then
return Vector2.new(newValue, value.Y) :: any
elseif index == 2 then
return Vector2.new(value.X, newValue) :: any
end
elseif typeof(value) == "Vector3" then
if index == 1 then
return Vector3.new(newValue, value.Y, value.Z) :: any
elseif index == 2 then
return Vector3.new(value.X, newValue, value.Z) :: any
elseif index == 3 then
return Vector3.new(value.X, value.Y, newValue) :: any
end
elseif typeof(value) == "UDim" then
if index == 1 then
return UDim.new(newValue, value.Offset) :: any
elseif index == 2 then
return UDim.new(value.Scale, newValue) :: any
end
elseif typeof(value) == "UDim2" then
if index == 1 then
return UDim2.new(UDim.new(newValue, value.X.Offset), value.Y) :: any
elseif index == 2 then
return UDim2.new(UDim.new(value.X.Scale, newValue), value.Y) :: any
elseif index == 3 then
return UDim2.new(value.X, UDim.new(newValue, value.Y.Offset)) :: any
elseif index == 4 then
return UDim2.new(value.X, UDim.new(value.Y.Scale, newValue)) :: any
end
elseif typeof(value) == "Rect" then
if index == 1 then
return Rect.new(Vector2.new(newValue, value.Min.Y), value.Max) :: any
elseif index == 2 then
return Rect.new(Vector2.new(value.Min.X, newValue), value.Max) :: any
elseif index == 3 then
return Rect.new(value.Min, Vector2.new(newValue, value.Max.Y)) :: any
elseif index == 4 then
return Rect.new(value.Min, Vector2.new(value.Max.X, newValue)) :: any
end
elseif typeof(value) == "Color3" then
if arguments.UseHSV then
local h: number, s: number, v: number = value:ToHSV()
if index == 1 then
return Color3.fromHSV(newValue, s, v) :: any
elseif index == 2 then
return Color3.fromHSV(h, newValue, v) :: any
elseif index == 3 then
return Color3.fromHSV(h, s, newValue) :: any
end
end
if index == 1 then
return Color3.new(newValue, value.G, value.B) :: any
elseif index == 2 then
return Color3.new(value.R, newValue, value.B) :: any
elseif index == 3 then
return Color3.new(value.R, value.G, newValue) :: any
end
end
error(`Incorrect datatype or value {value} {typeof(value)} {index}`)
end
local defaultIncrements: { [InputDataTypes]: { number } } = {
Num = { 1 },
Vector2 = { 1, 1 },
Vector3 = { 1, 1, 1 },
UDim = { 0.01, 1 },
UDim2 = { 0.01, 1, 0.01, 1 },
Color3 = { 1, 1, 1 },
Color4 = { 1, 1, 1, 1 },
Rect = { 1, 1, 1, 1 },
}
local defaultMin: { [InputDataTypes]: { number } } = {
Num = { 0 },
Vector2 = { 0, 0 },
Vector3 = { 0, 0, 0 },
UDim = { 0, 0 },
UDim2 = { 0, 0, 0, 0 },
Rect = { 0, 0, 0, 0 },
}
local defaultMax: { [InputDataTypes]: { number } } = {
Num = { 100 },
Vector2 = { 100, 100 },
Vector3 = { 100, 100, 100 },
UDim = { 1, 960 },
UDim2 = { 1, 960, 1, 960 },
Rect = { 960, 960, 960, 960 },
}
local defaultPrefx: { [InputDataTypes]: { string } } = {
Num = { "" },
Vector2 = { "X: ", "Y: " },
Vector3 = { "X: ", "Y: ", "Z: " },
UDim = { "", "" },
UDim2 = { "", "", "", "" },
Color3_RGB = { "R: ", "G: ", "B: " },
Color3_HSV = { "H: ", "S: ", "V: " },
Color4_RGB = { "R: ", "G: ", "B: ", "T: " },
Color4_HSV = { "H: ", "S: ", "V: ", "T: " },
Rect = { "X: ", "Y: ", "X: ", "Y: " },
}
local defaultSigFigs: { [InputDataTypes]: { number } } = {
Num = { 0 },
Vector2 = { 0, 0 },
Vector3 = { 0, 0, 0 },
UDim = { 3, 0 },
UDim2 = { 3, 0, 3, 0 },
Color3 = { 0, 0, 0 },
Color4 = { 0, 0, 0, 0 },
Rect = { 0, 0, 0, 0 },
}
--[[
Input
]]
local generateInputScalar: (dataType: InputDataTypes, components: number, defaultValue: any) -> WidgetClass
do
local function generateButtons(thisWidget: Input, parent: GuiObject, rightPadding: number, textHeight: number)
rightPadding += 2 * ImGui._config.ItemInnerSpacing.X + 2 * textHeight
local SubButton = widgets.abstractButton.Generate(thisWidget) :: TextButton
SubButton.Name = "SubButton"
SubButton.ZIndex = 5
SubButton.LayoutOrder = 5
SubButton.TextXAlignment = Enum.TextXAlignment.Center
SubButton.Text = "-"
SubButton.Size = UDim2.fromOffset(ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y, ImGui._config.TextSize)
SubButton.Parent = parent
widgets.applyButtonClick(SubButton, function()
local isCtrlHeld: boolean = widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)
local changeValue: number = (thisWidget.arguments.Increment and getValueByIndex(thisWidget.arguments.Increment, 1, thisWidget.arguments :: Argument) or 1) * (isCtrlHeld and 100 or 1)
local newValue: number = thisWidget.state.number.value - changeValue
if thisWidget.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(thisWidget.arguments.Min, 1, thisWidget.arguments :: Argument))
end
if thisWidget.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, 1, thisWidget.arguments :: Argument))
end
thisWidget.state.number:set(newValue)
thisWidget.lastNumberChangedTick = ImGui._cycleTick + 1
end)
local AddButton = widgets.abstractButton.Generate(thisWidget) :: TextButton
AddButton.Name = "AddButton"
AddButton.ZIndex = 6
AddButton.LayoutOrder = 6
AddButton.TextXAlignment = Enum.TextXAlignment.Center
AddButton.Text = "+"
AddButton.Size = UDim2.fromOffset(ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y, ImGui._config.TextSize)
AddButton.Parent = parent
widgets.applyButtonClick(AddButton, function()
local isCtrlHeld: boolean = widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)
local changeValue: number = (thisWidget.arguments.Increment and getValueByIndex(thisWidget.arguments.Increment, 1, thisWidget.arguments :: Argument) or 1) * (isCtrlHeld and 100 or 1)
local newValue: number = thisWidget.state.number.value + changeValue
if thisWidget.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(thisWidget.arguments.Min, 1, thisWidget.arguments :: Argument))
end
if thisWidget.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, 1, thisWidget.arguments :: Argument))
end
thisWidget.state.number:set(newValue)
thisWidget.lastNumberChangedTick = ImGui._cycleTick + 1
end)
return rightPadding
end
function generateInputScalar(dataType: InputDataTypes, components: number, defaultValue: any)
return {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Increment"] = 2,
["Min"] = 3,
["Max"] = 4,
["Format"] = 5,
},
Events = {
["numberChanged"] = numberChanged,
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Input)
local Input: Frame = Instance.new("Frame")
Input.Name = "ImGui_Input" .. dataType
Input.Size = UDim2.fromScale(1, 0)
Input.BackgroundTransparency = 1
Input.BorderSizePixel = 0
Input.LayoutOrder = thisWidget.ZIndex
Input.AutomaticSize = Enum.AutomaticSize.Y
local UIListLayout: UIListLayout = widgets.UIListLayout(Input, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
-- we add plus and minus buttons if there is only one box. This can be disabled through the argument.
local rightPadding: number = 0
local textHeight: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
if components == 1 then
rightPadding = generateButtons(thisWidget :: any, Input, rightPadding, textHeight)
end
-- we divide the total area evenly between each field. This includes accounting for any additional boxes and the offset.
-- for the final field, we make sure it's flush by calculating the space avaiable for it. This only makes the Vector2 box
-- 4 pixels shorter, all for the sake of flush.
local componentWidth: UDim = UDim.new(ImGui._config.ContentWidth.Scale / components, (ImGui._config.ContentWidth.Offset - (ImGui._config.ItemInnerSpacing.X * (components - 1)) - rightPadding) / components)
local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (ImGui._config.ItemInnerSpacing.X * (components - 1)) + rightPadding)
local lastComponentWidth: UDim = ImGui._config.ContentWidth - totalWidth
-- we handle each component individually since they don't need to interact with each other.
for index = 1, components do
local InputField: TextBox = Instance.new("TextBox")
InputField.Name = "InputField" .. tostring(index)
InputField.LayoutOrder = index
if index == components then
InputField.Size = UDim2.new(lastComponentWidth, ImGui._config.ContentHeight)
else
InputField.Size = UDim2.new(componentWidth, ImGui._config.ContentHeight)
end
InputField.AutomaticSize = Enum.AutomaticSize.Y
InputField.BackgroundColor3 = ImGui._config.FrameBgColor
InputField.BackgroundTransparency = ImGui._config.FrameBgTransparency
InputField.ClearTextOnFocus = false
InputField.TextTruncate = Enum.TextTruncate.AtEnd
InputField.ClipsDescendants = true
widgets.applyFrameStyle(InputField)
widgets.applyTextStyle(InputField)
widgets.UISizeConstraint(InputField, Vector2.xAxis)
InputField.Parent = Input
InputField.FocusLost:Connect(function()
local newValue: number? = tonumber(InputField.Text:match("-?%d*%.?%d*"))
if newValue ~= nil then
if thisWidget.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments))
end
if thisWidget.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments :: any))
end
if thisWidget.arguments.Increment then
newValue = math.round(newValue / getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)) * getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
end
thisWidget.state.number:set(updateValueByIndex(thisWidget.state.number.value, index, newValue, thisWidget.arguments :: any))
thisWidget.lastNumberChangedTick = ImGui._cycleTick + 1
end
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
InputField.Text = string.format(format, getValueByIndex(thisWidget.state.number.value, index, thisWidget.arguments))
thisWidget.state.editingText:set(0)
end)
InputField.Focused:Connect(function()
-- this highlights the entire field
InputField.CursorPosition = #InputField.Text + 1
InputField.SelectionStart = 1
thisWidget.state.editingText:set(index)
end)
end
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 7
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Input
return Input
end,
Update = function(thisWidget: Input)
local Input = thisWidget.Instance :: GuiObject
local TextLabel: TextLabel = Input.TextLabel
TextLabel.Text = thisWidget.arguments.Text or `Input {dataType}`
if components == 1 then
Input.SubButton.Visible = not thisWidget.arguments.NoButtons
Input.AddButton.Visible = not thisWidget.arguments.NoButtons
end
if thisWidget.arguments.Format and typeof(thisWidget.arguments.Format) ~= "table" then
thisWidget.arguments.Format = { thisWidget.arguments.Format }
elseif not thisWidget.arguments.Format then
-- we calculate the format for the s.f. using the max, min and increment arguments.
local format: { string } = {}
for index = 1, components do
local sigfigs: number = defaultSigFigs[dataType][index]
if thisWidget.arguments.Increment then
local value: number = getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Max then
local value: number = getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Min then
local value: number = getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if sigfigs > 0 then
-- we know it's a float.
format[index] = `%.{sigfigs}f`
else
format[index] = "%d"
end
end
thisWidget.arguments.Format = format
thisWidget.arguments.Prefix = defaultPrefx[dataType]
end
end,
Discard = function(thisWidget: Input)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: Input)
if thisWidget.state.number == nil then
thisWidget.state.number = ImGui._widgetState(thisWidget, "number", defaultValue)
end
if thisWidget.state.editingText == nil then
thisWidget.state.editingText = ImGui._widgetState(thisWidget, "editingText", 0)
end
end,
UpdateState = function(thisWidget: Input)
local Input = thisWidget.Instance :: GuiObject
for index = 1, components do
local InputField: TextBox = Input:FindFirstChild("InputField" .. tostring(index))
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
InputField.Text = string.format(format, getValueByIndex(thisWidget.state.number.value, index, thisWidget.arguments))
end
end,
}
end
end
--[[
Drag
]]
local generateDragScalar: (dataType: InputDataTypes, components: number, defaultValue: any) -> WidgetClass
local generateColorDragScalar: (dataType: InputDataTypes, ...any) -> WidgetClass
do
local PreviouseMouseXPosition: number = 0
local AnyActiveDrag: boolean = false
local ActiveDrag: Input? = nil
local ActiveIndex: number = 0
local ActiveDataType: InputDataTypes | "" = ""
local function updateActiveDrag()
local currentMouseX: number = widgets.getMouseLocation().X
local mouseXDelta: number = currentMouseX - PreviouseMouseXPosition
PreviouseMouseXPosition = currentMouseX
if AnyActiveDrag == false then
return
end
if ActiveDrag == nil then
return
end
local state: State = ActiveDrag.state.number
if ActiveDataType == "Color3" or ActiveDataType == "Color4" then
local Drag = ActiveDrag :: any
state = Drag.state.color
if ActiveIndex == 4 then
state = Drag.state.transparency
end
end
local increment: number = ActiveDrag.arguments.Increment and getValueByIndex(ActiveDrag.arguments.Increment, ActiveIndex, ActiveDrag.arguments) or defaultIncrements[ActiveDataType][ActiveIndex]
increment *= (widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightShift)) and 10 or 1
increment *= (widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftAlt) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightAlt)) and 0.1 or 1
-- we increase the speed for Color3 and Color4 since it's too slow because the increment argument needs to be low.
increment *= (ActiveDataType == "Color3" or ActiveDataType == "Color4") and 5 or 1
local value: number = getValueByIndex(state.value, ActiveIndex, ActiveDrag.arguments)
local newValue: number = value + (mouseXDelta * increment)
if ActiveDrag.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(ActiveDrag.arguments.Min, ActiveIndex, ActiveDrag.arguments))
end
if ActiveDrag.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(ActiveDrag.arguments.Max, ActiveIndex, ActiveDrag.arguments))
end
state:set(updateValueByIndex(state.value, ActiveIndex, newValue, ActiveDrag.arguments :: any))
ActiveDrag.lastNumberChangedTick = ImGui._cycleTick + 1
end
local function DragMouseDown(thisWidget: Input, dataTypes: InputDataTypes, index: number, x: number, y: number)
local currentTime: number = widgets.getTime()
local isTimeValid: boolean = currentTime - thisWidget.lastClickedTime < ImGui._config.MouseDoubleClickTime
local isCtrlHeld: boolean = widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)
if (isTimeValid and (Vector2.new(x, y) - thisWidget.lastClickedPosition).Magnitude < ImGui._config.MouseDoubleClickMaxDist) or isCtrlHeld then
thisWidget.state.editingText:set(index)
else
thisWidget.lastClickedTime = currentTime
thisWidget.lastClickedPosition = Vector2.new(x, y)
AnyActiveDrag = true
ActiveDrag = thisWidget
ActiveIndex = index
ActiveDataType = dataTypes
updateActiveDrag()
end
end
widgets.registerEvent("InputChanged", function()
if not ImGui._started then
return
end
updateActiveDrag()
end)
widgets.registerEvent("InputEnded", function(inputObject: InputObject)
if not ImGui._started then
return
end
if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and AnyActiveDrag then
AnyActiveDrag = false
ActiveDrag = nil
ActiveIndex = 0
end
end)
function generateDragScalar(dataType: InputDataTypes, components: number, defaultValue: any)
return {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Increment"] = 2,
["Min"] = 3,
["Max"] = 4,
["Format"] = 5,
},
Events = {
["numberChanged"] = numberChanged,
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Input)
thisWidget.lastClickedTime = -1
thisWidget.lastClickedPosition = Vector2.zero
local Drag: Frame = Instance.new("Frame")
Drag.Name = "ImGui_Drag" .. dataType
Drag.Size = UDim2.fromScale(1, 0)
Drag.BackgroundTransparency = 1
Drag.BorderSizePixel = 0
Drag.LayoutOrder = thisWidget.ZIndex
Drag.AutomaticSize = Enum.AutomaticSize.Y
local UIListLayout: UIListLayout = widgets.UIListLayout(Drag, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
-- we add a color box if it is Color3 or Color4.
local rightPadding: number = 0
local textHeight: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
if dataType == "Color3" or dataType == "Color4" then
rightPadding += ImGui._config.ItemInnerSpacing.X + textHeight
local ColorBox: ImageLabel = Instance.new("ImageLabel")
ColorBox.Name = "ColorBox"
ColorBox.BorderSizePixel = 0
ColorBox.Size = UDim2.fromOffset(textHeight, textHeight)
ColorBox.LayoutOrder = 5
ColorBox.Image = widgets.ICONS.ALPHA_BACKGROUND_TEXTURE
ColorBox.ImageTransparency = 1
widgets.applyFrameStyle(ColorBox, true)
ColorBox.Parent = Drag
end
-- we divide the total area evenly between each field. This includes accounting for any additional boxes and the offset.
-- for the final field, we make sure it's flush by calculating the space avaiable for it. This only makes the Vector2 box
-- 4 pixels shorter, all for the sake of flush.
local componentWidth: UDim = UDim.new(ImGui._config.ContentWidth.Scale / components, (ImGui._config.ContentWidth.Offset - (ImGui._config.ItemInnerSpacing.X * (components - 1)) - rightPadding) / components)
local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (ImGui._config.ItemInnerSpacing.X * (components - 1)) + rightPadding)
local lastComponentWidth: UDim = ImGui._config.ContentWidth - totalWidth
for index = 1, components do
local DragField: TextButton = Instance.new("TextButton")
DragField.Name = "DragField" .. tostring(index)
DragField.LayoutOrder = index
if index == components then
DragField.Size = UDim2.new(lastComponentWidth, ImGui._config.ContentHeight)
else
DragField.Size = UDim2.new(componentWidth, ImGui._config.ContentHeight)
end
DragField.AutomaticSize = Enum.AutomaticSize.Y
DragField.BackgroundColor3 = ImGui._config.FrameBgColor
DragField.BackgroundTransparency = ImGui._config.FrameBgTransparency
DragField.AutoButtonColor = false
DragField.Text = ""
DragField.ClipsDescendants = true
widgets.applyFrameStyle(DragField)
widgets.applyTextStyle(DragField)
widgets.UISizeConstraint(DragField, Vector2.xAxis)
DragField.TextXAlignment = Enum.TextXAlignment.Center
DragField.Parent = Drag
widgets.applyInteractionHighlights("Background", DragField, DragField, {
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
})
local InputField: TextBox = Instance.new("TextBox")
InputField.Name = "InputField"
InputField.Size = UDim2.new(1, 0, 1, 0)
InputField.BackgroundTransparency = 1
InputField.ClearTextOnFocus = false
InputField.TextTruncate = Enum.TextTruncate.AtEnd
InputField.ClipsDescendants = true
InputField.Visible = false
widgets.applyFrameStyle(InputField, true)
widgets.applyTextStyle(InputField)
InputField.Parent = DragField
InputField.FocusLost:Connect(function()
local newValue: number? = tonumber(InputField.Text:match("-?%d*%.?%d*"))
local state: State = thisWidget.state.number
local widget = thisWidget :: any
if dataType == "Color4" and index == 4 then
state = widget.state.transparency
elseif dataType == "Color3" or dataType == "Color4" then
state = widget.state.color
end
if newValue ~= nil then
if dataType == "Color3" or dataType == "Color4" and not widget.arguments.UseFloats then
newValue = newValue / 255
end
if thisWidget.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments))
end
if thisWidget.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments))
end
if thisWidget.arguments.Increment then
newValue = math.round(newValue / getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)) * getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
end
state:set(updateValueByIndex(state.value, index, newValue, thisWidget.arguments :: any))
thisWidget.lastNumberChangedTick = ImGui._cycleTick + 1
end
local value: number = getValueByIndex(state.value, index, thisWidget.arguments)
if dataType == "Color3" or dataType == "Color4" and not widget.arguments.UseFloats then
value = math.round(value * 255)
end
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
InputField.Text = string.format(format, value)
thisWidget.state.editingText:set(0)
InputField:ReleaseFocus(true)
end)
InputField.Focused:Connect(function()
-- this highlights the entire field
InputField.CursorPosition = #InputField.Text + 1
InputField.SelectionStart = 1
thisWidget.state.editingText:set(index)
end)
widgets.applyButtonDown(DragField, function(x: number, y: number)
DragMouseDown(thisWidget :: any, dataType, index, x, y)
end)
end
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 6
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Drag
return Drag
end,
Update = function(thisWidget: Input)
local Input = thisWidget.Instance :: GuiObject
local TextLabel: TextLabel = Input.TextLabel
TextLabel.Text = thisWidget.arguments.Text or `Drag {dataType}`
if thisWidget.arguments.Format and typeof(thisWidget.arguments.Format) ~= "table" then
thisWidget.arguments.Format = { thisWidget.arguments.Format }
elseif not thisWidget.arguments.Format then
-- we calculate the format for the s.f. using the max, min and increment arguments.
local format: { string } = {}
for index = 1, components do
local sigfigs: number = defaultSigFigs[dataType][index]
if thisWidget.arguments.Increment then
local value: number = getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Max then
local value: number = getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Min then
local value: number = getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if sigfigs > 0 then
-- we know it's a float.
format[index] = `%.{sigfigs}f`
else
format[index] = "%d"
end
end
thisWidget.arguments.Format = format
thisWidget.arguments.Prefix = defaultPrefx[dataType]
end
end,
Discard = function(thisWidget: Input)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: Input)
if thisWidget.state.number == nil then
thisWidget.state.number = ImGui._widgetState(thisWidget, "number", defaultValue)
end
if thisWidget.state.editingText == nil then
thisWidget.state.editingText = ImGui._widgetState(thisWidget, "editingText", false)
end
end,
UpdateState = function(thisWidget: Input)
local Drag = thisWidget.Instance :: Frame
local widget = thisWidget :: any
for index = 1, components do
local state: State = thisWidget.state.number
if dataType == "Color3" or dataType == "Color4" then
state = widget.state.color
if index == 4 then
state = widget.state.transparency
end
end
local DragField = Drag:FindFirstChild("DragField" .. tostring(index)) :: TextButton
local InputField: TextBox = DragField.InputField
local value: number = getValueByIndex(state.value, index, thisWidget.arguments)
if (dataType == "Color3" or dataType == "Color4") and not widget.arguments.UseFloats then
value = math.round(value * 255)
end
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
DragField.Text = string.format(format, value)
InputField.Text = tostring(value)
if thisWidget.state.editingText.value == index then
InputField.Visible = true
InputField:CaptureFocus()
DragField.TextTransparency = 1
else
InputField.Visible = false
DragField.TextTransparency = ImGui._config.TextTransparency
end
end
if dataType == "Color3" or dataType == "Color4" then
local ColorBox: ImageLabel = Drag.ColorBox
ColorBox.BackgroundColor3 = widget.state.color.value
if dataType == "Color4" then
ColorBox.ImageTransparency = 1 - widget.state.transparency.value
end
end
end,
}
end
function generateColorDragScalar(dataType: InputDataTypes, ...: any)
local defaultValues: { any } = { ... }
local input: WidgetClass = generateDragScalar(dataType, dataType == "Color4" and 4 or 3, defaultValues[1])
return widgets.extend(input, {
Args = {
["Text"] = 1,
["UseFloats"] = 2,
["UseHSV"] = 3,
["Format"] = 4,
},
Update = function(thisWidget: InputColor4)
local Input = thisWidget.Instance :: GuiObject
local TextLabel: TextLabel = Input.TextLabel
TextLabel.Text = thisWidget.arguments.Text or `Drag {dataType}`
if thisWidget.arguments.Format and typeof(thisWidget.arguments.Format) ~= "table" then
thisWidget.arguments.Format = { thisWidget.arguments.Format }
elseif not thisWidget.arguments.Format then
if thisWidget.arguments.UseFloats then
thisWidget.arguments.Format = { "%.3f" }
else
thisWidget.arguments.Format = { "%d" }
end
thisWidget.arguments.Prefix = defaultPrefx[dataType .. if thisWidget.arguments.UseHSV then "_HSV" else "_RGB"]
end
thisWidget.arguments.Min = { 0, 0, 0, 0 }
thisWidget.arguments.Max = { 1, 1, 1, 1 }
thisWidget.arguments.Increment = { 0.001, 0.001, 0.001, 0.001 }
-- since the state values have changed display, we call an update. The check is because state is not
-- initialised on creation, so it would error otherwise.
if thisWidget.state then
ImGui._widgets[thisWidget.type].UpdateState(thisWidget)
end
end,
GenerateState = function(thisWidget: InputColor4)
if thisWidget.state.color == nil then
thisWidget.state.color = ImGui._widgetState(thisWidget, "color", defaultValues[1])
end
if dataType == "Color4" then
if thisWidget.state.transparency == nil then
thisWidget.state.transparency = ImGui._widgetState(thisWidget, "transparency", defaultValues[2])
end
end
if thisWidget.state.editingText == nil then
thisWidget.state.editingText = ImGui._widgetState(thisWidget, "editingText", false)
end
end,
})
end
end
--[[
Slider
]]
local generateSliderScalar: (dataType: InputDataTypes, components: number, defaultValue: any) -> WidgetClass
local generateEnumSliderScalar: (enum: Enum, item: EnumItem) -> WidgetClass
do
local AnyActiveSlider: boolean = false
local ActiveSlider: Input? = nil
local ActiveIndex: number = 0
local ActiveDataType: InputDataTypes | "" = ""
local function updateActiveSlider()
if AnyActiveSlider == false then
return
end
if ActiveSlider == nil then
return
end
local Slider = ActiveSlider.Instance :: Frame
local SliderField = Slider:FindFirstChild("SliderField" .. tostring(ActiveIndex)) :: TextButton
local GrabBar: Frame = SliderField.GrabBar
local increment: number = ActiveSlider.arguments.Increment and getValueByIndex(ActiveSlider.arguments.Increment, ActiveIndex, ActiveSlider.arguments) or defaultIncrements[ActiveDataType][ActiveIndex]
local min: number = ActiveSlider.arguments.Min and getValueByIndex(ActiveSlider.arguments.Min, ActiveIndex, ActiveSlider.arguments) or defaultMin[ActiveDataType][ActiveIndex]
local max: number = ActiveSlider.arguments.Max and getValueByIndex(ActiveSlider.arguments.Max, ActiveIndex, ActiveSlider.arguments) or defaultMax[ActiveDataType][ActiveIndex]
local GrabWidth: number = GrabBar.AbsoluteSize.X
local Offset: number = widgets.getMouseLocation().X - (SliderField.AbsolutePosition.X - widgets.GuiOffset.X + GrabWidth / 2)
local Ratio: number = Offset / (SliderField.AbsoluteSize.X - GrabWidth)
local Positions: number = math.floor((max - min) / increment)
local newValue: number = math.clamp(math.round(Ratio * Positions) * increment + min, min, max)
ActiveSlider.state.number:set(updateValueByIndex(ActiveSlider.state.number.value, ActiveIndex, newValue, ActiveSlider.arguments :: any))
ActiveSlider.lastNumberChangedTick = ImGui._cycleTick + 1
end
local function SliderMouseDown(thisWidget: Input, dataType: InputDataTypes, index: number)
local isCtrlHeld: boolean = widgets.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) or widgets.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)
if isCtrlHeld then
thisWidget.state.editingText:set(index)
else
AnyActiveSlider = true
ActiveSlider = thisWidget
ActiveIndex = index
ActiveDataType = dataType
updateActiveSlider()
end
end
widgets.registerEvent("InputChanged", function()
if not ImGui._started then
return
end
updateActiveSlider()
end)
widgets.registerEvent("InputEnded", function(inputObject: InputObject)
if not ImGui._started then
return
end
if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and AnyActiveSlider then
AnyActiveSlider = false
ActiveSlider = nil
ActiveIndex = 0
ActiveDataType = ""
end
end)
function generateSliderScalar(dataType: InputDataTypes, components: number, defaultValue: any)
return {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Increment"] = 2,
["Min"] = 3,
["Max"] = 4,
["Format"] = 5,
},
Events = {
["numberChanged"] = numberChanged,
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Input)
local Slider: Frame = Instance.new("Frame")
Slider.Name = "ImGui_Slider" .. dataType
Slider.Size = UDim2.fromScale(1, 0)
Slider.BackgroundTransparency = 1
Slider.BorderSizePixel = 0
Slider.LayoutOrder = thisWidget.ZIndex
Slider.AutomaticSize = Enum.AutomaticSize.Y
local UIListLayout: UIListLayout = widgets.UIListLayout(Slider, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
-- we divide the total area evenly between each field. This includes accounting for any additional boxes and the offset.
-- for the final field, we make sure it's flush by calculating the space avaiable for it. This only makes the Vector2 box
-- 4 pixels shorter, all for the sake of flush.
local componentWidth: UDim = UDim.new(ImGui._config.ContentWidth.Scale / components, (ImGui._config.ContentWidth.Offset - (ImGui._config.ItemInnerSpacing.X * (components - 1))) / components)
local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (ImGui._config.ItemInnerSpacing.X * (components - 1)))
local lastComponentWidth: UDim = ImGui._config.ContentWidth - totalWidth
for index = 1, components do
local SliderField: TextButton = Instance.new("TextButton")
SliderField.Name = "SliderField" .. tostring(index)
SliderField.LayoutOrder = index
if index == components then
SliderField.Size = UDim2.new(lastComponentWidth, ImGui._config.ContentHeight)
else
SliderField.Size = UDim2.new(componentWidth, ImGui._config.ContentHeight)
end
SliderField.AutomaticSize = Enum.AutomaticSize.Y
SliderField.BackgroundColor3 = ImGui._config.FrameBgColor
SliderField.BackgroundTransparency = ImGui._config.FrameBgTransparency
SliderField.AutoButtonColor = false
SliderField.Text = ""
SliderField.ClipsDescendants = true
widgets.applyFrameStyle(SliderField)
widgets.applyTextStyle(SliderField)
widgets.UISizeConstraint(SliderField, Vector2.xAxis)
SliderField.Parent = Slider
local OverlayText = Instance.new("TextLabel")
OverlayText.Name = "OverlayText"
OverlayText.Size = UDim2.fromScale(1, 1)
OverlayText.BackgroundTransparency = 1
OverlayText.BorderSizePixel = 0
OverlayText.ZIndex = 10
OverlayText.ClipsDescendants = true
widgets.applyTextStyle(OverlayText)
OverlayText.TextXAlignment = Enum.TextXAlignment.Center
OverlayText.Parent = SliderField
widgets.applyInteractionHighlights("Background", SliderField, SliderField, {
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
})
local InputField: TextBox = Instance.new("TextBox")
InputField.Name = "InputField"
InputField.Size = UDim2.new(1, 0, 1, 0)
InputField.BackgroundTransparency = 1
InputField.ClearTextOnFocus = false
InputField.TextTruncate = Enum.TextTruncate.AtEnd
InputField.ClipsDescendants = true
InputField.Visible = false
widgets.applyFrameStyle(InputField, true)
widgets.applyTextStyle(InputField)
InputField.Parent = SliderField
InputField.FocusLost:Connect(function()
local newValue: number? = tonumber(InputField.Text:match("-?%d*%.?%d*"))
if newValue ~= nil then
if thisWidget.arguments.Min ~= nil then
newValue = math.max(newValue, getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments))
end
if thisWidget.arguments.Max ~= nil then
newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments))
end
if thisWidget.arguments.Increment then
newValue = math.round(newValue / getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)) * getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
end
thisWidget.state.number:set(updateValueByIndex(thisWidget.state.number.value, index, newValue, thisWidget.arguments :: any))
thisWidget.lastNumberChangedTick = ImGui._cycleTick + 1
end
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
InputField.Text = string.format(format, getValueByIndex(thisWidget.state.number.value, index, thisWidget.arguments))
thisWidget.state.editingText:set(0)
InputField:ReleaseFocus(true)
end)
InputField.Focused:Connect(function()
-- this highlights the entire field
InputField.CursorPosition = #InputField.Text + 1
InputField.SelectionStart = 1
thisWidget.state.editingText:set(index)
end)
widgets.applyButtonDown(SliderField, function()
SliderMouseDown(thisWidget :: any, dataType, index)
end)
local GrabBar: Frame = Instance.new("Frame")
GrabBar.Name = "GrabBar"
GrabBar.ZIndex = 5
GrabBar.AnchorPoint = Vector2.new(0.5, 0.5)
GrabBar.Position = UDim2.new(0, 0, 0.5, 0)
GrabBar.BorderSizePixel = 0
GrabBar.BackgroundColor3 = ImGui._config.SliderGrabColor
GrabBar.Transparency = ImGui._config.SliderGrabTransparency
if ImGui._config.GrabRounding > 0 then
widgets.UICorner(GrabBar, ImGui._config.GrabRounding)
end
widgets.UISizeConstraint(GrabBar, Vector2.new(ImGui._config.GrabMinSize, 0))
GrabBar.Parent = SliderField
end
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 5
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Slider
return Slider
end,
Update = function(thisWidget: Input)
local Input = thisWidget.Instance :: GuiObject
local TextLabel: TextLabel = Input.TextLabel
TextLabel.Text = thisWidget.arguments.Text or `Slider {dataType}`
if thisWidget.arguments.Format and typeof(thisWidget.arguments.Format) ~= "table" then
thisWidget.arguments.Format = { thisWidget.arguments.Format }
elseif not thisWidget.arguments.Format then
-- we calculate the format for the s.f. using the max, min and increment arguments.
local format: { string } = {}
for index = 1, components do
local sigfigs: number = defaultSigFigs[dataType][index]
if thisWidget.arguments.Increment then
local value: number = getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Max then
local value: number = getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if thisWidget.arguments.Min then
local value: number = getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments)
sigfigs = math.max(sigfigs, math.ceil(-math.log10(value == 0 and 1 or value)), sigfigs)
end
if sigfigs > 0 then
-- we know it's a float.
format[index] = `%.{sigfigs}f`
else
format[index] = "%d"
end
end
thisWidget.arguments.Format = format
thisWidget.arguments.Prefix = defaultPrefx[dataType]
end
for index = 1, components do
local SliderField = Input:FindFirstChild("SliderField" .. tostring(index)) :: TextButton
local GrabBar: Frame = SliderField.GrabBar
local increment: number = thisWidget.arguments.Increment and getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments) or defaultIncrements[dataType][index]
local min: number = thisWidget.arguments.Min and getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments) or defaultMin[dataType][index]
local max: number = thisWidget.arguments.Max and getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments) or defaultMax[dataType][index]
local grabScaleSize: number = 1 / math.floor((1 + max - min) / increment)
GrabBar.Size = UDim2.new(grabScaleSize, 0, 1, 0)
end
local callbackIndex: number = #ImGui._postCycleCallbacks + 1
local desiredCycleTick: number = ImGui._cycleTick + 1
ImGui._postCycleCallbacks[callbackIndex] = function()
if ImGui._cycleTick >= desiredCycleTick then
if thisWidget.lastCycleTick ~= -1 then
ImGui._widgets[`Slider{dataType}`].UpdateState(thisWidget)
end
ImGui._postCycleCallbacks[callbackIndex] = nil
end
end
end,
Discard = function(thisWidget: Input)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: Input)
if thisWidget.state.number == nil then
thisWidget.state.number = ImGui._widgetState(thisWidget, "number", defaultValue)
end
if thisWidget.state.editingText == nil then
thisWidget.state.editingText = ImGui._widgetState(thisWidget, "editingText", false)
end
end,
UpdateState = function(thisWidget: Input)
local Slider = thisWidget.Instance :: Frame
for index = 1, components do
local SliderField = Slider:FindFirstChild("SliderField" .. tostring(index)) :: TextButton
local InputField: TextBox = SliderField.InputField
local OverlayText: TextLabel = SliderField.OverlayText
local GrabBar: Frame = SliderField.GrabBar
local value: number = getValueByIndex(thisWidget.state.number.value, index, thisWidget.arguments)
local format: string = thisWidget.arguments.Format[index] or thisWidget.arguments.Format[1]
if thisWidget.arguments.Prefix then
format = thisWidget.arguments.Prefix[index] .. format
end
OverlayText.Text = string.format(format, value)
InputField.Text = tostring(value)
local increment: number = thisWidget.arguments.Increment and getValueByIndex(thisWidget.arguments.Increment, index, thisWidget.arguments) or defaultIncrements[dataType][index]
local min: number = thisWidget.arguments.Min and getValueByIndex(thisWidget.arguments.Min, index, thisWidget.arguments) or defaultMin[dataType][index]
local max: number = thisWidget.arguments.Max and getValueByIndex(thisWidget.arguments.Max, index, thisWidget.arguments) or defaultMax[dataType][index]
local SliderWidth: number = SliderField.AbsoluteSize.X
local PaddedWidth: number = SliderWidth - GrabBar.AbsoluteSize.X
local Ratio: number = (value - min) / (max - min)
local Positions: number = math.floor((max - min) / increment)
local ClampedRatio: number = math.clamp(math.floor((Ratio * Positions)) / Positions, 0, 1)
local PaddedRatio: number = ((PaddedWidth / SliderWidth) * ClampedRatio) + ((1 - (PaddedWidth / SliderWidth)) / 2)
GrabBar.Position = UDim2.new(PaddedRatio, 0, 0.5, 0)
if thisWidget.state.editingText.value == index then
InputField.Visible = true
OverlayText.Visible = false
GrabBar.Visible = false
InputField:CaptureFocus()
else
InputField.Visible = false
OverlayText.Visible = true
GrabBar.Visible = true
end
end
end,
}
end
function generateEnumSliderScalar(enum: Enum, item: EnumItem)
local input: WidgetClass = generateSliderScalar("Enum", 1, item.Value)
local valueToName = { string }
for _, enumItem: EnumItem in enum:GetEnumItems() do
valueToName[enumItem.Value] = enumItem.Name
end
return widgets.extend(input, {
Args = {
["Text"] = 1,
},
Update = function(thisWidget: InputEnum)
local Input = thisWidget.Instance :: GuiObject
local TextLabel: TextLabel = Input.TextLabel
TextLabel.Text = thisWidget.arguments.Text or "Input Enum"
thisWidget.arguments.Increment = 1
thisWidget.arguments.Min = 0
thisWidget.arguments.Max = #enum:GetEnumItems() - 1
local SliderField = Input:FindFirstChild("SliderField1") :: TextButton
local GrabBar: Frame = SliderField.GrabBar
local grabScaleSize: number = 1 / math.floor(#enum:GetEnumItems())
GrabBar.Size = UDim2.new(grabScaleSize, 0, 1, 0)
end,
GenerateState = function(thisWidget: InputEnum)
if thisWidget.state.number == nil then
thisWidget.state.number = ImGui._widgetState(thisWidget, "number", item.Value)
end
if thisWidget.state.enumItem == nil then
thisWidget.state.enumItem = ImGui._widgetState(thisWidget, "enumItem", item)
end
if thisWidget.state.editingText == nil then
thisWidget.state.editingText = ImGui._widgetState(thisWidget, "editingText", false)
end
end,
})
end
end
do
local inputNum: WidgetClass = generateInputScalar("Num", 1, 0)
inputNum.Args["NoButtons"] = 6
ImGui.WidgetConstructor("InputNum", inputNum)
end
ImGui.WidgetConstructor("InputVector2", generateInputScalar("Vector2", 2, Vector2.zero))
ImGui.WidgetConstructor("InputVector3", generateInputScalar("Vector3", 3, Vector3.zero))
ImGui.WidgetConstructor("InputUDim", generateInputScalar("UDim", 2, UDim.new()))
ImGui.WidgetConstructor("InputUDim2", generateInputScalar("UDim2", 4, UDim2.new()))
ImGui.WidgetConstructor("InputRect", generateInputScalar("Rect", 4, Rect.new(0, 0, 0, 0)))
ImGui.WidgetConstructor("DragNum", generateDragScalar("Num", 1, 0))
ImGui.WidgetConstructor("DragVector2", generateDragScalar("Vector2", 2, Vector2.zero))
ImGui.WidgetConstructor("DragVector3", generateDragScalar("Vector3", 3, Vector3.zero))
ImGui.WidgetConstructor("DragUDim", generateDragScalar("UDim", 2, UDim.new()))
ImGui.WidgetConstructor("DragUDim2", generateDragScalar("UDim2", 4, UDim2.new()))
ImGui.WidgetConstructor("DragRect", generateDragScalar("Rect", 4, Rect.new(0, 0, 0, 0)))
ImGui.WidgetConstructor("InputColor3", generateColorDragScalar("Color3", Color3.fromRGB(0, 0, 0)))
ImGui.WidgetConstructor("InputColor4", generateColorDragScalar("Color4", Color3.fromRGB(0, 0, 0), 0))
ImGui.WidgetConstructor("SliderNum", generateSliderScalar("Num", 1, 0))
ImGui.WidgetConstructor("SliderVector2", generateSliderScalar("Vector2", 2, Vector2.zero))
ImGui.WidgetConstructor("SliderVector3", generateSliderScalar("Vector3", 3, Vector3.zero))
ImGui.WidgetConstructor("SliderUDim", generateSliderScalar("UDim", 2, UDim.new()))
ImGui.WidgetConstructor("SliderUDim2", generateSliderScalar("UDim2", 4, UDim2.new()))
ImGui.WidgetConstructor("SliderRect", generateSliderScalar("Rect", 4, Rect.new(0, 0, 0, 0)))
-- ImGui.WidgetConstructor("SliderEnum", generateSliderScalar("Enum", 4, 0))
-- stylua: ignore
ImGui.WidgetConstructor("InputText", {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["TextHint"] = 2,
["ReadOnly"] = 3,
["MultiLine"] = 4,
},
Events = {
["textChanged"] = {
["Init"] = function(thisWidget: InputText)
thisWidget.lastTextChangedTick = 0
end,
["Get"] = function(thisWidget: InputText)
return thisWidget.lastTextChangedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: InputText)
local InputText: Frame = Instance.new("Frame")
InputText.Name = "ImGui_InputText"
InputText.AutomaticSize = Enum.AutomaticSize.Y
InputText.Size = UDim2.fromScale(1, 0)
InputText.BackgroundTransparency = 1
InputText.BorderSizePixel = 0
InputText.ZIndex = thisWidget.ZIndex
InputText.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(InputText, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local InputField: TextBox = Instance.new("TextBox")
InputField.Name = "InputField"
InputField.Size = UDim2.new(ImGui._config.ContentWidth, ImGui._config.ContentHeight)
InputField.AutomaticSize = Enum.AutomaticSize.Y
InputField.BackgroundColor3 = ImGui._config.FrameBgColor
InputField.BackgroundTransparency = ImGui._config.FrameBgTransparency
InputField.Text = ""
InputField.TextYAlignment = Enum.TextYAlignment.Top
InputField.PlaceholderColor3 = ImGui._config.TextDisabledColor
InputField.ClearTextOnFocus = false
InputField.ClipsDescendants = true
widgets.applyFrameStyle(InputField)
widgets.applyTextStyle(InputField)
widgets.UISizeConstraint(InputField, Vector2.xAxis) -- prevents sizes beaking when getting too small.
-- InputField.UIPadding.PaddingLeft = UDim.new(0, ImGui._config.ItemInnerSpacing.X)
-- InputField.UIPadding.PaddingRight = UDim.new(0, 0)
InputField.Parent = InputText
InputField.FocusLost:Connect(function()
thisWidget.state.text:set(InputField.Text)
thisWidget.lastTextChangedTick = ImGui._cycleTick + 1
end)
local frameHeight: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.Size = UDim2.fromOffset(0, frameHeight)
TextLabel.AutomaticSize = Enum.AutomaticSize.X
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = InputText
return InputText
end,
Update = function(thisWidget: InputText)
local InputText = thisWidget.Instance :: Frame
local TextLabel: TextLabel = InputText.TextLabel
local InputField: TextBox = InputText.InputField
TextLabel.Text = thisWidget.arguments.Text or "Input Text"
InputField.PlaceholderText = thisWidget.arguments.TextHint or ""
InputField.TextEditable = not thisWidget.arguments.ReadOnly
InputField.MultiLine = thisWidget.arguments.MultiLine or false
end,
Discard = function(thisWidget: InputText)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: InputText)
if thisWidget.state.text == nil then
thisWidget.state.text = ImGui._widgetState(thisWidget, "text", "")
end
end,
UpdateState = function(thisWidget: InputText)
local InputText = thisWidget.Instance :: Frame
local InputField: TextBox = InputText.InputField
InputField.Text = thisWidget.state.text.value
end,
} :: WidgetClass)
-- blah blah blah here
ImGui.WidgetConstructor("ScriptEditor", {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["TextHint"] = 2,
["ReadOnly"] = 3,
["MultiLine"] = 4,
},
Events = {
["textChanged"] = {
["Init"] = function(thisWidget: InputText)
thisWidget.lastTextChangedTick = 0
end,
["Get"] = function(thisWidget: InputText)
return thisWidget.lastTextChangedTick == ImGui._cycleTick
end,
},
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: InputText)
local InputText: Frame = Instance.new("Frame")
InputText.Name = "ImGui_InputText"
InputText.AutomaticSize = Enum.AutomaticSize.Y
InputText.Size = UDim2.fromScale(1, 0)
InputText.BackgroundTransparency = 1
InputText.BorderSizePixel = 0
InputText.ZIndex = thisWidget.ZIndex
InputText.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(InputText, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local Editor = ScriptEditor.Embed(InputText)
Editor.AutoWrapEnabled = true
Editor:SetStringAsync('print("Hello, World!")\n -- still need to hook this up to state managment', 1)
--[[
InputField.FocusLost:Connect(function()
thisWidget.state.text:set(InputField.Text)
thisWidget.lastTextChangedTick = ImGui._cycleTick + 1
end)
]]--
local frameHeight: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.Size = UDim2.fromOffset(0, frameHeight)
TextLabel.AutomaticSize = Enum.AutomaticSize.X
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = InputText
return InputText
end,
Update = function(thisWidget: InputText)
local InputText = thisWidget.Instance :: Frame
local TextLabel: TextLabel = InputText.TextLabel
TextLabel.Text = thisWidget.arguments.Text or "Input Text"
end,
Discard = function(thisWidget: InputText)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: InputText)
if thisWidget.state.text == nil then
thisWidget.state.text = ImGui._widgetState(thisWidget, "text", "")
end
end,
UpdateState = function(thisWidget: InputText)
local InputText = thisWidget.Instance :: Frame
end,
} :: WidgetClass)
end,
Combo = function(ImGui: Internal, widgets: WidgetUtility)
ImGui.WidgetConstructor("Selectable", {
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Index"] = 2,
["NoClick"] = 3,
},
Events = {
["selected"] = {
["Init"] = function(_thisWidget: Selectable) end,
["Get"] = function(thisWidget: Selectable)
return thisWidget.lastSelectedTick == ImGui._cycleTick
end,
},
["unselected"] = {
["Init"] = function(_thisWidget: Selectable) end,
["Get"] = function(thisWidget: Selectable)
return thisWidget.lastUnselectedTick == ImGui._cycleTick
end,
},
["active"] = {
["Init"] = function(_thisWidget: Selectable) end,
["Get"] = function(thisWidget: Selectable)
return thisWidget.state.index.value == thisWidget.arguments.Index
end,
},
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
local Selectable = thisWidget.Instance :: Frame
return Selectable.SelectableButton
end),
["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Widget)
local Selectable = thisWidget.Instance :: Frame
return Selectable.SelectableButton
end),
["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Widget)
local Selectable = thisWidget.Instance :: Frame
return Selectable.SelectableButton
end),
["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Widget)
local Selectable = thisWidget.Instance :: Frame
return Selectable.SelectableButton
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
local Selectable = thisWidget.Instance :: Frame
return Selectable.SelectableButton
end),
},
Generate = function(thisWidget: Selectable)
local Selectable: Frame = Instance.new("Frame")
Selectable.Name = "ImGui_Selectable"
Selectable.Size = UDim2.new(ImGui._config.ItemWidth, UDim.new(0, ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y - ImGui._config.ItemSpacing.Y))
Selectable.BackgroundTransparency = 1
Selectable.BorderSizePixel = 0
Selectable.ZIndex = 0
Selectable.LayoutOrder = thisWidget.ZIndex
local SelectableButton: TextButton = Instance.new("TextButton")
SelectableButton.Name = "SelectableButton"
SelectableButton.Size = UDim2.new(1, 0, 0, ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y)
SelectableButton.Position = UDim2.fromOffset(0, -bit32.rshift(ImGui._config.ItemSpacing.Y, 1))
SelectableButton.BackgroundColor3 = ImGui._config.HeaderColor
SelectableButton.ClipsDescendants = true
widgets.applyFrameStyle(SelectableButton)
widgets.applyTextStyle(SelectableButton)
widgets.UISizeConstraint(SelectableButton, Vector2.xAxis)
thisWidget.ButtonColors = {
Color = ImGui._config.HeaderColor,
Transparency = 1,
HoveredColor = ImGui._config.HeaderHoveredColor,
HoveredTransparency = ImGui._config.HeaderHoveredTransparency,
ActiveColor = ImGui._config.HeaderActiveColor,
ActiveTransparency = ImGui._config.HeaderActiveTransparency,
}
widgets.applyInteractionHighlights("Background", SelectableButton, SelectableButton, thisWidget.ButtonColors)
widgets.applyButtonClick(SelectableButton, function()
if thisWidget.arguments.NoClick ~= true then
if type(thisWidget.state.index.value) == "boolean" then
thisWidget.state.index:set(not thisWidget.state.index.value)
else
thisWidget.state.index:set(thisWidget.arguments.Index)
end
end
end)
SelectableButton.Parent = Selectable
return Selectable
end,
Update = function(thisWidget: Selectable)
local Selectable = thisWidget.Instance :: Frame
local SelectableButton: TextButton = Selectable.SelectableButton
SelectableButton.Text = thisWidget.arguments.Text or "Selectable"
end,
Discard = function(thisWidget: Selectable)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
GenerateState = function(thisWidget: Selectable)
if thisWidget.state.index == nil then
if thisWidget.arguments.Index ~= nil then
error("a shared state index is required for Selectables with an Index argument", 5)
end
thisWidget.state.index = ImGui._widgetState(thisWidget, "index", false)
end
end,
UpdateState = function(thisWidget: Selectable)
local Selectable = thisWidget.Instance :: Frame
local SelectableButton: TextButton = Selectable.SelectableButton
if thisWidget.state.index.value == (thisWidget.arguments.Index or true) then
thisWidget.ButtonColors.Transparency = ImGui._config.HeaderTransparency
SelectableButton.BackgroundTransparency = ImGui._config.HeaderTransparency
thisWidget.lastSelectedTick = ImGui._cycleTick + 1
else
thisWidget.ButtonColors.Transparency = 1
SelectableButton.BackgroundTransparency = 1
thisWidget.lastUnselectedTick = ImGui._cycleTick + 1
end
end,
} :: WidgetClass)
local AnyOpenedCombo: boolean = false
local ComboOpenedTick: number = -1
local OpenedCombo: Combo? = nil
local function UpdateChildContainerTransform(thisWidget: Combo)
local Combo = thisWidget.Instance :: Frame
local PreviewContainer = Combo.PreviewContainer :: TextButton
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
ChildContainer.Size = UDim2.fromOffset(PreviewContainer.AbsoluteSize.X, 0)
local previewPosition: Vector2 = PreviewContainer.AbsolutePosition - widgets.GuiOffset
local previewSize: Vector2 = PreviewContainer.AbsoluteSize
local containerSize: Vector2 = ChildContainer.AbsoluteSize
local borderSize: number = ImGui._config.PopupBorderSize
local screenSize: Vector2 = ChildContainer.Parent.AbsoluteSize
local x: number = previewPosition.X
local y: number
local anchor: Vector2 = Vector2.zero
if previewPosition.Y + containerSize.Y > screenSize.Y then
y = previewPosition.Y - borderSize
anchor = Vector2.yAxis
else
y = previewPosition.Y + previewSize.Y + borderSize
end
ChildContainer.AnchorPoint = anchor
ChildContainer.Position = UDim2.fromOffset(x, y)
end
widgets.registerEvent("InputBegan", function(inputObject: InputObject)
if not ImGui._started then
return
end
if inputObject.UserInputType ~= Enum.UserInputType.MouseButton1 and inputObject.UserInputType ~= Enum.UserInputType.MouseButton2 and inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
if AnyOpenedCombo == false or not OpenedCombo then
return
end
if ComboOpenedTick == ImGui._cycleTick then
return
end
local MouseLocation: Vector2 = widgets.getMouseLocation()
local Combo = OpenedCombo.Instance :: Frame
local PreviewContainer: TextButton = Combo.PreviewContainer
local ChildContainer = OpenedCombo.ChildContainer
local rectMin: Vector2 = PreviewContainer.AbsolutePosition - widgets.GuiOffset
local rectMax: Vector2 = PreviewContainer.AbsolutePosition - widgets.GuiOffset + PreviewContainer.AbsoluteSize
if widgets.isPosInsideRect(MouseLocation, rectMin, rectMax) then
return
end
rectMin = ChildContainer.AbsolutePosition - widgets.GuiOffset
rectMax = ChildContainer.AbsolutePosition - widgets.GuiOffset + ChildContainer.AbsoluteSize
if widgets.isPosInsideRect(MouseLocation, rectMin, rectMax) then
return
end
OpenedCombo.state.isOpened:set(false)
end)
ImGui.WidgetConstructor("Combo", {
hasState = true,
hasChildren = true,
Args = {
["Text"] = 1,
["NoButton"] = 2,
["NoPreview"] = 3,
},
Events = {
["opened"] = {
["Init"] = function(_thisWidget: Combo) end,
["Get"] = function(thisWidget: Combo)
return thisWidget.lastOpenedTick == ImGui._cycleTick
end,
},
["closed"] = {
["Init"] = function(_thisWidget: Combo) end,
["Get"] = function(thisWidget: Combo)
return thisWidget.lastClosedTick == ImGui._cycleTick
end,
},
["clicked"] = widgets.EVENTS.click(function(thisWidget: Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Combo)
local frameHeight: number = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
local Combo: Frame = Instance.new("Frame")
Combo.Name = "ImGui_Combo"
Combo.Size = UDim2.fromScale(1, 0)
Combo.AutomaticSize = Enum.AutomaticSize.Y
Combo.BackgroundTransparency = 1
Combo.BorderSizePixel = 0
Combo.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(Combo, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local PreviewContainer: TextButton = Instance.new("TextButton")
PreviewContainer.Name = "PreviewContainer"
PreviewContainer.Size = UDim2.new(ImGui._config.ContentWidth, UDim.new(0, 0))
PreviewContainer.AutomaticSize = Enum.AutomaticSize.Y
PreviewContainer.BackgroundTransparency = 1
PreviewContainer.Text = ""
PreviewContainer.ZIndex = thisWidget.ZIndex + 2
PreviewContainer.AutoButtonColor = false
widgets.applyFrameStyle(PreviewContainer, true)
widgets.UIListLayout(PreviewContainer, Enum.FillDirection.Horizontal, UDim.new(0, 0))
widgets.UISizeConstraint(PreviewContainer, Vector2.new(frameHeight + 1))
PreviewContainer.Parent = Combo
local PreviewLabel: TextLabel = Instance.new("TextLabel")
PreviewLabel.Name = "PreviewLabel"
PreviewLabel.Size = UDim2.new(UDim.new(1, 0), ImGui._config.ContentHeight)
PreviewLabel.AutomaticSize = Enum.AutomaticSize.Y
PreviewLabel.BackgroundColor3 = ImGui._config.FrameBgColor
PreviewLabel.BackgroundTransparency = ImGui._config.FrameBgTransparency
PreviewLabel.BorderSizePixel = 0
PreviewLabel.ClipsDescendants = true
widgets.applyTextStyle(PreviewLabel)
widgets.UIPadding(PreviewLabel, ImGui._config.FramePadding)
PreviewLabel.Parent = PreviewContainer
local DropdownButton: TextLabel = Instance.new("TextLabel")
DropdownButton.Name = "DropdownButton"
DropdownButton.Size = UDim2.new(0, frameHeight, ImGui._config.ContentHeight.Scale, math.max(ImGui._config.ContentHeight.Offset, frameHeight))
DropdownButton.BorderSizePixel = 0
DropdownButton.BackgroundColor3 = ImGui._config.ButtonColor
DropdownButton.BackgroundTransparency = ImGui._config.ButtonTransparency
DropdownButton.Text = ""
local padding: number = math.round(frameHeight * 0.2)
local dropdownSize: number = frameHeight - 2 * padding
local Dropdown: ImageLabel = Instance.new("ImageLabel")
Dropdown.Name = "Dropdown"
Dropdown.AnchorPoint = Vector2.new(0.5, 0.5)
Dropdown.Size = UDim2.fromOffset(dropdownSize, dropdownSize)
Dropdown.Position = UDim2.fromScale(0.5, 0.5)
Dropdown.BackgroundTransparency = 1
Dropdown.BorderSizePixel = 0
Dropdown.ImageColor3 = ImGui._config.TextColor
Dropdown.ImageTransparency = ImGui._config.TextTransparency
Dropdown.Parent = DropdownButton
DropdownButton.Parent = PreviewContainer
widgets.applyInteractionHighlightsWithMultiHighlightee("Background", PreviewContainer, {
{
PreviewLabel,
{
Color = ImGui._config.FrameBgColor,
Transparency = ImGui._config.FrameBgTransparency,
HoveredColor = ImGui._config.FrameBgHoveredColor,
HoveredTransparency = ImGui._config.FrameBgHoveredTransparency,
ActiveColor = ImGui._config.FrameBgActiveColor,
ActiveTransparency = ImGui._config.FrameBgActiveTransparency,
},
},
{
DropdownButton,
{
Color = ImGui._config.ButtonColor,
Transparency = ImGui._config.ButtonTransparency,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonHoveredColor,
ActiveTransparency = ImGui._config.ButtonHoveredTransparency,
},
},
})
widgets.applyButtonClick(PreviewContainer, function()
if AnyOpenedCombo and OpenedCombo ~= thisWidget then
return
end
thisWidget.state.isOpened:set(not thisWidget.state.isOpened.value)
end)
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.Size = UDim2.fromOffset(0, frameHeight)
TextLabel.AutomaticSize = Enum.AutomaticSize.X
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
widgets.applyTextStyle(TextLabel)
TextLabel.Parent = Combo
local ChildContainer: ScrollingFrame = Instance.new("ScrollingFrame")
ChildContainer.Name = "ComboContainer"
ChildContainer.AutomaticSize = Enum.AutomaticSize.Y
ChildContainer.BackgroundColor3 = ImGui._config.PopupBgColor
ChildContainer.BackgroundTransparency = ImGui._config.PopupBgTransparency
ChildContainer.BorderSizePixel = 0
ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y
ChildContainer.ScrollBarImageTransparency = ImGui._config.ScrollbarGrabTransparency
ChildContainer.ScrollBarImageColor3 = ImGui._config.ScrollbarGrabColor
ChildContainer.ScrollBarThickness = ImGui._config.ScrollbarSize
ChildContainer.CanvasSize = UDim2.fromScale(0, 0)
ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ChildContainer.ClipsDescendants = true
widgets.UIStroke(ChildContainer, ImGui._config.WindowBorderSize, ImGui._config.BorderColor, ImGui._config.BorderTransparency)
widgets.UIPadding(ChildContainer, Vector2.new(2, ImGui._config.WindowPadding.Y))
widgets.UISizeConstraint(ChildContainer, Vector2.new(100))
local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
ChildContainerUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
local RootPopupScreenGui = ImGui._rootInstance and ImGui._rootInstance:WaitForChild("PopupScreenGui") :: GuiObject
ChildContainer.Parent = RootPopupScreenGui
thisWidget.ChildContainer = ChildContainer
return Combo
end,
Update = function(thisWidget: Combo)
local ImGui_Combo = thisWidget.Instance :: Frame
local PreviewContainer = ImGui_Combo.PreviewContainer :: TextButton
local PreviewLabel: TextLabel = PreviewContainer.PreviewLabel
local DropdownButton: TextLabel = PreviewContainer.DropdownButton
local TextLabel: TextLabel = ImGui_Combo.TextLabel
TextLabel.Text = thisWidget.arguments.Text or "Combo"
if thisWidget.arguments.NoButton then
DropdownButton.Visible = false
PreviewLabel.Size = UDim2.new(UDim.new(1, 0), PreviewLabel.Size.Height)
else
DropdownButton.Visible = true
local DropdownButtonSize = ImGui._config.TextSize + 2 * ImGui._config.FramePadding.Y
PreviewLabel.Size = UDim2.new(UDim.new(1, -DropdownButtonSize), PreviewLabel.Size.Height)
end
if thisWidget.arguments.NoPreview then
PreviewLabel.Visible = false
PreviewContainer.Size = UDim2.new(0, 0, 0, 0)
PreviewContainer.AutomaticSize = Enum.AutomaticSize.XY
else
PreviewLabel.Visible = true
PreviewContainer.Size = UDim2.new(ImGui._config.ContentWidth, ImGui._config.ContentHeight)
PreviewContainer.AutomaticSize = Enum.AutomaticSize.Y
end
end,
ChildAdded = function(thisWidget: Combo, _thisChild: Widget)
UpdateChildContainerTransform(thisWidget)
return thisWidget.ChildContainer
end,
GenerateState = function(thisWidget: Combo)
if thisWidget.state.index == nil then
thisWidget.state.index = ImGui._widgetState(thisWidget, "index", "No Selection")
end
thisWidget.state.index:onChange(function()
if thisWidget.state.isOpened.value then
thisWidget.state.isOpened:set(false)
end
end)
if thisWidget.state.isOpened == nil then
thisWidget.state.isOpened = ImGui._widgetState(thisWidget, "isOpened", false)
end
end,
UpdateState = function(thisWidget: Combo)
local Combo = thisWidget.Instance :: Frame
local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame
local PreviewContainer = Combo.PreviewContainer :: TextButton
local PreviewLabel: TextLabel = PreviewContainer.PreviewLabel
local DropdownButton = PreviewContainer.DropdownButton :: TextLabel
local Dropdown: ImageLabel = DropdownButton.Dropdown
if thisWidget.state.isOpened.value then
AnyOpenedCombo = true
OpenedCombo = thisWidget
ComboOpenedTick = ImGui._cycleTick
thisWidget.lastOpenedTick = ImGui._cycleTick + 1
Dropdown.Image = widgets.ICONS.RIGHT_POINTING_TRIANGLE
ChildContainer.Visible = true
UpdateChildContainerTransform(thisWidget)
else
if AnyOpenedCombo then
AnyOpenedCombo = false
OpenedCombo = nil
thisWidget.lastClosedTick = ImGui._cycleTick + 1
end
Dropdown.Image = widgets.ICONS.DOWN_POINTING_TRIANGLE
ChildContainer.Visible = false
end
local stateIndex: any = thisWidget.state.index.value
PreviewLabel.Text = if typeof(stateIndex) == "EnumItem" then stateIndex.Name else tostring(stateIndex)
end,
Discard = function(thisWidget: Combo)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
} :: WidgetClass)
end,
Plot = function(ImGui: Internal, widgets: WidgetUtility)
ImGui.WidgetConstructor(
"ProgressBar",
{
hasState = true,
hasChildren = false,
Args = {
["Text"] = 1,
["Format"] = 2,
},
Events = {
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
["changed"] = {
["Init"] = function(_thisWidget: ProgressBar) end,
["Get"] = function(thisWidget: ProgressBar)
return thisWidget.lastChangedTick == ImGui._cycleTick
end,
},
},
Generate = function(thisWidget: ProgressBar)
local ProgressBar: Frame = Instance.new("Frame")
ProgressBar.Name = "ImGui_ProgressBar"
ProgressBar.Size = UDim2.new(ImGui._config.ItemWidth, UDim.new())
ProgressBar.BackgroundTransparency = 1
ProgressBar.AutomaticSize = Enum.AutomaticSize.Y
ProgressBar.LayoutOrder = thisWidget.ZIndex
local UIListLayout: UIListLayout = widgets.UIListLayout(ProgressBar, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
local Bar: Frame = Instance.new("Frame")
Bar.Name = "Bar"
Bar.Size = UDim2.new(ImGui._config.ContentWidth, ImGui._config.ContentHeight)
Bar.BackgroundColor3 = ImGui._config.FrameBgColor
Bar.BackgroundTransparency = ImGui._config.FrameBgTransparency
Bar.BorderSizePixel = 0
Bar.AutomaticSize = Enum.AutomaticSize.Y
Bar.ClipsDescendants = true
widgets.applyFrameStyle(Bar, true)
Bar.Parent = ProgressBar
local Progress: TextLabel = Instance.new("TextLabel")
Progress.Name = "Progress"
Progress.AutomaticSize = Enum.AutomaticSize.Y
Progress.Size = UDim2.new(UDim.new(0, 0), ImGui._config.ContentHeight)
Progress.BackgroundColor3 = ImGui._config.PlotHistogramColor
Progress.BackgroundTransparency = ImGui._config.PlotHistogramTransparency
Progress.BorderSizePixel = 0
widgets.applyTextStyle(Progress)
widgets.UIPadding(Progress, ImGui._config.FramePadding)
widgets.UICorner(Progress, ImGui._config.FrameRounding)
Progress.Text = ""
Progress.Parent = Bar
local Value: TextLabel = Instance.new("TextLabel")
Value.Name = "Value"
Value.AutomaticSize = Enum.AutomaticSize.XY
Value.Size = UDim2.new(UDim.new(0, 0), ImGui._config.ContentHeight)
Value.BackgroundTransparency = 1
Value.BorderSizePixel = 0
Value.ZIndex = 1
widgets.applyTextStyle(Value)
widgets.UIPadding(Value, ImGui._config.FramePadding)
Value.Parent = Bar
local TextLabel: TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.AnchorPoint = Vector2.new(0, 0.5)
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
TextLabel.LayoutOrder = 1
widgets.applyTextStyle(TextLabel)
widgets.UIPadding(Value, ImGui._config.FramePadding)
TextLabel.Parent = ProgressBar
return ProgressBar
end,
GenerateState = function(thisWidget: ProgressBar)
if thisWidget.state.progress == nil then
thisWidget.state.progress = ImGui._widgetState(thisWidget, "Progress", 0)
end
end,
Update = function(thisWidget: ProgressBar)
local Progress = thisWidget.Instance :: Frame
local TextLabel: TextLabel = Progress.TextLabel
local Bar = Progress.Bar :: Frame
local Value: TextLabel = Bar.Value
if thisWidget.arguments.Format ~= nil and typeof(thisWidget.arguments.Format) == "string" then
Value.Text = thisWidget.arguments.Format
end
TextLabel.Text = thisWidget.arguments.Text or "Progress Bar"
end,
UpdateState = function(thisWidget: ProgressBar)
local ProgressBar = thisWidget.Instance :: Frame
local Bar = ProgressBar.Bar :: Frame
local Progress: TextLabel = Bar.Progress
local Value: TextLabel = Bar.Value
local progress: number = thisWidget.state.progress.value
progress = math.clamp(progress, 0, 1)
local totalWidth: number = Bar.AbsoluteSize.X
local textWidth: number = Value.AbsoluteSize.X
if totalWidth * (1 - progress) < textWidth then
Value.AnchorPoint = Vector2.xAxis
Value.Position = UDim2.fromScale(1, 0)
else
Value.AnchorPoint = Vector2.zero
Value.Position = UDim2.new(progress, 0, 0, 0)
end
Progress.Size = UDim2.new(UDim.new(progress, 0), Progress.Size.Height)
if thisWidget.arguments.Format ~= nil and typeof(thisWidget.arguments.Format) == "string" then
Value.Text = thisWidget.arguments.Format
else
Value.Text = string.format("%d%%", progress * 100)
end
thisWidget.lastChangedTick = ImGui._cycleTick + 1
end,
Discard = function(thisWidget: ProgressBar)
thisWidget.Instance:Destroy()
widgets.discardState(thisWidget)
end,
} :: WidgetClass
)
end,
Table = function(ImGui: Internal, widgets: WidgetUtility)
local tableWidgets: { [ID]: Table } = {}
-- reset the cell index every frame.
table.insert(ImGui._postCycleCallbacks, function()
for _, thisWidget: Table in tableWidgets do
thisWidget.RowColumnIndex = 0
end
end)
--stylua: ignore
ImGui.WidgetConstructor("Table", {
hasState = false,
hasChildren = true,
Args = {
["NumColumns"] = 1,
["RowBg"] = 2,
["BordersOuter"] = 3,
["BordersInner"] = 4,
},
Events = {
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Widget)
return thisWidget.Instance
end),
},
Generate = function(thisWidget: Table)
tableWidgets[thisWidget.ID] = thisWidget
thisWidget.InitialNumColumns = -1
thisWidget.RowColumnIndex = 0
-- reference to these is stored as an optimization
thisWidget.ColumnInstances = {}
thisWidget.CellInstances = {}
local Table: Frame = Instance.new("Frame")
Table.Name = "ImGui_Table"
Table.Size = UDim2.new(ImGui._config.ItemWidth, UDim.new(0, 0))
Table.AutomaticSize = Enum.AutomaticSize.Y
Table.BackgroundTransparency = 1
Table.BorderSizePixel = 0
Table.ZIndex = thisWidget.ZIndex + 1024 -- allocate room for 1024 cells, because Table UIStroke has to appear above cell UIStroke
Table.LayoutOrder = thisWidget.ZIndex
Table.ClipsDescendants = true
widgets.UIListLayout(Table, Enum.FillDirection.Horizontal, UDim.new(0, 0))
widgets.UIStroke(Table, 1, ImGui._config.TableBorderStrongColor, ImGui._config.TableBorderStrongTransparency)
return Table
end,
Update = function(thisWidget: Table)
local Table = thisWidget.Instance :: Frame
if thisWidget.arguments.BordersOuter == false then
Table.UIStroke.Thickness = 0
else
Table.UIStroke.Thickness = 1
end
if thisWidget.InitialNumColumns == -1 then
if thisWidget.arguments.NumColumns == nil then
error("ImGui.Table NumColumns argument is required", 5)
end
thisWidget.InitialNumColumns = thisWidget.arguments.NumColumns
for index = 1, thisWidget.InitialNumColumns do
local zindex: number = thisWidget.ZIndex + 1 + index
local Column: Frame = Instance.new("Frame")
Column.Name = `Column_{index}`
Column.Size = UDim2.new(1 / thisWidget.InitialNumColumns, 0, 0, 0)
Column.AutomaticSize = Enum.AutomaticSize.Y
Column.BackgroundTransparency = 1
Column.BorderSizePixel = 0
Column.ZIndex = zindex
Column.LayoutOrder = zindex
Column.ClipsDescendants = true
widgets.UIListLayout(Column, Enum.FillDirection.Vertical, UDim.new(0, 0))
thisWidget.ColumnInstances[index] = Column
Column.Parent = Table
end
elseif thisWidget.arguments.NumColumns ~= thisWidget.InitialNumColumns then
-- its possible to make it so that the NumColumns can increase,
-- but decreasing it would interfere with child widget instances
error("ImGui.Table NumColumns Argument must be static")
end
if thisWidget.arguments.RowBg == false then
for _, Cell: Frame in thisWidget.CellInstances do
Cell.BackgroundTransparency = 1
end
else
for index: number, Cell: Frame in thisWidget.CellInstances do
local currentRow: number = math.ceil(index / thisWidget.InitialNumColumns)
Cell.BackgroundTransparency = if currentRow % 2 == 0 then ImGui._config.TableRowBgAltTransparency else ImGui._config.TableRowBgTransparency
end
end
-- wooo, I love lua Especially on an object and child based system like Roblox! I never have to do anything
-- annoying or dumb to make it like me!
if thisWidget.arguments.BordersInner == false then
for _, Cell: Frame & { UIStroke: UIStroke } in thisWidget.CellInstances :: any do
Cell.UIStroke.Thickness = 0
end
else
for _, Cell: Frame & { UIStroke: UIStroke } in thisWidget.CellInstances :: any do
Cell.UIStroke.Thickness = 0.5
end
end
end,
Discard = function(thisWidget: Table)
tableWidgets[thisWidget.ID] = nil
thisWidget.Instance:Destroy()
end,
ChildAdded = function(thisWidget: Table, _thisChild: Widget)
if thisWidget.RowColumnIndex == 0 then
thisWidget.RowColumnIndex = 1
end
local potentialCellParent: Frame = thisWidget.CellInstances[thisWidget.RowColumnIndex]
if potentialCellParent then
return potentialCellParent
end
local selectedParent: Frame = thisWidget.ColumnInstances[((thisWidget.RowColumnIndex - 1) % thisWidget.InitialNumColumns) + 1]
local zindex: number = selectedParent.ZIndex + thisWidget.RowColumnIndex
local Cell: Frame = Instance.new("Frame")
Cell.Name = `Cell_{thisWidget.RowColumnIndex}`
Cell.Size = UDim2.new(1, 0, 0, 0)
Cell.AutomaticSize = Enum.AutomaticSize.Y
Cell.BackgroundTransparency = 1
Cell.BorderSizePixel = 0
Cell.ZIndex = zindex
Cell.LayoutOrder = zindex
Cell.ClipsDescendants = true
widgets.UIPadding(Cell, ImGui._config.CellPadding)
widgets.UIListLayout(Cell, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
if thisWidget.arguments.BordersInner == false then
widgets.UIStroke(Cell, 0, ImGui._config.TableBorderLightColor, ImGui._config.TableBorderLightTransparency)
else
widgets.UIStroke(Cell, 0.5, ImGui._config.TableBorderLightColor, ImGui._config.TableBorderLightTransparency)
-- this takes advantage of unintended behavior when UIStroke is set to 0.5 to render cell borders,
-- at 0.5, only the top and left side of the cell will be rendered with a border.
end
if thisWidget.arguments.RowBg ~= false then
local currentRow: number = math.ceil(thisWidget.RowColumnIndex / thisWidget.InitialNumColumns)
local color: Color3 = if currentRow % 2 == 0 then ImGui._config.TableRowBgAltColor else ImGui._config.TableRowBgColor
local transparency: number = if currentRow % 2 == 0 then ImGui._config.TableRowBgAltTransparency else ImGui._config.TableRowBgTransparency
Cell.BackgroundColor3 = color
Cell.BackgroundTransparency = transparency
end
thisWidget.CellInstances[thisWidget.RowColumnIndex] = Cell
Cell.Parent = selectedParent
return Cell
end,
} :: WidgetClass)
end,
Tab = function(ImGui: Types.Internal, widgets: Types.WidgetUtility)
local function openTab(TabBar: Types.TabBar, Index: number)
if TabBar.state.index.value > 0 then
return
end
TabBar.state.index:set(Index)
end
local function closeTab(TabBar: Types.TabBar, Index: number)
-- search left for open tabs
for i = Index - 1, 1, -1 do
if TabBar.Tabs[i].state.isOpened.value == true then
TabBar.state.index:set(i)
return
end
end
-- search right for open tabs
for i = Index, #TabBar.Tabs do
if TabBar.Tabs[i].state.isOpened.value == true then
TabBar.state.index:set(i)
return
end
end
-- no open tabs, so wait for one
TabBar.state.index:set(0)
end
--stylua: ignore
ImGui.WidgetConstructor("TabBar", {
hasState = true,
hasChildren = true,
Args = {},
Events = {},
Generate = function(thisWidget: Types.TabBar)
local TabBar: Frame = Instance.new("Frame")
TabBar.Name = "ImGui_TabBar"
TabBar.AutomaticSize = Enum.AutomaticSize.Y
TabBar.Size = UDim2.fromScale(1, 0)
TabBar.BackgroundTransparency = 1
TabBar.BorderSizePixel = 0
TabBar.LayoutOrder = thisWidget.ZIndex
widgets.UIListLayout(TabBar, Enum.FillDirection.Vertical, UDim.new()).VerticalAlignment = Enum.VerticalAlignment.Bottom
local Bar: Frame = Instance.new("Frame")
Bar.Name = "Bar"
Bar.AutomaticSize = Enum.AutomaticSize.Y
Bar.Size = UDim2.fromScale(1, 0)
Bar.BackgroundTransparency = 1
Bar.BorderSizePixel = 0
widgets.UIListLayout(Bar, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X))
Bar.Parent = TabBar
local Underline: Frame = Instance.new("Frame")
Underline.Name = "Underline"
Underline.Size = UDim2.new(1, 0, 0, 1)
Underline.BackgroundColor3 = ImGui._config.TabActiveColor
Underline.BackgroundTransparency = ImGui._config.TabActiveTransparency
Underline.BorderSizePixel = 0
Underline.LayoutOrder = 1
Underline.Parent = TabBar
local ChildContainer: Frame = Instance.new("Frame")
ChildContainer.Name = "TabContainer"
ChildContainer.AutomaticSize = Enum.AutomaticSize.Y
ChildContainer.Size = UDim2.fromScale(1, 0)
ChildContainer.BackgroundTransparency = 1
ChildContainer.BorderSizePixel = 0
ChildContainer.LayoutOrder = 2
ChildContainer.ClipsDescendants = true
ChildContainer.Parent = TabBar
thisWidget.ChildContainer = ChildContainer
thisWidget.Tabs = {}
return TabBar
end,
Update = function(_thisWidget: Types.TabBar) end,
ChildAdded = function(thisWidget: Types.TabBar, thisChild: Types.Tab)
assert(thisChild.type == "Tab", "Only ImGui.Tab can be parented to ImGui.TabBar.")
local TabBar = thisWidget.Instance :: Frame
thisChild.ChildContainer.Parent = thisWidget.ChildContainer
thisChild.Index = #thisWidget.Tabs + 1
thisWidget.state.index.ConnectedWidgets[thisChild.ID] = thisChild
table.insert(thisWidget.Tabs, thisChild)
return TabBar.Bar
end,
ChildDiscarded = function(thisWidget: Types.TabBar, thisChild: Types.Tab)
local Index: number = thisChild.Index
table.remove(thisWidget.Tabs, Index)
for i = Index, #thisWidget.Tabs do
thisWidget.Tabs[i].Index = i
end
closeTab(thisWidget, Index)
end,
GenerateState = function(thisWidget: Types.Tab)
if thisWidget.state.index == nil then
thisWidget.state.index = ImGui._widgetState(thisWidget, "index", 1)
end
end,
UpdateState = function(_thisWidget: Types.Tab)
end,
Discard = function(thisWidget: Types.TabBar)
thisWidget.Instance:Destroy()
end,
} :: Types.WidgetClass)
--stylua: ignore
ImGui.WidgetConstructor("Tab", {
hasState = true,
hasChildren = true,
Args = {
["Text"] = 1,
["Hideable"] = 2,
},
Events = {
["clicked"] = widgets.EVENTS.click(function(thisWidget: Types.Widget)
return thisWidget.Instance
end),
["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget)
return thisWidget.Instance
end),
["selected"] = {
["Init"] = function(_thisWidget: Types.Tab) end,
["Get"] = function(thisWidget: Types.Tab)
return thisWidget.lastSelectedTick == ImGui._cycleTick
end,
},
["unselected"] = {
["Init"] = function(_thisWidget: Types.Tab) end,
["Get"] = function(thisWidget: Types.Tab)
return thisWidget.lastUnselectedTick == ImGui._cycleTick
end,
},
["active"] = {
["Init"] = function(_thisWidget: Types.Tab) end,
["Get"] = function(thisWidget: Types.Tab)
return thisWidget.state.index.value == thisWidget.Index
end,
},
["opened"] = {
["Init"] = function(_thisWidget: Types.Tab) end,
["Get"] = function(thisWidget: Types.Tab)
return thisWidget.lastOpenedTick == ImGui._cycleTick
end,
},
["closed"] = {
["Init"] = function(_thisWidget: Types.Tab) end,
["Get"] = function(thisWidget: Types.Tab)
return thisWidget.lastClosedTick == ImGui._cycleTick
end,
},
},
Generate = function(thisWidget: Types.Tab)
local Tab = Instance.new("TextButton")
Tab.Name = "ImGui_Tab"
Tab.AutomaticSize = Enum.AutomaticSize.XY
Tab.BackgroundColor3 = ImGui._config.TabColor
Tab.BackgroundTransparency = ImGui._config.TabTransparency
Tab.BorderSizePixel = 0
Tab.Text = ""
Tab.AutoButtonColor = false
thisWidget.ButtonColors = {
Color = ImGui._config.TabColor,
Transparency = ImGui._config.TabTransparency,
HoveredColor = ImGui._config.TabHoveredColor,
HoveredTransparency = ImGui._config.TabHoveredTransparency,
ActiveColor = ImGui._config.TabActiveColor,
ActiveTransparency = ImGui._config.TabActiveTransparency,
}
widgets.UIPadding(Tab, Vector2.new(ImGui._config.FramePadding.X, 0))
widgets.applyFrameStyle(Tab, true, true)
widgets.UIListLayout(Tab, Enum.FillDirection.Horizontal, UDim.new(0, ImGui._config.ItemInnerSpacing.X)).VerticalAlignment = Enum.VerticalAlignment.Center
widgets.applyInteractionHighlights("Background", Tab, Tab, thisWidget.ButtonColors)
widgets.applyButtonClick(Tab, function()
thisWidget.state.index:set(thisWidget.Index)
end)
local TextLabel = Instance.new("TextLabel")
TextLabel.Name = "TextLabel"
TextLabel.AutomaticSize = Enum.AutomaticSize.XY
TextLabel.BackgroundTransparency = 1
TextLabel.BorderSizePixel = 0
widgets.applyTextStyle(TextLabel)
widgets.UIPadding(TextLabel, Vector2.new(0, ImGui._config.FramePadding.Y))
TextLabel.Parent = Tab
local ButtonSize: number = ImGui._config.TextSize + ((ImGui._config.FramePadding.Y - 1) * 2)
local CloseButton = Instance.new("TextButton")
CloseButton.Name = "CloseButton"
CloseButton.BackgroundTransparency = 1
CloseButton.BorderSizePixel = 0
CloseButton.LayoutOrder = 1
CloseButton.Size = UDim2.fromOffset(ButtonSize, ButtonSize)
CloseButton.Text = ""
CloseButton.AutoButtonColor = false
widgets.UICorner(CloseButton)
widgets.applyButtonClick(CloseButton, function()
thisWidget.state.isOpened:set(false)
closeTab(thisWidget.parentWidget, thisWidget.Index)
end)
widgets.applyInteractionHighlights("Background", CloseButton, CloseButton, {
Color = ImGui._config.TabColor,
Transparency = 1,
HoveredColor = ImGui._config.ButtonHoveredColor,
HoveredTransparency = ImGui._config.ButtonHoveredTransparency,
ActiveColor = ImGui._config.ButtonActiveColor,
ActiveTransparency = ImGui._config.ButtonActiveTransparency,
})
CloseButton.Parent = Tab
local Icon = Instance.new("ImageLabel")
Icon.Name = "Icon"
Icon.AnchorPoint = Vector2.new(0.5, 0.5)
Icon.BackgroundTransparency = 1
Icon.BorderSizePixel = 0
Icon.Image = widgets.ICONS.MULTIPLICATION_SIGN
Icon.ImageTransparency = 1
Icon.Position = UDim2.fromScale(0.5, 0.5)
Icon.Size = UDim2.fromOffset(math.floor(0.7 * ButtonSize), math.floor(0.7 * ButtonSize))
widgets.applyInteractionHighlights("Image", Tab, Icon, {
Color = ImGui._config.TextColor,
Transparency = 1,
HoveredColor = ImGui._config.TextColor,
HoveredTransparency = ImGui._config.TextTransparency,
ActiveColor = ImGui._config.TextColor,
ActiveTransparency = ImGui._config.TextTransparency,
})
Icon.Parent = CloseButton
local ChildContainer: Frame = Instance.new("Frame")
ChildContainer.Name = "TabContainer"
ChildContainer.AutomaticSize = Enum.AutomaticSize.Y
ChildContainer.Size = UDim2.fromScale(1, 0)
ChildContainer.BackgroundTransparency = 1
ChildContainer.BorderSizePixel = 0
ChildContainer.ClipsDescendants = true
widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, ImGui._config.ItemSpacing.Y))
widgets.UIPadding(ChildContainer, Vector2.new(0, ImGui._config.ItemSpacing.Y)).PaddingBottom = UDim.new()
thisWidget.ChildContainer = ChildContainer
return Tab
end,
Update = function(thisWidget: Types.Tab)
local Tab = thisWidget.Instance :: TextButton
local TextLabel: TextLabel = Tab.TextLabel
local CloseButton: TextButton = Tab.CloseButton
TextLabel.Text = thisWidget.arguments.Text
CloseButton.Visible = if thisWidget.arguments.Hideable == true then true else false
end,
ChildAdded = function(thisWidget: Types.Tab, _thisChild: Types.Widget)
return thisWidget.ChildContainer
end,
GenerateState = function(thisWidget: Types.Tab)
thisWidget.state.index = thisWidget.parentWidget.state.index
thisWidget.state.index.ConnectedWidgets[thisWidget.ID] = thisWidget
if thisWidget.state.isOpened == nil then
thisWidget.state.isOpened = ImGui._widgetState(thisWidget, "isOpened", true)
end
end,
UpdateState = function(thisWidget: Types.Tab)
local Tab = thisWidget.Instance :: TextButton
local Container = thisWidget.ChildContainer :: Frame
if thisWidget.state.isOpened.value == true then
thisWidget.lastOpenedTick = ImGui._cycleTick + 1
openTab(thisWidget.parentWidget, thisWidget.Index)
Tab.Visible = true
else
thisWidget.lastClosedTick = ImGui._cycleTick + 1
closeTab(thisWidget.parentWidget, thisWidget.Index)
Tab.Visible = false
end
if thisWidget.state.index.value == thisWidget.Index then
thisWidget.ButtonColors.Color = ImGui._config.TabActiveColor
thisWidget.ButtonColors.Transparency = ImGui._config.TabActiveTransparency
Tab.BackgroundColor3 = ImGui._config.TabActiveColor
Tab.BackgroundTransparency = ImGui._config.TabActiveTransparency
Container.Visible = true
thisWidget.lastSelectedTick = ImGui._cycleTick + 1
else
thisWidget.ButtonColors.Color = ImGui._config.TabColor
thisWidget.ButtonColors.Transparency = ImGui._config.TabTransparency
Tab.BackgroundColor3 = ImGui._config.TabColor
Tab.BackgroundTransparency = ImGui._config.TabTransparency
Container.Visible = false
thisWidget.lastUnselectedTick = ImGui._cycleTick + 1
end
end,
Discard = function(thisWidget: Types.Tab)
thisWidget.Instance:Destroy()
end
} :: Types.WidgetClass)
end
}
for _, Element in widgets.ELEMENTS do
Element(ImGui, widgets)
end
end
Widgets(Internal)
local function API(ImGui: ImGui)
local function wrapper(name: string)
return function(arguments: WidgetArguments?, states: WidgetStates?): Widget
return ImGui.Internal._Insert(name, arguments, states)
end
end
ImGui.Window = wrapper("Window")
ImGui.SetFocusedWindow = ImGui.Internal.SetFocusedWindow
ImGui.Tooltip = wrapper("Tooltip")
ImGui.MenuBar = wrapper("MenuBar")
ImGui.Menu = wrapper("Menu")
ImGui.MenuItem = wrapper("MenuItem")
ImGui.MenuToggle = wrapper("MenuToggle")
ImGui.Separator = wrapper("Separator")
ImGui.Indent = wrapper("Indent")
ImGui.SameLine = wrapper("SameLine")
ImGui.Group = wrapper("Group")
ImGui.Text = wrapper("Text")
ImGui.TabBar = wrapper("TabBar")
ImGui.Tab = wrapper("Tab")
ImGui.ScriptEditor = wrapper("ScriptEditor")
ImGui.TextWrapped = function(arguments: WidgetArguments): Text
arguments[2] = true
return ImGui.Internal._Insert("Text", arguments) :: Text
end
ImGui.TextColored = function(arguments: WidgetArguments): Text
arguments[3] = arguments[2]
arguments[2] = nil
return ImGui.Internal._Insert("Text", arguments) :: Text
end
ImGui.SeparatorText = wrapper("SeparatorText")
ImGui.InputText = wrapper("InputText")
ImGui.Button = wrapper("Button")
ImGui.SmallButton = wrapper("SmallButton")
ImGui.Checkbox = wrapper("Checkbox")
ImGui.RadioButton = wrapper("RadioButton")
ImGui.Image = wrapper("Image")
ImGui.ImageButton = wrapper("ImageButton")
ImGui.Tree = wrapper("Tree")
ImGui.CollapsingHeader = wrapper("CollapsingHeader")
ImGui.InputNum = wrapper("InputNum")
ImGui.InputVector2 = wrapper("InputVector2")
ImGui.InputVector3 = wrapper("InputVector3")
ImGui.InputUDim = wrapper("InputUDim")
ImGui.InputUDim2 = wrapper("InputUDim2")
ImGui.InputRect = wrapper("InputRect")
ImGui.DragNum = wrapper("DragNum")
ImGui.DragVector2 = wrapper("DragVector2")
ImGui.DragVector3 = wrapper("DragVector3")
ImGui.DragUDim = wrapper("DragUDim")
ImGui.DragUDim2 = wrapper("DragUDim2")
ImGui.DragRect = wrapper("DragRect")
ImGui.InputColor3 = wrapper("InputColor3")
ImGui.InputColor4 = wrapper("InputColor4")
ImGui.SliderNum = wrapper("SliderNum")
ImGui.SliderVector2 = wrapper("SliderVector2")
ImGui.SliderVector3 = wrapper("SliderVector3")
ImGui.SliderUDim = wrapper("SliderUDim")
ImGui.SliderUDim2 = wrapper("SliderUDim2")
ImGui.SliderRect = wrapper("SliderRect")
ImGui.Selectable = wrapper("Selectable")
ImGui.Combo = wrapper("Combo")
ImGui.ComboArray = function(arguments: WidgetArguments, states: WidgetStates?, selectionArray: { T })
local defaultState
if states == nil then
defaultState = ImGui.State(selectionArray[1])
else
defaultState = states
end
local thisWidget = ImGui.Internal._Insert("Combo", arguments, defaultState)
local sharedIndex: State = thisWidget.state.index
for _, Selection in selectionArray do
ImGui.Internal._Insert("Selectable", { Selection, Selection }, { index = sharedIndex } :: States)
end
ImGui.End()
return thisWidget
end
ImGui.ComboEnum = function(arguments: WidgetArguments, states: WidgetStates?, enumType: Enum)
local defaultState
if states == nil then
defaultState = ImGui.State(enumType:GetEnumItems()[1])
else
defaultState = states
end
local thisWidget = ImGui.Internal._Insert("Combo", arguments, defaultState)
local sharedIndex = thisWidget.state.index
for _, Selection in enumType:GetEnumItems() do
ImGui.Internal._Insert("Selectable", { Selection.Name, Selection }, { index = sharedIndex } :: States)
end
ImGui.End()
return thisWidget
end
ImGui.InputEnum = ImGui.ComboEnum
ImGui.ProgressBar = wrapper("ProgressBar")
ImGui.Table = wrapper("Table")
ImGui.NextColumn = function()
local parentWidget = ImGui.Internal._GetParentWidget() :: Table
assert(parentWidget.type == "Table", "ImGui.NextColumn can only be called within a table.")
parentWidget.RowColumnIndex += 1
end
ImGui.SetColumnIndex = function(columnIndex: number)
local parentWidget = ImGui.Internal._GetParentWidget() :: Table
assert(parentWidget.type == "Table", "ImGui.SetColumnIndex can only be called within a table.")
assert(columnIndex >= parentWidget.InitialNumColumns, "ImGui.SetColumnIndex Argument must be in column range")
parentWidget.RowColumnIndex = math.floor(parentWidget.RowColumnIndex / parentWidget.InitialNumColumns) + (columnIndex - 1)
end
ImGui.NextRow = function()
local parentWidget = ImGui.Internal._GetParentWidget() :: Table
assert(parentWidget.type == "Table", "ImGui.NextColumn can only be called within a table.")
local InitialNumColumns: number = parentWidget.InitialNumColumns
local nextRow: number = math.floor((parentWidget.RowColumnIndex + 1) / InitialNumColumns) * InitialNumColumns
parentWidget.RowColumnIndex = nextRow
end
ImGui.RenderMarkup = function(element)
local elementType = element[1]
local args = element[2] or {}
local children = element[3] or {}
local events = element[4] or {}
local Element = ImGui[elementType](unpack(args))
if #children > 0 then
for _, child in ipairs(children) do
if type(child) == "table" then
ImGui.RenderMarkup(child)
end
end
ImGui.End()
end
for event, callback in pairs(events) do
if Element[event]() then
callback()
end
end
end
end
API(ImGui)
return ImGui.Init()