From 077ab9de4e9c508a66e660feb6b8a01dc73f4316 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:10:44 +0700 Subject: [PATCH 01/22] Initial DebuggerUI --- src/DebuggerUI/Client/IrisLocal.client.luau | 570 ++++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 src/DebuggerUI/Client/IrisLocal.client.luau diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau new file mode 100644 index 0000000..f13bc4e --- /dev/null +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -0,0 +1,570 @@ +--[[ + - Author: Mawin_CK + - Date: 2025 +]] + +-- TODO: Add blockcast support + +-- Services +local Rep = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") +local RepFirst = game:GetService("ReplicatedFirst") +local UIS = game:GetService("UserInputService") + +-- Modules +local FastCast2 = Rep:WaitForChild("FastCast2") + +-- Requires +local iris = require(Rep:WaitForChild("iris")) +local FastCastM = require(FastCast2) +local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) +local Jolt = require(Rep:WaitForChild("Jolt")) +local Signal = require(FastCast2:WaitForChild("Signal")) + +-- Types +type RequestLogDataType = "FastCastBehavior" | "Setting" + +-- Variables +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() + +--local TargetPartOrigin: BasePart = nil +--local TargetOrigin: Vector3 = Vector3.new() + +--local TargetDirection = Vector3.new() + +local ProjectileContainer = workspace:WaitForChild('Projectiles') +local ProjectileTemplate = Rep:WaitForChild("Projectile") + +local ClientProjectileCount = 0 +local ServerProjectileCount = 0 + + +local HighFidelityBehaviorName = { + [1] = "Default", + [2] = "Automatic", + [3] = "Always" +} + +local TestModules = {} +for _, value in Rep:WaitForChild("Tests"):GetChildren() do + TestModules[value.Name] = require(value) +end + +local FastCastEventsModules = {} +for _, value in Rep:WaitForChild("FastCastEventsModules"):GetChildren() do + FastCastEventsModules[value.Name] = value +end + +local debounce_lc = false +local debounce_lc_time = 1.5 + +-- CastParams +local CastParams = RaycastParams.new() +CastParams.FilterDescendantsInstances = {character} +CastParams.FilterType = Enum.RaycastFilterType.Exclude +CastParams.IgnoreWater = true + +-- Behavior +local CastBehaviorClient: FastCastTypes.FastCastBehavior = FastCastM.newBehavior() +CastBehaviorClient.RaycastParams = CastParams +CastBehaviorClient.VisualizeCasts = false +CastBehaviorClient.Acceleration = Vector3.new() +CastBehaviorClient.AutoIgnoreContainer = true +CastBehaviorClient.MaxDistance = 1000 +CastBehaviorClient.HighFidelitySegmentSize = 1 + +CastBehaviorClient.CosmeticBulletContainer = ProjectileContainer +CastBehaviorClient.CosmeticBulletTemplate = ProjectileTemplate + +CastBehaviorClient.FastCastEventsConfig = { + UseLengthChanged = false, + UseHit = false, + UseCastTerminating = true, + UseCastFire = false, + UsePierced = false +} + +CastBehaviorClient.FastCastEventsModuleConfig = { + UseLengthChanged = false, + UseHit = false, + UseCastTerminating = true, + UseCastFire = false, + UsePierced = false, + UseCanPierce = false +} + +CastBehaviorClient.SimulateAfterPhysic = true + +-- Events +local CastBehaviorServerUpdate = Jolt.Client("CastBehaviorServerUpdate") :: Jolt.Client +local LoggingServer = Jolt.Client("LoggingServer") :: Jolt.Client +local ServerSettingUpdate = Jolt.Client("ServerSettingUpdate") :: Jolt.Client<{velocity: number, ProjectileLimit: number}> +local ServerProjectile = Jolt.Client("ServerProjectile") :: Jolt.Client +local ServerProjectileCountEvent = Jolt.Client("ServerProjectileCount") :: Jolt.Client +local ServerCastModuleUpdate = Jolt.Client("ServerCastModuleUpdate") :: Jolt.Client + +-- Caster +local Caster = FastCastM.new() +Caster:Init( + 4, + RepFirst, + "CastVMs", + RepFirst, + "VMContainer", + "CastVM", + true +) + +-- CONSTANTS +local DEFAULT_CACHE_SIZE = 1000 + +-- States + +local WindowSize = iris.State(Vector2.new(500, 400)) +local WindowVisible = iris.State(true) + +local SelectedBehavior = iris.State(CastBehaviorClient.HighFidelityBehavior) +local AcclerationState = iris.State(CastBehaviorClient.Acceleration) +local AutoIgnoreContainerState = iris.State(CastBehaviorClient.AutoIgnoreContainer) +local MaxDistanceState = iris.State(CastBehaviorClient.MaxDistance) +local VisualizeCastsState = iris.State(CastBehaviorClient.VisualizeCasts) +local HighFidelitySegmentSizeState = iris.State(CastBehaviorClient.HighFidelitySegmentSize) +local UseCosmeticBulletTemplate = iris.State(true) +local SimulateAfterPhysicState = iris.State(CastBehaviorClient.SimulateAfterPhysic) + +local OriginValue = iris.State(Vector3.new(0, 5, 0)) +local DirectionValue = iris.State(Vector3.new(0,0,-1000)) +local VelocityValue = iris.State(50) +local ClientProjectileLimitValue = iris.State(1000) +local P_ClientTestValue = iris.State(false) +local P_ServerTestValue = iris.State(false) +local FastCastEventsConfigStates = {} +local FastCastEventsModuleConfigStates = {} + +local CastTypeState = iris.State("Raycast") + +local CastTypes = { + "Raycast", + "Blockcast", + "Spherecast" +} + +local P_TestValue = iris.State(false) + +for key, value in CastBehaviorClient.FastCastEventsConfig do + FastCastEventsConfigStates[key] = iris.State(value) +end + +for key, value in CastBehaviorClient.FastCastEventsModuleConfig do + FastCastEventsModuleConfigStates[key] = iris.State(value) +end + +local AutomaticPerformanceState = iris.State(true) +local AdaptivePerformanceStates = {} +for key, value in CastBehaviorClient.AdaptivePerformance do + AdaptivePerformanceStates[key] = iris.State(value) +end + +local VisualizeCastSettingSt = {} +for key, value in CastBehaviorClient.VisualizeCastSettings do + VisualizeCastSettingSt[key] = iris.State(value) +end + +local ObjectEnabledValue = iris.State(Caster.ObjectCacheEnabled) +local ObjectCacheSizeValue = iris.State(DEFAULT_CACHE_SIZE) +local BulkMoveEnabledValue = iris.State(Caster.BulkMoveEnabled) + +local SelectedTests = {} +for key, _ in TestModules do + SelectedTests[key] = iris.State(false) +end + +local SelcetedModule = iris.State(nil) + +-- Local functions + +--[[local function Format(Time: number): string + if Time < 1E-6 then + return `{Time * 1E+9} ns` + elseif Time < 0.001 then + return `{Time * 1E+6} μs` + elseif Time < 1 then + return `{Time * 1000} ms` + else + return `{Time} s` + end +end]] + +local function UpdateClientBehavior() + CastBehaviorClient.HighFidelityBehavior = SelectedBehavior.value + CastBehaviorClient.Acceleration = AcclerationState.value + CastBehaviorClient.AutoIgnoreContainer = AutoIgnoreContainerState.value + CastBehaviorClient.MaxDistance = MaxDistanceState.value + CastBehaviorClient.VisualizeCasts = VisualizeCastsState.value + CastBehaviorClient.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value + CastBehaviorClient.CosmeticBulletTemplate = UseCosmeticBulletTemplate.value == true and ProjectileTemplate or nil + CastBehaviorClient.SimulateAfterPhysic = SimulateAfterPhysicState.value + CastBehaviorClient.AutomaticPerformance = AutomaticPerformanceState.value + for key, v in FastCastEventsConfigStates do + CastBehaviorClient.FastCastEventsConfig[key] = v.value + end + for key, v in FastCastEventsModuleConfigStates do + CastBehaviorClient.FastCastEventsModuleConfig[key] = v.value + end + for key, v in AdaptivePerformanceStates do + CastBehaviorClient.AdaptivePerformance[key] = v.value + end + for key, v in VisualizeCastSettingSt do + CastBehaviorClient.VisualizeCastSettings[key] = v.value + end +end + +local function UpdateServerBehavior() + local newBehavior = FastCastM.newBehavior() + newBehavior.HighFidelityBehavior = SelectedBehavior.value + newBehavior.Acceleration = AcclerationState.value + newBehavior.AutoIgnoreContainer = AutoIgnoreContainerState.value + newBehavior.MaxDistance = MaxDistanceState.value + newBehavior.VisualizeCasts = VisualizeCastsState.value + newBehavior.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value + newBehavior.SimulateAfterPhysic = SimulateAfterPhysicState.value + newBehavior.AutomaticPerformance = AutomaticPerformanceState.value + for key, v in FastCastEventsConfigStates do + newBehavior.FastCastEventsConfig[key] = v.value + end + for key, v in FastCastEventsModuleConfigStates do + newBehavior.FastCastEventsModuleConfig[key] = v.value + end + for key, v in AdaptivePerformanceStates do + newBehavior.AdaptivePerformance[key] = v.value + end + for key, v in VisualizeCastSettingSt do + newBehavior.VisualizeCastSettings[key] = v.value + end + CastBehaviorServerUpdate:Fire(newBehavior) +end + +local function IntCount(amount: number) + ClientProjectileCount += amount +end + +local function OnCastTerminating(cast: FastCastTypes.ActiveCastCompement) + local obj = cast.RayInfo.CosmeticBulletObject + if obj then + obj:Destroy() + end + IntCount(-1) +end + +local function OnCastTerminating_ObjectCache(cast: FastCastTypes.ActiveCastCompement) + local obj = cast.RayInfo.CosmeticBulletObject + if obj then + Caster.ObjectCache:ReturnObject(obj) + end + IntCount(-1) +end + +local function CasterFire( + targetType: "Raycast" | "Blockcast" | "Spherecast", + origin: Vector3, + arg: any?, + direction: Vector3, + velocity: Vector3 | number, + behavior: FastCastTypes.FastCastBehavior? +) + --print(targetType, origin, arg, direction, velocity) + if targetType == "Raycast" then + Caster:RaycastFire(origin, direction, velocity, behavior) + elseif targetType == "Blockcast" then + Caster:BlockcastFire(origin, arg, direction, velocity, behavior) + elseif targetType == "Spherecast" then + Caster:SpherecastFire(origin, arg, direction, velocity, behavior) + end +end + +local BlockSizeState = iris.State(Vector3.new(1,1,1)) +local SphereRadiusState = iris.State(1) + +-- Init +iris.Init() +CastBehaviorServerUpdate:Fire(CastBehaviorClient) +ServerSettingUpdate:Fire({ + velocity = VelocityValue.value, + projectileLimit = ClientProjectileLimitValue.value +}) + +local IntCountEvent = Signal.new() +IntCountEvent:Connect(function(amount: number) + ClientProjectileCount += amount +end) + +Caster.CastTerminating = OnCastTerminating +Caster.CastFire = function() + print("CastFire Test!") +end +Caster.Hit = function() + print("Hit Test!") +end +Caster.LengthChanged = function() + if not debounce_lc then + debounce_lc = true + print("OnLengthChanged Test!") + task.delay(debounce_lc_time, function() + debounce_lc = false + end) + end +end +Caster.Pierced = function() + print("pierced Test!") +end + +-- iris +iris:Connect(function() + iris.Window({"FastCast2 TestGUI"},{size=WindowSize, isOpened=WindowVisible}) + iris.Tree("Setting") + iris.InputNum({"Velocity"}, {number=VelocityValue}) + iris.InputNum({"Projectile limit"}, {number=ClientProjectileLimitValue}) + iris.Tree("CastType") + for k,v in CastTypes do + iris.RadioButton( + { v, v }, + {index = CastTypeState}, + k + ) + end + iris.End() + + local arg = nil + + if CastTypeState.value == "Blockcast" then + local input = iris.InputVector3({"BlockcastSize"}, {number = BlockSizeState}) + if input.numberChanged() then + arg = BlockSizeState.value + end + end + + if CastTypeState.value == "Spherecast" then + local input = iris.InputNum({"SpherecastRadius"}, {number = SphereRadiusState}) + if input.numberChanged() then + arg = SphereRadiusState.value + end + end + if iris.Button("Update Server").clicked() then + ServerSettingUpdate:Fire({ + velocity = VelocityValue.value, + projectileLimit = ClientProjectileLimitValue.value, + castType = CastTypeState.value, + arg = arg + }) + end + iris.End() + + -- FastCastBehavior + iris.Tree({"FastCastBehavior"}) + iris.Tree("HighFidelityBehavior") + for i = 1, 3 do + iris.RadioButton( + { HighFidelityBehaviorName[i], i }, + {index = SelectedBehavior}, + i + ) + end + iris.End() + + iris.InputVector3({"Acceleration"}, {number = AcclerationState}) + iris.Checkbox({"AutoIgnoreContainer"}, {isChecked = AutoIgnoreContainerState}) + iris.InputNum({"MaxDistance"}, {number = MaxDistanceState}) + iris.Checkbox({"VisualizeCasts"}, {isChecked = VisualizeCastsState}) + iris.InputNum({"HighFidelitySegmentSize", 0.1, 0.1, 2}, {number = HighFidelitySegmentSizeState}) + iris.Checkbox({"Use CosmeticBulletTemplate"}, {isChecked = UseCosmeticBulletTemplate}) + iris.Checkbox({"SimulateAfterPhysic"}, {isChecked=SimulateAfterPhysicState}) + + iris.Tree("FastCastEventsConfig") + for key, state in FastCastEventsConfigStates do + iris.Checkbox({key}, {isChecked = state}) + end + iris.End() + + iris.Tree("FastCastEventsModuleConfig") + for key, state in FastCastEventsModuleConfigStates do + iris.Checkbox({key}, {isChecked = state}) + end + iris.End() + + iris.Checkbox({"Automatic performance"}, {isChecked=AutomaticPerformanceState}) + + iris.Tree("Adaptive performance") + iris.InputNum({"HighFidelitySegmentSizeIncrease", 0.1, 0.1, 2}, {number=AdaptivePerformanceStates.HighFidelitySegmentSizeIncrease}) + iris.Checkbox({"LowerHighFidelityBehavior"}, {isChecked=AdaptivePerformanceStates.LowerHighFidelityBehavior}) + iris.End() + + iris.Tree("VisualizeCastSettings") + iris.Text({"Segment"}) + + iris.InputColor3({"Debug_SegmentColor"}, {number=VisualizeCastSettingSt.Debug_SegmentColor}) + iris.InputNum({"Debug_SegmentTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_SegmentTransparency}) + iris.InputNum({"Debug_SegmentSize", 0.05, 0.05, 2}, {number=VisualizeCastSettingSt.Debug_SegmentSize}) + + iris.Text({"Hit"}) + + iris.InputColor3({"Debug_HitColor"}, {number=VisualizeCastSettingSt.Debug_HitColor}) + iris.InputNum({"Debug_HitTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_HitTransparency}) + iris.InputNum({"Debug_HitSize", 0.5, 0.5, 5}, {number=VisualizeCastSettingSt.Debug_HitSize}) + + iris.Text({"Raypierce"}) + + iris.InputColor3({"Debug_RayPierceColor"}, {number=VisualizeCastSettingSt.Debug_RayPierceColor}) + iris.InputNum({"Debug_RayPierceTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_RayPierceTransparency}) + iris.InputNum({"Debug_RayPierceSize", 0.5, 0.5, 5}, {number=VisualizeCastSettingSt.Debug_RayPierceSize}) + + iris.Text({"Lifetime"}) + iris.InputNum({"Debug_RayLifetime"}, {number=VisualizeCastSettingSt.Debug_RayLifetime}) + iris.InputNum({"Debug_HitLifetime"}, {number=VisualizeCastSettingSt.Debug_HitLifetime}) + iris.End() + + if iris.Button("Update Client").clicked() then + print("Updated FastCastBehaviorClient") + UpdateClientBehavior() + end + + if iris.Button("Update Server").clicked() then + print("Updated FastCastBehaviorServer") + UpdateServerBehavior() + end + + iris.End() + + iris.Tree({"Caster"}) + do + iris.Checkbox({"ObjectCache Enabled"}, {isChecked = ObjectEnabledValue}) + if ObjectEnabledValue.value == true then + if not Caster.ObjectCacheEnabled then + Caster:SetObjectCacheEnabled(true, ProjectileTemplate, DEFAULT_CACHE_SIZE, ProjectileContainer) + Caster.CastTerminating = OnCastTerminating_ObjectCache + end + else + if Caster.ObjectCacheEnabled then + Caster:SetObjectCacheEnabled(false) + Caster.CastTerminating = OnCastTerminating + end + end + + iris.Checkbox({"BulkMoveTo"}, {isChecked = BulkMoveEnabledValue}) + if BulkMoveEnabledValue.value == true then + if not Caster.BulkMoveEnabled then + Caster:SetBulkMoveEnabled(true) + end + else + if Caster.BulkMoveEnabled then + Caster:SetBulkMoveEnabled(false) + end + end + end + iris.End() + + iris.Tree({"Logging"}) + if iris.Button({"FastCastBehavior Client"}).clicked() then + print(CastBehaviorClient) + end + + if iris.Button({"FastCastBehavior Server"}).clicked() then + LoggingServer:Fire("FastCastBehavior") + end + + if iris.Button({"Server Setting"}).clicked() then + LoggingServer:Fire("Setting") + end + + iris.End() + + iris.Tree("Performance Test") + iris.InputVector3({"Origin"}, {number=OriginValue}) + iris.InputVector3({"Direction"}, {number=DirectionValue}) + do + iris.Checkbox({"Client Test"}, {isChecked=P_ClientTestValue}) + iris.Checkbox({"Server Test"}, {isChecked=P_ServerTestValue}) + + iris.Checkbox({"Fire Projectiles"}, {isChecked=P_TestValue}) + + if P_TestValue.value == true then + if ClientProjectileCount < ClientProjectileLimitValue.value and P_ClientTestValue.value == true then + CasterFire(CastTypeState.value, OriginValue.value, arg, DirectionValue.value, VelocityValue.value, CastBehaviorClient) + IntCount(1) + end + + if P_ServerTestValue.value == true then + ServerProjectile:FireUnreliable(OriginValue.value, DirectionValue.value) + end + end + end + + iris.End() + + iris.Tree("FastCastEventsModule") + --[[ + S1 = nil + + A1 + A2 + ]] + for key, moduleScript in FastCastEventsModules do + local isThisChecked = (SelcetedModule == key) + + local checkbox = iris.Checkbox({key}, {isChecked = isThisChecked}) + if checkbox.checked() then + SelcetedModule = key + Caster:SetFastCastEventsModule(moduleScript) + end + + if checkbox.unchecked() then + SelcetedModule = nil + Caster:SetFastCastEventsModule(nil) + end + end + + if iris.Button("Update Server").clicked() then + ServerCastModuleUpdate:Fire(Caster.FastCastEventsModule) + end + iris.End() + + iris.Tree("TestModules") + for key, state in SelectedTests do + local checkbox = iris.Checkbox({key}, {isChecked = state}) + + if checkbox.checked() then + TestModules[key].Start(IntCountEvent, Caster, VelocityValue.value, CastBehaviorClient) + end + + if checkbox.unchecked() then + TestModules[key].Stop() + end + end + + if iris.Button("Update TestModules").clicked() then + for key, state in SelectedTests do + if state.value == true then + TestModules[key].Update(Caster, VelocityValue.value, CastBehaviorClient) + end + end + end + iris.End() + + iris.Text({"Client projectile count : " .. ClientProjectileCount}) + iris.Text({"Server projectile count : " .. ServerProjectileCount}) + + iris.End() +end) + +game:GetService("UserInputService").InputEnded:Connect(function(input: InputObject, gp: boolean) + if gp then return end + if input.KeyCode == Enum.KeyCode.E then + WindowVisible:set(not WindowVisible:get()) + end +end) + +-- Event Connections + +ServerProjectileCountEvent:Connect(function(newCount: number) + ServerProjectileCount = newCount +end) \ No newline at end of file From b9f06b2776ba042ce299fbf2ace9fa1653696737 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:14:56 +0700 Subject: [PATCH 02/22] Add Jolt external lib --- .../Shared/External/Jolt/Bridge.luau | 140 ++++ .../Shared/External/Jolt/Client.luau | 226 +++++++ .../Shared/External/Jolt/Server.luau | 215 ++++++ .../Shared/External/Jolt/Utils/Buffers.luau | 625 ++++++++++++++++++ .../Shared/External/Jolt/Utils/Remotes.luau | 62 ++ src/DebuggerUI/Shared/External/Jolt/init.luau | 48 ++ 6 files changed, 1316 insertions(+) create mode 100644 src/DebuggerUI/Shared/External/Jolt/Bridge.luau create mode 100644 src/DebuggerUI/Shared/External/Jolt/Client.luau create mode 100644 src/DebuggerUI/Shared/External/Jolt/Server.luau create mode 100644 src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau create mode 100644 src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau create mode 100644 src/DebuggerUI/Shared/External/Jolt/init.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Bridge.luau b/src/DebuggerUI/Shared/External/Jolt/Bridge.luau new file mode 100644 index 0000000..f568047 --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Bridge.luau @@ -0,0 +1,140 @@ +--!strict +local RunService = game:GetService("RunService") +local Players = game:GetService("Players") + +local Buffers = require("./Utils/Buffers") +local Remotes = require("./Utils/Remotes") + +local Bridge = {} + +local IS_SERVER = RunService:IsServer() +local FRAME_TIME = 1 / 60 + +type WriterMap = {[Player | string]: Buffers.Writer} + +local ReliableMap: WriterMap = {} +local UnreliableMap: WriterMap = {} + +local BroadcastReliable: Buffers.Writer? = nil +local BroadcastUnreliable: Buffers.Writer? = nil + +local reliableRemote: RemoteEvent +local unreliableRemote: UnreliableRemoteEvent + +local accumulator = 0 +local initialized = false +local SERVER_KEY = "SERVER" + +function Bridge.Initialize() + if initialized then return end + initialized = true + + local group = if IS_SERVER then Remotes.Create() else Remotes.Get() + reliableRemote = group.Reliable + unreliableRemote = group.Unreliable + + RunService.Heartbeat:Connect(function(dt) + accumulator += dt + if accumulator >= FRAME_TIME then + local flushed = false + while accumulator >= FRAME_TIME do + accumulator -= FRAME_TIME + flushed = true + end + if flushed then Bridge.FlushAll() end + end + end) + + if IS_SERVER then + Players.PlayerRemoving:Connect(function(player) + local w = ReliableMap[player] + if w then Buffers.FreeWriter(w); ReliableMap[player] = nil end + local uw = UnreliableMap[player] + if uw then Buffers.FreeWriter(uw); UnreliableMap[player] = nil end + end) + end +end + +function Bridge.FlushAll() + if not initialized then return end + + if IS_SERVER then + if BroadcastReliable then + local w = BroadcastReliable + BroadcastReliable = nil + local b, i = Buffers.Finalize(w) + reliableRemote:FireAllClients(b, i) + Buffers.FreeWriter(w) + end + + if BroadcastUnreliable then + local w = BroadcastUnreliable + BroadcastUnreliable = nil + local b, i = Buffers.Finalize(w) + unreliableRemote:FireAllClients(b, i) + Buffers.FreeWriter(w) + end + + for player, w in pairs(ReliableMap) do + ReliableMap[player] = nil + local b, i = Buffers.Finalize(w) + reliableRemote:FireClient(player :: Player, b, i) + Buffers.FreeWriter(w) + end + + for player, w in pairs(UnreliableMap) do + UnreliableMap[player] = nil + local b, i = Buffers.Finalize(w) + unreliableRemote:FireClient(player :: Player, b, i) + Buffers.FreeWriter(w) + end + else + local w = ReliableMap[SERVER_KEY] + if w then + ReliableMap[SERVER_KEY] = nil + local b, i = Buffers.Finalize(w) + reliableRemote:FireServer(b, i) + Buffers.FreeWriter(w) + end + + local uw = UnreliableMap[SERVER_KEY] + if uw then + UnreliableMap[SERVER_KEY] = nil + local b, i = Buffers.Finalize(uw) + unreliableRemote:FireServer(b, i) + Buffers.FreeWriter(uw) + end + end +end + +function Bridge.Writer(reliable: boolean, player: Player?): Buffers.Writer + if IS_SERVER then + if player then + local map = if reliable then ReliableMap else UnreliableMap + local w = map[player] + if not w then + w = Buffers.CreateWriter() + map[player] = w + end + return w + else + if reliable then + if not BroadcastReliable then BroadcastReliable = Buffers.CreateWriter() end + return BroadcastReliable + else + if not BroadcastUnreliable then BroadcastUnreliable = Buffers.CreateWriter() end + return BroadcastUnreliable + end + end + else + local map = if reliable then ReliableMap else UnreliableMap + local w = map[SERVER_KEY] + if not w then + w = Buffers.CreateWriter() + map[SERVER_KEY] = w + end + return w + end +end + +return Bridge \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/Client.luau b/src/DebuggerUI/Shared/External/Jolt/Client.luau new file mode 100644 index 0000000..5b51db2 --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Client.luau @@ -0,0 +1,226 @@ +--!strict +local Client = {} +Client.__index = Client + +local Buffers = require("./Utils/Buffers") +local Remotes = require("./Utils/Remotes") +local Bridge = require("./Bridge") + +local PACKET_EVENT = 1 +local PACKET_REQUEST = 2 +local PACKET_RESPONSE = 3 +local REQUEST_TIMEOUT = 60 + +local task_spawn = task.spawn +local task_delay = task.delay +local task_cancel = task.cancel +local coroutine_running = coroutine.running +local coroutine_yield = coroutine.yield +local table_unpack = table.unpack + +local GlobalInitialized = false +type Listener = { + __type: "Connect", + Callback: (...any) -> (), +} | { + __type: "Once", + Callback: (...any) -> (), +} | { + __type: "Wait", + Thread: thread, +} +local Listeners = {} :: {[string]: {Listener}} +local Requests = {} :: {[string]: {[number]: {thread: thread, timer: thread}}} +local RequestIdCounters = {} :: {[string]: number} + +local reliable: RemoteEvent +local unreliable: UnreliableRemoteEvent + +local function Initialize() + if GlobalInitialized then return end + GlobalInitialized = true + + Bridge.Initialize() + local group = Remotes.Get() + reliable = group.Reliable + unreliable = group.Unreliable + + local function dispatch(id: string, args: {any}, count: number) + local type = args[1] + + if type == PACKET_EVENT then + local listeners = Listeners[id] + if not listeners then + return + end + + for i = #listeners, 1, -1 do + local listener = listeners[i] + if listener then + if listener.__type == "Connect" then + task_spawn(listener.Callback, table_unpack(args, 2, count)) + elseif listener.__type == "Once" then + task_spawn(listener.Callback, table_unpack(args, 2, count)) + table.remove(listeners, i) + elseif listener.__type == "Wait" then + task_spawn(listener.Thread, table_unpack(args, 2, count)) + table.remove(listeners, i) + end + end + end + elseif type == PACKET_RESPONSE then + local reqId = args[2] + local ok = args[3] == true or args[3] == 1 + + local channelRequests = Requests[id] + if channelRequests then + local req = channelRequests[reqId] + if req then + channelRequests[reqId] = nil + task_cancel(req.timer) + task_spawn(req.thread, ok, table_unpack(args, 4, count)) + end + end + end + end + + local function onEvent(b: buffer, i: {Instance}) + local r = Buffers.CreateReader(b, i) + + while r.cursor < r.len do + local id = Buffers.ReadString(r) + + local type, argCount = Buffers.ReadPacketHeader(r) + + local args = table.create(argCount + 1) + args[1] = type + for k = 1, argCount do + args[k + 1] = Buffers.ReadAny(r) + end + + dispatch(id, args, argCount + 1) + end + + Buffers.FreeReader(r) + end + + reliable.OnClientEvent:Connect(onEvent) + unreliable.OnClientEvent:Connect(onEvent) +end + +function Client.new(name: string) + if not GlobalInitialized then + Initialize() + end + + local self = setmetatable({}, Client) + self._id = name + + if not Requests[self._id] then + Requests[self._id] = {} + RequestIdCounters[self._id] = 0 + end + + return self +end + +function Client:Connect(callback: (...any) -> ()) + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + local newListener: Listener = { + __type = "Connect", + Callback = callback, + } + table.insert(Listeners[self._id], newListener) + + return { + Disconnect = function() + if not Listeners[self._id] then + return + end + for i, listener in ipairs(Listeners[self._id]) do + if listener == newListener then + table.remove(Listeners[self._id], i) + break + end + end + end, + } +end + +function Client:Once(callback: (...any) -> ()) + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + table.insert(Listeners[self._id], ({ + __type = "Once", + Callback = callback, + } :: Listener)) +end + +function Client:Wait() + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + local thread = coroutine.running() + table.insert(Listeners[self._id], ({ + __type = "Wait", + Thread = thread, + } :: Listener)) + + return coroutine.yield() +end + +local function Send(self: any, reliable: boolean, packetType: number, ...: any) + local id = self._id + local w = Bridge.Writer(reliable) + + Buffers.WriteString(w, id) + + local n = select("#", ...) + Buffers.WritePacketHeader(w, packetType, n) + + for i = 1, n do + Buffers.WriteAny(w, select(i, ...)) + end +end + +function Client:Fire(...: any) + Send(self, true, PACKET_EVENT, ...) +end + +function Client:FireUnreliable(...: any) + Send(self, false, PACKET_EVENT, ...) +end + +function Client:Invoke(...: any) + local id = RequestIdCounters[self._id] + RequestIdCounters[self._id] += 1 + + Send(self, true, PACKET_REQUEST, id, ...) + + local thread = coroutine_running() + local timer = task_delay(REQUEST_TIMEOUT, function() + if Requests[self._id][id] and Requests[self._id][id].thread == thread then + Requests[self._id][id] = nil + task_spawn(thread, false, "Request timed out") + end + end) + + Requests[self._id][id] = {thread = thread, timer = timer} + + local results = {coroutine_yield()} + local success = results[1] + + if not success then + error(tostring(results[2])) + end + + return table_unpack(results, 2) +end + +return Client \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/Server.luau b/src/DebuggerUI/Shared/External/Jolt/Server.luau new file mode 100644 index 0000000..b579eea --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Server.luau @@ -0,0 +1,215 @@ +--!strict +local Server = {} +Server.__index = Server + +local Buffers = require("./Utils/Buffers") +local Remotes = require("./Utils/Remotes") +local Bridge = require("./Bridge") + +local PACKET_EVENT = 1 +local PACKET_REQUEST = 2 +local PACKET_RESPONSE = 3 + +local task_spawn = task.spawn +local table_remove = table.remove +local table_unpack = table.unpack + +local GlobalInitialized = false +type Listener = { + __type: "Connect", + Callback: (Player, ...any) -> (), +} | { + __type: "Once", + Callback: (Player, ...any) -> (), +} | { + __type: "Wait", + Thread: thread, +} +local Listeners = {} :: {[string]: {Listener}} +local InvokeHandlers = {} :: {[string]: (Player, ...any) -> ...any} + +local reliable: RemoteEvent +local unreliable: UnreliableRemoteEvent + +local function Initialize() + if GlobalInitialized then return end + GlobalInitialized = true + + Bridge.Initialize() + local group = Remotes.Get() + reliable = group.Reliable + unreliable = group.Unreliable + + local function dispatch(player: Player, id: string, args: {any}, count: number) + local type = args[1] + if type == PACKET_EVENT then + local listeners = Listeners[id] + if not listeners then + return + end + + for i = #listeners, 1, -1 do + local listener = listeners[i] + if listener then + if listener.__type == "Connect" then + task_spawn(listener.Callback, player, table_unpack(args, 2, count)) + elseif listener.__type == "Once" then + task_spawn(listener.Callback, player, table_unpack(args, 2, count)) + table.remove(listeners, i) + elseif listener.__type == "Wait" then + task_spawn(listener.Thread, player, table_unpack(args, 2, count)) + table.remove(listeners, i) + end + end + end + elseif type == PACKET_REQUEST then + local reqId = args[2] + local handler = InvokeHandlers[id] + if handler then + task_spawn(function() + local results = {pcall(handler, player, table_unpack(args, 3, count))} + local ok = results[1] + table_remove(results, 1) + + local w = Bridge.Writer(true, player) + Buffers.WriteString(w, id) + + local n = #results + 2 + Buffers.WritePacketHeader(w, PACKET_RESPONSE, n) + + Buffers.WriteAny(w, reqId) + Buffers.WriteAny(w, ok) + + for _, v in ipairs(results) do + Buffers.WriteAny(w, v) + end + end) + end + end + end + + local function onEvent(player: Player, b: buffer, i: {Instance}) + local r = Buffers.CreateReader(b, i) + + while r.cursor < r.len do + local id = Buffers.ReadString(r) + + local type, argCount = Buffers.ReadPacketHeader(r) + + local args = table.create(argCount + 1) + args[1] = type + for k = 1, argCount do + args[k + 1] = Buffers.ReadAny(r) + end + + dispatch(player, id, args, argCount + 1) + end + + Buffers.FreeReader(r) + end + + reliable.OnServerEvent:Connect(onEvent) + unreliable.OnServerEvent:Connect(onEvent) +end + +function Server.new(name: string) + if not GlobalInitialized then + Initialize() + end + + local self = setmetatable({}, Server) + self._id = name + + return self +end + +function Server:Connect(callback: (Player, ...any) -> ()) + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + local newListener: Listener = { + __type = "Connect", + Callback = callback, + } + table.insert(Listeners[self._id], newListener) + + return { + Disconnect = function() + if not Listeners[self._id] then + return + end + for i, listener in ipairs(Listeners[self._id]) do + if listener == newListener then + table.remove(Listeners[self._id], i) + break + end + end + end, + } +end + +function Server:Once(callback: (Player, ...any) -> ()) + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + table.insert(Listeners[self._id], ({ + __type = "Once", + Callback = callback, + } :: Listener)) +end + +function Server:Wait() + if not Listeners[self._id] then + Listeners[self._id] = {} + end + + local thread = coroutine.running() + table.insert(Listeners[self._id], ({ + __type = "Wait", + Thread = thread, + } :: Listener)) + + return coroutine.yield() +end + +local function Send(self: any, reliable: boolean, player: Player?, packetType: number, ...: any) + local id = self._id + local w = Bridge.Writer(reliable, player) + + Buffers.WriteString(w, id) + + local n = select("#", ...) + Buffers.WritePacketHeader(w, packetType, n) + + for i = 1, n do + Buffers.WriteAny(w, select(i, ...)) + end +end + +function Server:Fire(player: Player, ...: any) + Send(self, true, player, PACKET_EVENT, ...) +end + +function Server:FireUnreliable(player: Player, ...: any) + Send(self, false, player, PACKET_EVENT, ...) +end + +function Server:FireAll(...: any) + Send(self, true, nil, PACKET_EVENT, ...) +end + +function Server:FireAllUnreliable(...: any) + Send(self, false, nil, PACKET_EVENT, ...) +end + +function Server:__newindex(k, v) + if k == "OnInvoke" then + InvokeHandlers[self._id] = v + else + rawset(self, k, v) + end +end + +return Server \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau b/src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau new file mode 100644 index 0000000..821910e --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau @@ -0,0 +1,625 @@ +--!strict +--!native +--!optimize 2 + +local Buffers = {} + +local b_create = buffer.create +local b_len = buffer.len +local b_copy = buffer.copy +local b_w_u8 = buffer.writeu8 +local b_r_u8 = buffer.readu8 +local b_w_u16 = buffer.writeu16 +local b_r_u16 = buffer.readu16 +local b_w_u32 = buffer.writeu32 +local b_r_u32 = buffer.readu32 +local b_w_i8 = buffer.writei8 +local b_r_i8 = buffer.readi8 +local b_w_i16 = buffer.writei16 +local b_r_i16 = buffer.readi16 +local b_w_i32 = buffer.writei32 +local b_r_i32 = buffer.readi32 +local b_w_f32 = buffer.writef32 +local b_r_f32 = buffer.readf32 +local b_w_f64 = buffer.writef64 +local b_r_f64 = buffer.readf64 +local b_w_str = buffer.writestring +local b_r_str = buffer.readstring + +local math_floor = math.floor +local math_pi = math.pi + +local t_insert = table.insert +local t_remove = table.remove +local t_clear = table.clear +local t_create = table.create + +local b_lshift = bit32.lshift +local b_rshift = bit32.rshift +local b_bor = bit32.bor +local b_band = bit32.band + +export type Writer = { + buff: buffer, + cursor: number, + len: number, + insts: {Instance} +} + +export type Reader = { + buff: buffer, + cursor: number, + len: number, + insts: {Instance} +} + +local ALLOC_SIZE = 4096 +local POOL_SIZE = 512 +local MAX_REUSE_SIZE = 64 * 1024 + +local TYPE_NIL = 0 +local TYPE_BOOL = 1 +local TYPE_NUMBER = 2 +local TYPE_STRING = 3 +local TYPE_BUFFER = 4 +local TYPE_VECTOR2 = 5 +local TYPE_VECTOR3 = 6 +local TYPE_CFRAME = 7 +local TYPE_COLOR3 = 8 +local TYPE_UDIM = 9 +local TYPE_UDIM2 = 10 +local TYPE_REGION3 = 11 +local TYPE_INSTANCE = 12 +local TYPE_ENUM = 13 +local TYPE_BRICKCOLOR = 14 +local TYPE_TWEENINFO = 15 +local TYPE_TABLE = 16 +local TYPE_INT_U8 = 17 +local TYPE_INT_U16 = 18 +local TYPE_INT_U32 = 19 +local TYPE_INT_I16 = 20 +local TYPE_INT_I32 = 21 + +local WriterPool = t_create(POOL_SIZE) +local ReaderPool = t_create(POOL_SIZE) + +local EnumCache = {} :: {[EnumItem]: string} +local StringToEnumCache = {} :: {[string]: EnumItem} + +local function Resize(w: Writer, needed: number) + local oldLen = w.len + local newLen = oldLen + while w.cursor + needed > newLen do + newLen = newLen * 2 + end + if newLen ~= oldLen then + local newBuff = b_create(newLen) + b_copy(newBuff, 0, w.buff, 0, w.cursor) + w.buff = newBuff + w.len = newLen + end +end + +function Buffers.CreateWriter(capacity: number?): Writer + local w = t_remove(WriterPool) + if w then + return w + end + + capacity = capacity or ALLOC_SIZE + + return { + buff = b_create(capacity :: number), + cursor = 0, + len = capacity :: number, + insts = {} + } +end + +function Buffers.CreateReader(buff: buffer, instances: {Instance}?): Reader + local r = t_remove(ReaderPool) + if r then + r.buff = buff + r.cursor = 0 + r.len = b_len(buff) + r.insts = instances or {} + return r + end + + return { + buff = buff, + cursor = 0, + len = b_len(buff), + insts = instances or {} + } +end + +function Buffers.FreeWriter(w: Writer) + if w.len > MAX_REUSE_SIZE then + return + end + + w.cursor = 0 + t_clear(w.insts) + if #WriterPool < POOL_SIZE then + t_insert(WriterPool, w) + end +end + +function Buffers.FreeReader(r: Reader) + r.insts = {} :: any + if #ReaderPool < POOL_SIZE then + t_insert(ReaderPool, r) + end +end + +function Buffers.Finalize(w: Writer): (buffer, {Instance}) + local b = b_create(w.cursor) + b_copy(b, 0, w.buff, 0, w.cursor) + return b, table.clone(w.insts) +end + +function Buffers.WriteU8(w: Writer, v: number) + if w.cursor + 1 > w.len then Resize(w, 1) end + b_w_u8(w.buff, w.cursor, v) + w.cursor += 1 +end + +function Buffers.ReadU8(r: Reader): number + local v = b_r_u8(r.buff, r.cursor) + r.cursor += 1 + return v +end + +function Buffers.WriteU16(w: Writer, v: number) + if w.cursor + 2 > w.len then Resize(w, 2) end + b_w_u16(w.buff, w.cursor, v) + w.cursor += 2 +end + +function Buffers.ReadU16(r: Reader): number + local v = b_r_u16(r.buff, r.cursor) + r.cursor += 2 + return v +end + +function Buffers.WriteU32(w: Writer, v: number) + if w.cursor + 4 > w.len then Resize(w, 4) end + b_w_u32(w.buff, w.cursor, v) + w.cursor += 4 +end + +function Buffers.ReadU32(r: Reader): number + local v = b_r_u32(r.buff, r.cursor) + r.cursor += 4 + return v +end + +function Buffers.WriteI8(w: Writer, v: number) + if w.cursor + 1 > w.len then Resize(w, 1) end + b_w_i8(w.buff, w.cursor, v) + w.cursor += 1 +end + +function Buffers.ReadI8(r: Reader): number + local v = b_r_i8(r.buff, r.cursor) + r.cursor += 1 + return v +end + +function Buffers.WriteI16(w: Writer, v: number) + if w.cursor + 2 > w.len then Resize(w, 2) end + b_w_i16(w.buff, w.cursor, v) + w.cursor += 2 +end + +function Buffers.ReadI16(r: Reader): number + local v = b_r_i16(r.buff, r.cursor) + r.cursor += 2 + return v +end + +function Buffers.WriteI32(w: Writer, v: number) + if w.cursor + 4 > w.len then Resize(w, 4) end + b_w_i32(w.buff, w.cursor, v) + w.cursor += 4 +end + +function Buffers.ReadI32(r: Reader): number + local v = b_r_i32(r.buff, r.cursor) + r.cursor += 4 + return v +end + +function Buffers.WriteF32(w: Writer, v: number) + if w.cursor + 4 > w.len then Resize(w, 4) end + b_w_f32(w.buff, w.cursor, v) + w.cursor += 4 +end + +function Buffers.ReadF32(r: Reader): number + local v = b_r_f32(r.buff, r.cursor) + r.cursor += 4 + return v +end + +function Buffers.WriteF64(w: Writer, v: number) + if w.cursor + 8 > w.len then Resize(w, 8) end + b_w_f64(w.buff, w.cursor, v) + w.cursor += 8 +end + +function Buffers.ReadF64(r: Reader): number + local v = b_r_f64(r.buff, r.cursor) + r.cursor += 8 + return v +end + +function Buffers.WriteBool(w: Writer, v: boolean) + Buffers.WriteU8(w, v and 1 or 0) +end + +function Buffers.ReadBool(r: Reader): boolean + return Buffers.ReadU8(r) == 1 +end + +function Buffers.WriteVarInt(w: Writer, v: number) + if w.cursor + 5 > w.len then Resize(w, 5) end + while v >= 128 do + b_w_u8(w.buff, w.cursor, b_bor(v, 0x80)) + w.cursor += 1 + v = b_rshift(v, 7) + end + b_w_u8(w.buff, w.cursor, v) + w.cursor += 1 +end + +function Buffers.ReadVarInt(r: Reader): number + local c = 0 + local v = 0 + local b + repeat + b = b_r_u8(r.buff, r.cursor) + r.cursor += 1 + v = b_bor(v, b_lshift(b_band(b, 0x7F), c)) + c += 7 + until b_band(b, 0x80) == 0 + return v +end + +function Buffers.WriteString(w: Writer, v: string) + local len = #v + Buffers.WriteVarInt(w, len) + if w.cursor + len > w.len then Resize(w, len) end + b_w_str(w.buff, w.cursor, v, len) + w.cursor += len +end + +function Buffers.ReadString(r: Reader): string + local len = Buffers.ReadVarInt(r) + local v = b_r_str(r.buff, r.cursor, len) + r.cursor += len + return v +end + +function Buffers.WriteBuffer(w: Writer, v: buffer) + local len = b_len(v) + Buffers.WriteVarInt(w, len) + if w.cursor + len > w.len then Resize(w, len) end + b_copy(w.buff, w.cursor, v, 0, len) + w.cursor += len +end + +function Buffers.ReadBuffer(r: Reader): buffer + local len = Buffers.ReadVarInt(r) + local b = b_create(len) + b_copy(b, 0, r.buff, r.cursor, len) + r.cursor += len + return b +end + +function Buffers.WriteVector2(w: Writer, v: Vector2) + Buffers.WriteF32(w, v.X) + Buffers.WriteF32(w, v.Y) +end + +function Buffers.ReadVector2(r: Reader): Vector2 + return Vector2.new(Buffers.ReadF32(r), Buffers.ReadF32(r)) +end + +function Buffers.WriteVector3(w: Writer, v: Vector3) + Buffers.WriteF32(w, v.X) + Buffers.WriteF32(w, v.Y) + Buffers.WriteF32(w, v.Z) +end + +function Buffers.ReadVector3(r: Reader): Vector3 + return Vector3.new(Buffers.ReadF32(r), Buffers.ReadF32(r), Buffers.ReadF32(r)) +end + +function Buffers.WriteCFrame(w: Writer, v: CFrame) + local axis, angle = v:ToAxisAngle() + Buffers.WriteVector3(w, v.Position) + Buffers.WriteVector3(w, axis) + Buffers.WriteF32(w, angle) +end + +function Buffers.ReadCFrame(r: Reader): CFrame + local pos = Buffers.ReadVector3(r) + local axis = Buffers.ReadVector3(r) + local angle = Buffers.ReadF32(r) + return CFrame.fromAxisAngle(axis, angle) + pos +end + +function Buffers.WriteColor3(w: Writer, v: Color3) + Buffers.WriteU8(w, math_floor(v.R * 255)) + Buffers.WriteU8(w, math_floor(v.G * 255)) + Buffers.WriteU8(w, math_floor(v.B * 255)) +end + +function Buffers.ReadColor3(r: Reader): Color3 + return Color3.fromRGB(Buffers.ReadU8(r), Buffers.ReadU8(r), Buffers.ReadU8(r)) +end + +function Buffers.WriteUDim(w: Writer, v: UDim) + Buffers.WriteF32(w, v.Scale) + Buffers.WriteI32(w, v.Offset) +end + +function Buffers.ReadUDim(r: Reader): UDim + return UDim.new(Buffers.ReadF32(r), Buffers.ReadI32(r)) +end + +function Buffers.WriteUDim2(w: Writer, v: UDim2) + Buffers.WriteUDim(w, v.X) + Buffers.WriteUDim(w, v.Y) +end + +function Buffers.ReadUDim2(r: Reader): UDim2 + return UDim2.new(Buffers.ReadUDim(r), Buffers.ReadUDim(r)) +end + +function Buffers.WriteRegion3(w: Writer, v: Region3) + Buffers.WriteCFrame(w, v.CFrame) + Buffers.WriteVector3(w, v.Size) +end + +function Buffers.ReadRegion3(r: Reader): Region3 + local cf = Buffers.ReadCFrame(r) + local size = Buffers.ReadVector3(r) + return Region3.new(cf.Position - size/2, cf.Position + size/2) +end + +function Buffers.WriteInstance(w: Writer, v: Instance) + t_insert(w.insts, v) + Buffers.WriteVarInt(w, #w.insts) +end + +function Buffers.ReadInstance(r: Reader): Instance? + local idx = Buffers.ReadVarInt(r) + return r.insts[idx] +end + +function Buffers.WriteEnum(w: Writer, v: EnumItem) + local str = EnumCache[v] + if not str then + str = tostring(v) + EnumCache[v] = str + end + Buffers.WriteString(w, str) +end + +function Buffers.ReadEnum(r: Reader): EnumItem? + local str = Buffers.ReadString(r) + local v = StringToEnumCache[str] + if v then return v end + + local parts = string.split(str, ".") + if #parts == 3 and parts[1] == "Enum" then + local enumName = parts[2] + local itemName = parts[3] + + local enum = (Enum :: any)[enumName] + if enum then + local item = enum[itemName] + if item then + StringToEnumCache[str] = item + return item + end + end + end + return nil +end + +function Buffers.WriteBrickColor(w: Writer, v: BrickColor) + Buffers.WriteU16(w, v.Number) +end + +function Buffers.ReadBrickColor(r: Reader): BrickColor + return BrickColor.new(Buffers.ReadU16(r)) +end + +function Buffers.WriteTweenInfo(w: Writer, v: TweenInfo) + Buffers.WriteF64(w, v.Time) + Buffers.WriteEnum(w, v.EasingStyle) + Buffers.WriteEnum(w, v.EasingDirection) + Buffers.WriteF64(w, v.RepeatCount) + Buffers.WriteBool(w, v.Reverses) + Buffers.WriteF64(w, v.DelayTime) +end + +function Buffers.ReadTweenInfo(r: Reader): TweenInfo + local t = Buffers.ReadF64(r) + local es = Buffers.ReadEnum(r) :: Enum.EasingStyle + local ed = Buffers.ReadEnum(r) :: Enum.EasingDirection + local rc = Buffers.ReadF64(r) + local rev = Buffers.ReadBool(r) + local dt = Buffers.ReadF64(r) + return TweenInfo.new(t, es, ed, rc, rev, dt) +end + +function Buffers.WritePacketHeader(w: Writer, packetType: number, n: number) + if n < 63 then + Buffers.WriteU8(w, b_bor(b_lshift(packetType, 6), n)) + else + Buffers.WriteU8(w, b_bor(b_lshift(packetType, 6), 63)) + Buffers.WriteVarInt(w, n) + end +end + +function Buffers.ReadPacketHeader(r: Reader): (number, number) + local h = Buffers.ReadU8(r) + local t = b_rshift(h, 6) + local c = b_band(h, 0x3F) + if c == 63 then c = Buffers.ReadVarInt(r) end + return t, c +end + +function Buffers.WriteAny(w: Writer, v: any) + local t = typeof(v) + + if t == "nil" then + Buffers.WriteU8(w, TYPE_NIL) + elseif t == "boolean" then + Buffers.WriteU8(w, TYPE_BOOL) + Buffers.WriteBool(w, v) + elseif t == "number" then + if v % 1 == 0 then + if v >= 0 then + if v <= 255 then + Buffers.WriteU8(w, TYPE_INT_U8) + Buffers.WriteU8(w, v) + elseif v <= 65535 then + Buffers.WriteU8(w, TYPE_INT_U16) + Buffers.WriteU16(w, v) + elseif v <= 4294967295 then + Buffers.WriteU8(w, TYPE_INT_U32) + Buffers.WriteU32(w, v) + else + Buffers.WriteU8(w, TYPE_NUMBER) + Buffers.WriteF64(w, v) + end + else + if v >= -32768 and v <= 32767 then + Buffers.WriteU8(w, TYPE_INT_I16) + Buffers.WriteI16(w, v) + elseif v >= -2147483648 and v <= 2147483647 then + Buffers.WriteU8(w, TYPE_INT_I32) + Buffers.WriteI32(w, v) + else + Buffers.WriteU8(w, TYPE_NUMBER) + Buffers.WriteF64(w, v) + end + end + else + Buffers.WriteU8(w, TYPE_NUMBER) + Buffers.WriteF64(w, v) + end + elseif t == "string" then + Buffers.WriteU8(w, TYPE_STRING) + Buffers.WriteString(w, v) + elseif t == "buffer" then + Buffers.WriteU8(w, TYPE_BUFFER) + Buffers.WriteBuffer(w, v) + elseif t == "Vector2" then + Buffers.WriteU8(w, TYPE_VECTOR2) + Buffers.WriteVector2(w, v) + elseif t == "Vector3" then + Buffers.WriteU8(w, TYPE_VECTOR3) + Buffers.WriteVector3(w, v) + elseif t == "CFrame" then + Buffers.WriteU8(w, TYPE_CFRAME) + Buffers.WriteCFrame(w, v) + elseif t == "Color3" then + Buffers.WriteU8(w, TYPE_COLOR3) + Buffers.WriteColor3(w, v) + elseif t == "UDim" then + Buffers.WriteU8(w, TYPE_UDIM) + Buffers.WriteUDim(w, v) + elseif t == "UDim2" then + Buffers.WriteU8(w, TYPE_UDIM2) + Buffers.WriteUDim2(w, v) + elseif t == "Region3" then + Buffers.WriteU8(w, TYPE_REGION3) + Buffers.WriteRegion3(w, v) + elseif t == "Instance" then + Buffers.WriteU8(w, TYPE_INSTANCE) + Buffers.WriteInstance(w, v) + elseif t == "EnumItem" then + Buffers.WriteU8(w, TYPE_ENUM) + Buffers.WriteEnum(w, v) + elseif t == "BrickColor" then + Buffers.WriteU8(w, TYPE_BRICKCOLOR) + Buffers.WriteBrickColor(w, v) + elseif t == "TweenInfo" then + Buffers.WriteU8(w, TYPE_TWEENINFO) + Buffers.WriteTweenInfo(w, v) + elseif t == "table" then + Buffers.WriteU8(w, TYPE_TABLE) + for k, val in pairs(v) do + Buffers.WriteAny(w, k) + Buffers.WriteAny(w, val) + end + Buffers.WriteU8(w, TYPE_NIL) + else + --warn("Unsupported type: " .. t) + Buffers.WriteU8(w, TYPE_NIL) + end +end + +function Buffers.ReadAny(r: Reader): any + local typeId = Buffers.ReadU8(r) + + if typeId == TYPE_NIL then return nil + elseif typeId == TYPE_BOOL then return Buffers.ReadBool(r) + elseif typeId == TYPE_NUMBER then return Buffers.ReadF64(r) + elseif typeId == TYPE_STRING then return Buffers.ReadString(r) + elseif typeId == TYPE_BUFFER then return Buffers.ReadBuffer(r) + elseif typeId == TYPE_VECTOR2 then return Buffers.ReadVector2(r) + elseif typeId == TYPE_VECTOR3 then return Buffers.ReadVector3(r) + elseif typeId == TYPE_CFRAME then return Buffers.ReadCFrame(r) + elseif typeId == TYPE_COLOR3 then return Buffers.ReadColor3(r) + elseif typeId == TYPE_UDIM then return Buffers.ReadUDim(r) + elseif typeId == TYPE_UDIM2 then return Buffers.ReadUDim2(r) + elseif typeId == TYPE_REGION3 then return Buffers.ReadRegion3(r) + elseif typeId == TYPE_INSTANCE then return Buffers.ReadInstance(r) + elseif typeId == TYPE_ENUM then return Buffers.ReadEnum(r) + elseif typeId == TYPE_BRICKCOLOR then return Buffers.ReadBrickColor(r) + elseif typeId == TYPE_TWEENINFO then return Buffers.ReadTweenInfo(r) + elseif typeId == TYPE_TABLE then + local t = {} + while true do + local k = Buffers.ReadAny(r) + if k == nil then break end + local v = Buffers.ReadAny(r) + t[k] = v + end + return t + elseif typeId == TYPE_INT_U8 then return Buffers.ReadU8(r) + elseif typeId == TYPE_INT_U16 then return Buffers.ReadU16(r) + elseif typeId == TYPE_INT_U32 then return Buffers.ReadU32(r) + elseif typeId == TYPE_INT_I16 then return Buffers.ReadI16(r) + elseif typeId == TYPE_INT_I32 then return Buffers.ReadI32(r) + end + + return nil +end + +function Buffers.Pack(w: Writer, ...) + local n = select("#", ...) + Buffers.WriteVarInt(w, n) + for i = 1, n do + Buffers.WriteAny(w, select(i, ...)) + end +end + +function Buffers.Unpack(r: Reader): {any} + local n = Buffers.ReadVarInt(r) + local t = t_create(n) + for i = 1, n do + t[i] = Buffers.ReadAny(r) + end + return t +end + +return Buffers \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau b/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau new file mode 100644 index 0000000..60f05e2 --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau @@ -0,0 +1,62 @@ +--!strict +local Remotes = {} + +local instance_new = Instance.new + +local RELIABLE_NAME = "Jolt_Reliable" +local UNRELIABLE_NAME = "Jolt_Unreliable" + +export type RemoteGroup = { + Reliable: RemoteEvent, + Unreliable: UnreliableRemoteEvent, +} + +local _cachedGroup: RemoteGroup? = nil + +function Remotes.Get(): RemoteGroup + if _cachedGroup then + return _cachedGroup + end + + local reliable = script:WaitForChild(RELIABLE_NAME) + local unreliable = script:WaitForChild(UNRELIABLE_NAME) + + if not reliable or not unreliable then + error("Jolt remotes not found. Ensure Jolt is initialized on the server.") + end + + _cachedGroup = { + Reliable = reliable :: RemoteEvent, + Unreliable = unreliable :: UnreliableRemoteEvent, + } + return _cachedGroup :: RemoteGroup +end + +function Remotes.Create(): RemoteGroup + if _cachedGroup then + return _cachedGroup + end + + local reliable = script:FindFirstChild(RELIABLE_NAME) + local unreliable = script:FindFirstChild(UNRELIABLE_NAME) + + if not reliable then + reliable = instance_new("RemoteEvent") + reliable.Name = RELIABLE_NAME + reliable.Parent = script + end + + if not unreliable then + unreliable = instance_new("UnreliableRemoteEvent") + unreliable.Name = UNRELIABLE_NAME + unreliable.Parent = script + end + + _cachedGroup = { + Reliable = reliable :: RemoteEvent, + Unreliable = unreliable :: UnreliableRemoteEvent, + } + return _cachedGroup :: RemoteGroup +end + +return Remotes \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/init.luau b/src/DebuggerUI/Shared/External/Jolt/init.luau new file mode 100644 index 0000000..628cb34 --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/init.luau @@ -0,0 +1,48 @@ +--!strict +local Jolt = {} + +local RunService = game:GetService("RunService") + +local IS_SERVER = RunService:IsServer() +local IS_CLIENT = RunService:IsClient() + +local Client = require("@self/Client") +local Server = require("@self/Server") + +export type Server = { + Fire: (self: Server, player: Player, Args...) -> (), + FireUnreliable: (self: Server, player: Player, Args...) -> (), + FireAll: (self: Server, Args...) -> (), + FireAllUnreliable: (self: Server, Args...) -> (), + Connect: (self: Server, callback: (player: Player, Args...) -> ()) -> { Disconnect: () -> () }, + Once: (self: Server, callback: (player: Player, Args...) -> ()) -> (), + Wait: (self: Server) -> (Player, Args...), + OnInvoke: ((player: Player, Args...) -> Out...)?, +} + +export type Client = { + Fire: (self: Client, Args...) -> (), + FireUnreliable: (self: Client, Args...) -> (), + Invoke: (self: Client, Args...) -> Out..., + Connect: (self: Client, callback: (Args...) -> ()) -> { Disconnect: () -> () }, + Once: (self: Client, callback: (Args...) -> ()) -> (), + Wait: (self: Client) -> Args..., +} + +function Jolt.Server(name: string): Server + if not IS_SERVER then + error("Jolt.Server can only be called on the server.") + end + + return Server.new(name) :: any +end + +function Jolt.Client(name: string): Client + if not IS_CLIENT then + error("Jolt.Client can only be called on the client.") + end + + return Client.new(name) :: any +end + +return Jolt \ No newline at end of file From b1f8562fb3d9f335efb7ba401e53f68f4d8110e6 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:16:42 +0700 Subject: [PATCH 03/22] Add iris external lib --- src/DebuggerUI/Shared/External/iris/API.luau | 2085 +++++++++++++++++ .../Shared/External/iris/Internal.luau | 908 +++++++ .../Shared/External/iris/PubTypes.luau | 43 + .../Shared/External/iris/Types.luau | 659 ++++++ .../Shared/External/iris/WidgetTypes.luau | 521 ++++ .../Shared/External/iris/config.luau | 304 +++ .../Shared/External/iris/demoWindow.luau | 1911 +++++++++++++++ src/DebuggerUI/Shared/External/iris/init.luau | 692 ++++++ .../Shared/External/iris/widgets/Button.luau | 93 + .../External/iris/widgets/Checkbox.luau | 118 + .../Shared/External/iris/widgets/Combo.luau | 495 ++++ .../Shared/External/iris/widgets/Format.luau | 155 ++ .../Shared/External/iris/widgets/Image.luau | 157 ++ .../Shared/External/iris/widgets/Input.luau | 1390 +++++++++++ .../Shared/External/iris/widgets/Menu.luau | 606 +++++ .../Shared/External/iris/widgets/Plot.luau | 648 +++++ .../External/iris/widgets/RadioButton.luau | 129 + .../Shared/External/iris/widgets/Root.luau | 141 ++ .../Shared/External/iris/widgets/Tab.luau | 334 +++ .../Shared/External/iris/widgets/Table.luau | 634 +++++ .../Shared/External/iris/widgets/Text.luau | 134 ++ .../Shared/External/iris/widgets/Tree.luau | 296 +++ .../Shared/External/iris/widgets/Window.luau | 1078 +++++++++ .../Shared/External/iris/widgets/init.luau | 448 ++++ 24 files changed, 13979 insertions(+) create mode 100644 src/DebuggerUI/Shared/External/iris/API.luau create mode 100644 src/DebuggerUI/Shared/External/iris/Internal.luau create mode 100644 src/DebuggerUI/Shared/External/iris/PubTypes.luau create mode 100644 src/DebuggerUI/Shared/External/iris/Types.luau create mode 100644 src/DebuggerUI/Shared/External/iris/WidgetTypes.luau create mode 100644 src/DebuggerUI/Shared/External/iris/config.luau create mode 100644 src/DebuggerUI/Shared/External/iris/demoWindow.luau create mode 100644 src/DebuggerUI/Shared/External/iris/init.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Button.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Combo.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Format.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Image.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Input.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Menu.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Plot.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Root.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Tab.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Table.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Text.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Tree.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/Window.luau create mode 100644 src/DebuggerUI/Shared/External/iris/widgets/init.luau diff --git a/src/DebuggerUI/Shared/External/iris/API.luau b/src/DebuggerUI/Shared/External/iris/API.luau new file mode 100644 index 0000000..9fabd8f --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/API.luau @@ -0,0 +1,2085 @@ +local Types = require(script.Parent.Types) + +return function(Iris: Types.Iris) + -- basic wrapper for nearly every widget, saves space. + local function wrapper(name) + return function(arguments, states) + return Iris.Internal._Insert(name, arguments, states) + end + end + + --[[ + ---------------------------- + [SECTION] Window API + ---------------------------- + ]] + --[=[ + @class Window + + Windows are the fundamental widget for Iris. Every other widget must be a descendant of a window. + + ```lua + Iris.Window({ "Example Window" }) + Iris.Text({ "This is an example window!" }) + Iris.End() + ``` + + ![Example window](/Iris/assets/api/window/basicWindow.png) + + If you do not want the code inside a window to run unless it is open then you can use the following: + ```lua + local window = Iris.Window({ "Many Widgets Window" }) + + if window.state.isOpened.value and window.state.isUncollapsed.value then + Iris.Text({ "I will only be created when the window is open." }) + end + Iris.End() -- must always call Iris.End(), regardless of whether the window is open or not. + ``` + ]=] + + --[=[ + @within Window + @prop Window Iris.Window + @tag Widget + @tag HasChildren + @tag HasState + + The top-level container for all other widgets to be created within. + Can be moved and resized across the screen. Cannot contain embedded windows. + Menus can be appended to windows creating a menubar. + + ```lua + hasChildren = true + hasState = true + Arguments = { + Title: string, + NoTitleBar: boolean? = false, + NoBackground: boolean? = false, -- the background behind the widget container. + NoCollapse: boolean? = false, + NoClose: boolean? = false, + NoMove: boolean? = false, + NoScrollbar: boolean? = false, -- the scrollbar if the window is too short for all widgets. + NoResize: boolean? = false, + NoNav: boolean? = false, -- unimplemented. + NoMenu: boolean? = false -- whether the menubar will show if created. + } + Events = { + opened: () -> boolean, -- once when opened. + closed: () -> boolean, -- once when closed. + collapsed: () -> boolean, -- once when collapsed. + uncollapsed: () -> boolean, -- once when uncollapsed. + hovered: () -> boolean -- fires when the mouse hovers over any of the window. + } + States = { + size = State? = Vector2.new(400, 300), + position = State?, + isUncollapsed = State? = true, + isOpened = State? = true, + scrollDistance = State? -- vertical scroll distance, if too short. + } + ``` + ]=] + Iris.Window = wrapper("Window") + + --[=[ + @within Iris + @function SetFocusedWindow + @param window Types.Window -- the window to focus. + + Sets the focused window to the window provided, which brings it to the front and makes it active. + ]=] + Iris.SetFocusedWindow = Iris.Internal.SetFocusedWindow + + --[=[ + @within Window + @prop Tooltip Iris.Tooltip + @tag Widget + + Displays a text label next to the cursor + + ```lua + Iris.Tooltip({"My custom tooltip"}) + ``` + + ![Basic tooltip example](/Iris/assets/api/window/basicTooltip.png) + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string + } + ``` + ]=] + Iris.Tooltip = wrapper("Tooltip") + + --[[ + --------------------------------- + [SECTION] Menu Widget API + --------------------------------- + ]] + --[=[ + @class Menu + Menu API + ]=] + + --[=[ + @within Menu + @prop MenuBar Iris.MenuBar + @tag Widget + @tag HasChildren + + Creates a MenuBar for the current window. Must be called directly under a Window and not within a child widget. + :::info + This does not create any menus, just tells the window that we going to add menus within. + ::: + + ```lua + hasChildren = true + hasState = false + ``` + ]=] + Iris.MenuBar = wrapper("MenuBar") + + --[=[ + @within Menu + @prop Menu Iris.Menu + @tag Widget + @tag HasChildren + @tag HasState + + Creates an collapsable menu. If the Menu is created directly under a MenuBar, then the widget will + be placed horizontally below the window title. If the menu Menu is created within another menu, then + it will be placed vertically alongside MenuItems and display an arrow alongside. + + The opened menu will be a vertically listed box below or next to the button. + + ```lua + Iris.Window({"Menu Demo"}) + Iris.MenuBar() + Iris.Menu({"Test Menu"}) + Iris.Button({"Menu Option 1"}) + Iris.Button({"Menu Option 2"}) + Iris.End() + Iris.End() + Iris.End() + ``` + + ![Example menu](/Iris/assets/api/menu/basicMenu.gif) + + :::info + There are widgets which are designed for being parented to a menu whilst other happens to work. There is nothing + preventing you from adding any widget as a child, but the behaviour is unexplained and not intended. + ::: + + ```lua + hasChildren = true + hasState = true + Arguments = { + Text: string -- menu text. + } + Events = { + clicked: () -> boolean, + opened: () -> boolean, -- once when opened. + closed: () -> boolean, -- once when closed. + hovered: () -> boolean + } + States = { + isOpened: State? -- whether the menu is open, including any sub-menus within. + } + ``` + ]=] + Iris.Menu = wrapper("Menu") + + --[=[ + @within Menu + @prop MenuItem Iris.MenuItem + @tag Widget + + Creates a button within a menu. The optional KeyCode and ModiferKey arguments will show the keys next + to the title, but **will not** bind any connection to them. You will need to do this yourself. + + ```lua + Iris.Window({"MenuToggle Demo"}) + Iris.MenuBar() + Iris.MenuToggle({"Menu Item"}) + Iris.End() + Iris.End() + ``` + + ![Example Menu Item](/Iris/assets/api/menu/basicMenuItem.gif) + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + KeyCode: Enum.KeyCode? = nil, -- an optional keycode, does not actually connect an event. + ModifierKey: Enum.ModifierKey? = nil -- an optional modifer key for the key code. + } + Events = { + clicked: () -> boolean, + hovered: () -> boolean + } + ``` + ]=] + Iris.MenuItem = wrapper("MenuItem") + + --[=[ + @within Menu + @prop MenuToggle Iris.MenuToggle + @tag Widget + @tag HasState + + Creates a togglable button within a menu. The optional KeyCode and ModiferKey arguments act the same + as the MenuItem. It is not visually the same as a checkbox, but has the same functionality. + + ```lua + Iris.Window({"MenuToggle Demo"}) + Iris.MenuBar() + Iris.MenuToggle({"Menu Toggle"}) + Iris.End() + Iris.End() + ``` + + ![Example Menu Toggle](/Iris/assets/api/menu/basicMenuToggle.gif) + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string, + KeyCode: Enum.KeyCode? = nil, -- an optional keycode, does not actually connect an event. + ModifierKey: Enum.ModifierKey? = nil -- an optional modifer key for the key code. + } + Events = { + checked: () -> boolean, -- once on check. + unchecked: () -> boolean, -- once on uncheck. + hovered: () -> boolean + } + States = { + isChecked: State? + } + ``` + ]=] + Iris.MenuToggle = wrapper("MenuToggle") + + --[[ + ----------------------------------- + [SECTION] Format Widget Iris + ----------------------------------- + ]] + --[=[ + @class Format + Format API + ]=] + + --[=[ + @within Format + @prop Separator Iris.Separator + @tag Widget + + A vertical or horizonal line, depending on the context, which visually seperates widgets. + + ```lua + Iris.Window({"Separator Demo"}) + Iris.Text({"Some text here!"}) + Iris.Separator() + Iris.Text({"This text has been separated!"}) + Iris.End() + ``` + + ![Example Separator](/Iris/assets/api/format/basicSeparator.png) + + ```lua + hasChildren = false + hasState = false + ``` + ]=] + Iris.Separator = wrapper("Separator") + + --[=[ + @within Format + @prop Indent Iris.Indent + @tag Widget + @tag HasChildren + + Indents its child widgets. + + ```lua + Iris.Window({"Indent Demo"}) + Iris.Text({"Unindented text!"}) + Iris.Indent() + Iris.Text({"This text has been indented!"}) + Iris.End() + Iris.End() + ``` + + ![Example Indent](/Iris/assets/api/format/basicIndent.png) + + ```lua + hasChildren = true + hasState = false + Arguments = { + Width: number? = Iris._config.IndentSpacing -- indent width ammount. + } + ``` + ]=] + Iris.Indent = wrapper("Indent") + + --[=[ + @within Format + @prop SameLine Iris.SameLine + @tag Widget + @tag HasChildren + + Positions its children in a row, horizontally. + + ```lua + Iris.Window({"Same Line Demo"}) + Iris.Text({"All of these buttons are on the same line!"}) + Iris.SameLine() + Iris.Button({"Button 1"}) + Iris.Button({"Button 2"}) + Iris.Button({"Button 3"}) + Iris.End() + Iris.End() + ``` + + ![Example SameLine](/Iris/assets/api/format/basicSameLine.png) + + ```lua + hasChildren = true + hasState = false + Arguments = { + Width: number? = Iris._config.ItemSpacing.X, -- horizontal spacing between child widgets. + VerticalAlignment: Enum.VerticalAlignment? = Enum.VerticalAlignment.Center -- how widgets vertically to each other. + HorizontalAlignment: Enum.HorizontalAlignment? = Enum.HorizontalAlignment.Center -- how widgets are horizontally. + } + ``` + ]=] + Iris.SameLine = wrapper("SameLine") + + --[=[ + @within Format + @prop Group Iris.Group + @tag Widget + @tag HasChildren + + Layout widget which contains its children as a single group. + + ```lua + hasChildren = true + hasState = false + ``` + ]=] + Iris.Group = wrapper("Group") + + --[[ + --------------------------------- + [SECTION] Text Widget API + --------------------------------- + ]] + --[=[ + @class Text + Text Widget API + ]=] + + --[=[ + @within Text + @prop Text Iris.Text + @tag Widget + + A text label to display the text argument. + The Wrapped argument will make the text wrap around if it is cut off by its parent. + The Color argument will change the color of the text, by default it is defined in the configuration file. + + ```lua + Iris.Window({"Text Demo"}) + Iris.Text({"This is regular text"}) + Iris.End() + ``` + + ![Example Text](/Iris/assets/api/text/basicText.png) + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + Wrapped: boolean? = [CONFIG] = false, -- whether the text will wrap around inside the parent container. If not specified, then equal to the config + Color: Color3? = Iris._config.TextColor, -- the colour of the text. + RichText: boolean? = [CONFIG] = false -- enable RichText. If not specified, then equal to the config + } + Events = { + hovered: () -> boolean + } + ``` + ]=] + Iris.Text = wrapper("Text") + + --[=[ + @within Text + @prop TextWrapped Iris.Text + @tag Widget + @deprecated v2.0.0 -- Use 'Text' with the Wrapped argument or change the config. + + An alias for [Iris.Text](Text#Text) with the Wrapped argument set to true, and the text will wrap around if cut off by its parent. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + } + Events = { + hovered: () -> boolean + } + ``` + ]=] + Iris.TextWrapped = function(arguments: Types.WidgetArguments): Types.Text + arguments[2] = true + return Iris.Internal._Insert("Text", arguments) :: Types.Text + end + + --[=[ + @within Text + @prop TextColored Iris.Text + @tag Widget + @deprecated v2.0.0 -- Use 'Text' with the Color argument or change the config. + + An alias for [Iris.Text](Text#Text) with the color set by the Color argument. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + Color: Color3 -- the colour of the text. + } + Events = { + hovered: () -> boolean + } + ``` + ]=] + Iris.TextColored = function(arguments: Types.WidgetArguments): Types.Text + arguments[3] = arguments[2] + arguments[2] = nil + return Iris.Internal._Insert("Text", arguments) :: Types.Text + end + + --[=[ + @within Text + @prop SeparatorText Iris.SeparatorText + @tag Widget + + Similar to [Iris.Separator](Format#Separator) but with a text label to be used as a header + when an [Iris.Tree](Tree#Tree) or [Iris.CollapsingHeader](Tree#CollapsingHeader) is not appropriate. + + Visually a full width thin line with a text label clipping out part of the line. + + ```lua + Iris.Window({"Separator Text Demo"}) + Iris.Text({"Regular Text"}) + Iris.SeparatorText({"This is a separator with text"}) + Iris.Text({"More Regular Text"}) + Iris.End() + ``` + + ![Example Separator Text](/Iris/assets/api/text/basicSeparatorText.png) + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string + } + ``` + ]=] + Iris.SeparatorText = wrapper("SeparatorText") + + --[=[ + @within Text + @prop InputText Iris.InputText + @tag Widget + @tag HasState + + A field which allows the user to enter text. + + ```lua + Iris.Window({"Input Text Demo"}) + local inputtedText = Iris.State("") + + Iris.InputText({"Enter text here:"}, {text = inputtedText}) + Iris.Text({"You entered: " .. inputtedText:get()}) + Iris.End() + ``` + + ![Example Input Text](/Iris/assets/api/text/basicInputText.gif) + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputText", + TextHint: string? = "", -- a hint to display when the text box is empty. + ReadOnly: boolean? = false, + MultiLine: boolean? = false + } + Events = { + textChanged: () -> boolean, -- whenever the textbox looses focus and a change was made. + hovered: () -> boolean + } + States = { + text: State? + } + ``` + ]=] + Iris.InputText = wrapper("InputText") + + --[[ + ---------------------------------- + [SECTION] Basic Widget API + ---------------------------------- + ]] + --[=[ + @class Basic + Basic Widget API + ]=] + + --[=[ + @within Basic + @prop Button Iris.Button + @tag Widget + + A clickable button the size of the text with padding. Can listen to the `clicked()` event to determine if it was pressed. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + Size: UDim2? = UDim2.fromOffset(0, 0), + } + Events = { + clicked: () -> boolean, + rightClicked: () -> boolean, + doubleClicked: () -> boolean, + ctrlClicked: () -> boolean, -- when the control key is down and clicked. + hovered: () -> boolean + } + ``` + ]=] + Iris.Button = wrapper("Button") + + --[=[ + @within Basic + @prop SmallButton Iris.SmallButton + @tag Widget + + A smaller clickable button, the same as a [Iris.Button](Basic#Button) but without padding. Can listen to the `clicked()` event to determine if it was pressed. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Text: string, + Size: UDim2? = 0, + } + Events = { + clicked: () -> boolean, + rightClicked: () -> boolean, + doubleClicked: () -> boolean, + ctrlClicked: () -> boolean, -- when the control key is down and clicked. + hovered: () -> boolean + } + ``` + ]=] + Iris.SmallButton = wrapper("SmallButton") + + --[=[ + @within Basic + @prop Checkbox Iris.Checkbox + @tag Widget + @tag HasState + + A checkable box with a visual tick to represent a boolean true or false state. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string + } + Events = { + checked: () -> boolean, -- once when checked. + unchecked: () -> boolean, -- once when unchecked. + hovered: () -> boolean + } + State = { + isChecked = State? -- whether the box is checked. + } + ``` + ]=] + Iris.Checkbox = wrapper("Checkbox") + + --[=[ + @within Basic + @prop RadioButton Iris.RadioButton + @tag Widget + @tag HasState + + A circular selectable button, changing the state to its index argument. Used in conjunction with multiple other RadioButtons sharing the same state to represent one value from multiple options. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string, + Index: any -- the state object is set to when clicked. + } + Events = { + selected: () -> boolean, + unselected: () -> boolean, + active: () -> boolean, -- if the state index equals the RadioButton's index. + hovered: () -> boolean + } + State = { + index = State? -- the state set by the index of a RadioButton. + } + ``` + ]=] + Iris.RadioButton = wrapper("RadioButton") + + --[[ + ---------------------------------- + [SECTION] Image Widget API + ---------------------------------- + ]] + --[=[ + @class Image + Image Widget API + + Provides two widgets for Images and ImageButtons, which provide the same control as a an ImageLabel instance. + ]=] + + --[=[ + @within Image + @prop Image Iris.Image + @tag Widget + + An image widget for displaying an image given its texture ID and a size. The widget also supports Rect Offset and Size allowing cropping of the image and the rest of the ScaleType properties. + Some of the arguments are only used depending on the ScaleType property, such as TileSize or Slice which will be ignored. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Image: string, -- the texture asset id + Size: UDim2, + Rect: Rect? = Rect.new(), -- Rect structure which is used to determine the offset or size. An empty, zeroed rect is equivalent to nil + ScaleType: Enum.ScaleType? = Enum.ScaleType.Stretch, -- used to determine whether the TileSize, SliceCenter and SliceScale arguments are used + ResampleMode: Enum.ResampleMode? = Enum.ResampleMode.Default, + TileSize: UDim2? = UDim2.fromScale(1, 1), -- only used if the ScaleType is set to Tile + SliceCenter: Rect? = Rect.new(), -- only used if the ScaleType is set to Slice + SliceScale: number? = 1 -- only used if the ScaleType is set to Slice + } + Events = { + hovered: () -> boolean + } + ``` + ]=] + Iris.Image = wrapper("Image") + + --[=[ + @within Image + @prop ImageButton Iris.ImageButton + @tag Widget + + An image button widget for a button as an image given its texture ID and a size. The widget also supports Rect Offset and Size allowing cropping of the image, and the rest of the ScaleType properties. + Supports all of the events of a regular button. + + ```lua + hasChildren = false + hasState = false + Arguments = { + Image: string, -- the texture asset id + Size: UDim2, + Rect: Rect? = Rect.new(), -- Rect structure which is used to determine the offset or size. An empty, zeroed rect is equivalent to nil + ScaleType: Enum.ScaleType? = Enum.ScaleType.Stretch, -- used to determine whether the TileSize, SliceCenter and SliceScale arguments are used + ResampleMode: Enum.ResampleMode? = Enum.ResampleMode.Default, + TileSize: UDim2? = UDim2.fromScale(1, 1), -- only used if the ScaleType is set to Tile + SliceCenter: Rect? = Rect.new(), -- only used if the ScaleType is set to Slice + SliceScale: number? = 1 -- only used if the ScaleType is set to Slice + } + Events = { + clicked: () -> boolean, + rightClicked: () -> boolean, + doubleClicked: () -> boolean, + ctrlClicked: () -> boolean, -- when the control key is down and clicked. + hovered: () -> boolean + } + ``` + ]=] + Iris.ImageButton = wrapper("ImageButton") + + --[[ + --------------------------------- + [SECTION] Tree Widget API + --------------------------------- + ]] + --[=[ + @class Tree + Tree Widget API + ]=] + + --[=[ + @within Tree + @prop Tree Iris.Tree + @tag Widget + @tag HasChildren + @tag HasState + + A collapsable container for other widgets, to organise and hide widgets when not needed. The state determines whether the child widgets are visible or not. Clicking on the widget will collapse or uncollapse it. + + ```lua + hasChildren: true + hasState: true + Arguments = { + Text: string, + SpanAvailWidth: boolean? = false, -- the tree title will fill all horizontal space to the end its parent container. + NoIndent: boolean? = false -- the child widgets will not be indented underneath. + } + Events = { + collapsed: () -> boolean, + uncollapsed: () -> boolean, + hovered: () -> boolean + } + State = { + isUncollapsed: State? -- whether the widget is collapsed. + } + ``` + ]=] + Iris.Tree = wrapper("Tree") + + --[=[ + @within Tree + @prop CollapsingHeader Iris.CollapsingHeader + @tag Widget + @tag HasChildren + @tag HasState + + The same as a Tree Widget, but with a larger title and clearer, used mainly for organsing widgets on the first level of a window. + + ```lua + hasChildren: true + hasState: true + Arguments = { + Text: string + } + Events = { + collapsed: () -> boolean, + uncollapsed: () -> boolean, + hovered: () -> boolean + } + State = { + isUncollapsed: State? -- whether the widget is collapsed. + } + ``` + ]=] + Iris.CollapsingHeader = wrapper("CollapsingHeader") + + --[[ + -------------------------------- + [SECTION] Tab Widget API + -------------------------------- + ]] + --[=[ + @class Tab + Tab Widget API + ]=] + + --[=[ + @within Tab + @prop TabBar Iris.TabBar + @tag Widget + @tag HasChildren + @tag HasState + + Creates a TabBar for putting tabs under. This does not create the tabs but just the container for them to be in. + The index state is used to control the current tab and is based on an index starting from 1 rather than the + text provided to a Tab. The TabBar will replicate the index to the Tab children . + + ```lua + hasChildren: true + hasState: true + Arguments = {} + Events = {} + State = { + index: State? -- whether the widget is collapsed. + } + ``` + ]=] + Iris.TabBar = wrapper("TabBar") + + --[=[ + @within Tab + @prop Tab Iris.Tab + @tag Widget + @tag HasChildren + @tag HasState + + The tab item for use under a TabBar. The TabBar must be the parent and determines the index value. You cannot + provide a state for this tab. The optional Hideable argument determines if a tab can be closed, which is + controlled by the isOpened state. + + A tab will take up the full horizontal width of the parent and hide any other tabs in the TabBar. + + ```lua + hasChildren: true + hasState: true + Arguments = { + Text: string, + Hideable: boolean? = nil -- determines whether a tab can be closed/hidden + } + Events = { + clicked: () -> boolean, + hovered: () -> boolean + selected: () -> boolean + unselected: () -> boolean + active: () -> boolean + opened: () -> boolean + closed: () -> boolean + } + State = { + isOpened: State? + } + ``` + ]=] + Iris.Tab = wrapper("Tab") + + --[[ + ---------------------------------- + [SECTION] Input Widget API + ---------------------------------- + ]] + --[=[ + @class Input + Input Widget API + + Input Widgets are textboxes for typing in specific number values. See [Drag], [Slider] or [InputText](Text#InputText) for more input types. + + Iris provides a set of specific inputs for the datatypes: + Number, + [Vector2](https://create.roblox.com/docs/reference/engine/datatypes/Vector2), + [Vector3](https://create.roblox.com/docs/reference/engine/datatypes/Vector3), + [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim), + [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2), + [Rect](https://create.roblox.com/docs/reference/engine/datatypes/Rect), + [Color3](https://create.roblox.com/docs/reference/engine/datatypes/Color3) + and the custom [Color4](https://create.roblox.com/docs/reference/engine/datatypes/Color3). + + Each Input widget has the same arguments but the types depend of the DataType: + 1. Text: string? = "Input{type}" -- the text to be displayed to the right of the textbox. + 2. Increment: DataType? = nil, -- the increment argument determines how a value will be rounded once the textbox looses focus. + 3. Min: DataType? = nil, -- the minimum value that the widget will allow, no clamping by default. + 4. Max: DataType? = nil, -- the maximum value that the widget will allow, no clamping by default. + 5. Format: string | { string }? = [DYNAMIC] -- uses `string.format` to customise visual display. + + The format string can either by a single value which will apply to every box, or a table allowing specific text. + + :::note + If you do not specify a format option then Iris will dynamically calculate a relevant number of sigifs and format option. + For example, if you have Increment, Min and Max values of 1, 0 and 100, then Iris will guess that you are only using integers + and will format the value as an integer. + As another example, if you have Increment, Min and max values of 0.005, 0, 1, then Iris will guess you are using a float of 3 + significant figures. + + Additionally, for certain DataTypes, Iris will append an prefix to each box if no format option is provided. + For example, a Vector3 box will have the append values of "X: ", "Y: " and "Z: " to the relevant input box. + ::: + ]=] + + --[=[ + @within Input + @prop InputNum Iris.InputNum + @tag Widget + @tag HasState + + An input box for numbers. The number can be either an integer or a float. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputNum", + Increment: number? = nil, + Min: number? = nil, + Max: number? = nil, + Format: string? | { string }? = [DYNAMIC], -- Iris will dynamically generate an approriate format. + NoButtons: boolean? = false -- whether to display + and - buttons next to the input box. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputNum = wrapper("InputNum") + + --[=[ + @within Input + @prop InputVector2 Iris.InputVector2 + @tag Widget + @tag HasState + + An input box for Vector2. The numbers can be either integers or floats. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputVector2", + Increment: Vector2? = nil, + Min: Vector2? = nil, + Max: Vector2? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputVector2 = wrapper("InputVector2") + + --[=[ + @within Input + @prop InputVector3 Iris.InputVector3 + @tag Widget + @tag HasState + + An input box for Vector3. The numbers can be either integers or floats. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputVector3", + Increment: Vector3? = nil, + Min: Vector3? = nil, + Max: Vector3? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputVector3 = wrapper("InputVector3") + + --[=[ + @within Input + @prop InputUDim Iris.InputUDim + @tag Widget + @tag HasState + + An input box for UDim. The Scale box will be a float and the Offset box will be + an integer, unless specified differently. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputUDim", + Increment: UDim? = nil, + Min: UDim? = nil, + Max: UDim? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputUDim = wrapper("InputUDim") + + --[=[ + @within Input + @prop InputUDim2 Iris.InputUDim2 + @tag Widget + @tag HasState + + An input box for UDim2. The Scale boxes will be floats and the Offset boxes will be + integers, unless specified differently. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputUDim2", + Increment: UDim2? = nil, + Min: UDim2? = nil, + Max: UDim2? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputUDim2 = wrapper("InputUDim2") + + --[=[ + @within Input + @prop InputRect Iris.InputRect + @tag Widget + @tag HasState + + An input box for Rect. The numbers will default to integers, unless specified differently. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputRect", + Increment: Rect? = nil, + Min: Rect? = nil, + Max: Rect? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.InputRect = wrapper("InputRect") + + --[[ + --------------------------------- + [SECTION] Drag Widget API + --------------------------------- + ]] + --[=[ + @class Drag + Drag Widget API + + A draggable widget for each datatype. Allows direct typing input but also dragging values by clicking and holding. + + See [Input] for more details on the arguments. + ]=] + + --[=[ + @within Drag + @prop DragNum Iris.DragNum + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a number. + You can ctrl + click to directly input a number, like InputNum. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragNum", + Increment: number? = nil, + Min: number? = nil, + Max: number? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragNum = wrapper("DragNum") + + --[=[ + @within Drag + @prop DragVector2 Iris.DragVector2 + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a Vector2. + You can ctrl + click to directly input a Vector2, like InputVector2. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragVector2", + Increment: Vector2? = nil, + Min: Vector2? = nil, + Max: Vector2? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragVector2 = wrapper("DragVector2") + + --[=[ + @within Drag + @prop DragVector3 Iris.DragVector3 + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a Vector3. + You can ctrl + click to directly input a Vector3, like InputVector3. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragVector3", + Increment: Vector3? = nil, + Min: Vector3? = nil, + Max: Vector3? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragVector3 = wrapper("DragVector3") + + --[=[ + @within Drag + @prop DragUDim Iris.DragUDim + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a UDim. + You can ctrl + click to directly input a UDim, like InputUDim. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragUDim", + Increment: UDim? = nil, + Min: UDim? = nil, + Max: UDim? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragUDim = wrapper("DragUDim") + + --[=[ + @within Drag + @prop DragUDim2 Iris.DragUDim2 + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a UDim2. + You can ctrl + click to directly input a UDim2, like InputUDim2. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragUDim2", + Increment: UDim2? = nil, + Min: UDim2? = nil, + Max: UDim2? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragUDim2 = wrapper("DragUDim2") + + --[=[ + @within Drag + @prop DragRect Iris.DragRect + @tag Widget + @tag HasState + + A field which allows the user to click and drag their cursor to enter a Rect. + You can ctrl + click to directly input a Rect, like InputRect. + You can hold Shift to increase speed, and Alt to decrease speed when dragging. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "DragRect", + Increment: Rect? = nil, + Min: Rect? = nil, + Max: Rect? = nil, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.DragRect = wrapper("DragRect") + + --[=[ + @within Input + @prop InputColor3 Iris.InputColor3 + @tag Widget + @tag HasState + + An input box for Color3. The input boxes are draggable between 0 and 255 or if UseFloats then between 0 and 1. + Input can also be done using HSV instead of the default RGB. + If no format argument is provided then a default R, G, B or H, S, V prefix is applied. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputColor3", + UseFloats: boolean? = false, -- constrain the values between floats 0 and 1 or integers 0 and 255. + UseHSV: boolean? = false, -- input using HSV instead. + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + color: State?, + editingText: State? + } + ``` + ]=] + Iris.InputColor3 = wrapper("InputColor3") + + --[=[ + @within Input + @prop InputColor4 Iris.InputColor4 + @tag Widget + @tag HasState + + An input box for Color4. Color4 is a combination of Color3 and a fourth transparency argument. + It has two states for this purpose. + The input boxes are draggable between 0 and 255 or if UseFloats then between 0 and 1. + Input can also be done using HSV instead of the default RGB. + If no format argument is provided then a default R, G, B, T or H, S, V, T prefix is applied. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputColor4", + UseFloats: boolean? = false, -- constrain the values between floats 0 and 1 or integers 0 and 255. + UseHSV: boolean? = false, -- input using HSV instead. + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + color: State?, + transparency: State?, + editingText: State? + } + ``` + ]=] + Iris.InputColor4 = wrapper("InputColor4") + + --[[ + ----------------------------------- + [SECTION] Slider Widget API + ----------------------------------- + ]] + --[=[ + @class Slider + Slider Widget API + + A draggable widget with a visual bar constrained between a min and max for each datatype. + Allows direct typing input but also dragging the slider by clicking and holding anywhere in the box. + + See [Input] for more details on the arguments. + ]=] + + --[=[ + @within Slider + @prop SliderNum Iris.SliderNum + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a number within a range. + You can ctrl + click to directly input a number, like InputNum. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderNum", + Increment: number? = 1, + Min: number? = 0, + Max: number? = 100, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderNum = wrapper("SliderNum") + + --[=[ + @within Slider + @prop SliderVector2 Iris.SliderVector2 + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a Vector2 within a range. + You can ctrl + click to directly input a Vector2, like InputVector2. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderVector2", + Increment: Vector2? = { 1, 1 }, + Min: Vector2? = { 0, 0 }, + Max: Vector2? = { 100, 100 }, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderVector2 = wrapper("SliderVector2") + + --[=[ + @within Slider + @prop SliderVector3 Iris.SliderVector3 + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a Vector3 within a range. + You can ctrl + click to directly input a Vector3, like InputVector3. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderVector3", + Increment: Vector3? = { 1, 1, 1 }, + Min: Vector3? = { 0, 0, 0 }, + Max: Vector3? = { 100, 100, 100 }, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderVector3 = wrapper("SliderVector3") + + --[=[ + @within Slider + @prop SliderUDim Iris.SliderUDim + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a UDim within a range. + You can ctrl + click to directly input a UDim, like InputUDim. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderUDim", + Increment: UDim? = { 0.01, 1 }, + Min: UDim? = { 0, 0 }, + Max: UDim? = { 1, 960 }, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderUDim = wrapper("SliderUDim") + + --[=[ + @within Slider + @prop SliderUDim2 Iris.SliderUDim2 + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a UDim2 within a range. + You can ctrl + click to directly input a UDim2, like InputUDim2. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderUDim2", + Increment: UDim2? = { 0.01, 1, 0.01, 1 }, + Min: UDim2? = { 0, 0, 0, 0 }, + Max: UDim2? = { 1, 960, 1, 960 }, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderUDim2 = wrapper("SliderUDim2") + + --[=[ + @within Slider + @prop SliderRect Iris.SliderRect + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a Rect within a range. + You can ctrl + click to directly input a Rect, like InputRect. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "SliderRect", + Increment: Rect? = { 1, 1, 1, 1 }, + Min: Rect? = { 0, 0, 0, 0 }, + Max: Rect? = { 960, 960, 960, 960 }, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State? + } + ``` + ]=] + Iris.SliderRect = wrapper("SliderRect") + + --[[ + ---------------------------------- + [SECTION] Combo Widget API + ---------------------------------- + ]] + --[=[ + @class Combo + Combo Widget API + ]=] + + --[=[ + @within Combo + @prop Selectable Iris.Selectable + @tag Widget + @tag HasState + + An object which can be selected. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string, + Index: any, -- index of selectable value. + NoClick: boolean? = false -- prevents the selectable from being clicked by the user. + } + Events = { + selected: () -> boolean, + unselected: () -> boolean, + active: () -> boolean, + clicked: () -> boolean, + rightClicked: () -> boolean, + doubleClicked: () -> boolean, + ctrlClicked: () -> boolean, + hovered: () -> boolean, + } + States = { + index: State -- a shared state between all selectables. + } + ``` + ]=] + Iris.Selectable = wrapper("Selectable") + + --[=[ + @within Combo + @prop Combo Iris.Combo + @tag Widget + @tag HasChildren + @tag HasState + + A dropdown menu box to make a selection from a list of values. + + ```lua + hasChildren = true + hasState = true + Arguments = { + Text: string, + NoButton: boolean? = false, -- hide the dropdown button. + NoPreview: boolean? = false -- hide the preview field. + } + Events = { + opened: () -> boolean, + closed: () -> boolean, + changed: () -> boolean, + clicked: () -> boolean, + hovered: () -> boolean + } + States = { + index: State, + isOpened: State? + } + ``` + ]=] + Iris.Combo = wrapper("Combo") + + --[=[ + @within Combo + @prop ComboArray Iris.Combo + @tag Widget + @tag HasChildren + @tag HasState + + A selection box to choose a value from an array. + + ```lua + hasChildren = true + hasState = true + Arguments = { + Text: string, + NoButton: boolean? = false, -- hide the dropdown button. + NoPreview: boolean? = false -- hide the preview field. + } + Events = { + opened: () -> boolean, + closed: () -> boolean, + clicked: () -> boolean, + hovered: () -> boolean + } + States = { + index: State, + isOpened: State? + } + Extra = { + selectionArray: { any } -- the array to generate a combo from. + } + ``` + ]=] + Iris.ComboArray = function(arguments: Types.WidgetArguments, states: Types.WidgetStates?, selectionArray: { T }) + local defaultState + if states == nil then + defaultState = Iris.State(selectionArray[1]) + else + defaultState = states + end + local thisWidget = Iris.Internal._Insert("Combo", arguments, defaultState) + local sharedIndex: Types.State = thisWidget.state.index + for _, Selection in selectionArray do + Iris.Internal._Insert("Selectable", { Selection, Selection }, { index = sharedIndex } :: Types.States) + end + Iris.End() + + return thisWidget + end + + --[=[ + @within Combo + @prop ComboEnum Iris.Combo + @tag Widget + @tag HasChildren + @tag HasState + + A selection box to choose a value from an Enum. + + ```lua + hasChildren = true + hasState = true + Arguments = { + Text: string, + NoButton: boolean? = false, -- hide the dropdown button. + NoPreview: boolean? = false -- hide the preview field. + } + Events = { + opened: () -> boolean, + closed: () -> boolean, + clicked: () -> boolean, + hovered: () -> boolean + } + States = { + index: State, + isOpened: State? + } + Extra = { + enumType: Enum -- the enum to generate a combo from. + } + ``` + ]=] + Iris.ComboEnum = function(arguments: Types.WidgetArguments, states: Types.WidgetStates?, enumType: Enum) + local defaultState + if states == nil then + defaultState = Iris.State(enumType:GetEnumItems()[1]) + else + defaultState = states + end + local thisWidget = Iris.Internal._Insert("Combo", arguments, defaultState) + local sharedIndex = thisWidget.state.index + for _, Selection in enumType:GetEnumItems() do + Iris.Internal._Insert("Selectable", { Selection.Name, Selection }, { index = sharedIndex } :: Types.States) + end + Iris.End() + + return thisWidget + end + + --[=[ + @private + @within Slider + @prop InputEnum Iris.InputEnum + @tag Widget + @tag HasState + + A field which allows the user to slide a grip to enter a number within a range. + You can ctrl + click to directly input a number, like InputNum. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "InputEnum", + Increment: number? = 1, + Min: number? = 0, + Max: number? = 100, + Format: string? | { string }? = [DYNAMIC] -- Iris will dynamically generate an approriate format. + } + Events = { + numberChanged: () -> boolean, + hovered: () -> boolean + } + States = { + number: State?, + editingText: State?, + enumItem: EnumItem + } + ``` + ]=] + Iris.InputEnum = Iris.ComboEnum + + --[[ + --------------------------------- + [SECTION] Plot Widget API + --------------------------------- + ]] + --[=[ + @class Plot + Plot Widget API + ]=] + + --[=[ + @within Plot + @prop ProgressBar Iris.ProgressBar + @tag Widget + @tag HasState + + A progress bar line with a state value to show the current state. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "Progress Bar", + Format: string? = nil -- optional to override with a custom progress such as `29/54` + } + Events = { + hovered: () -> boolean, + changed: () -> boolean + } + States = { + progress: State? + } + ``` + ]=] + Iris.ProgressBar = wrapper("ProgressBar") + + --[=[ + @within Plot + @prop PlotLines Iris.PlotLines + @tag Widget + @tag HasState + + A line graph for plotting a single line. Includes hovering to see a specific value on the graph, + and automatic scaling. Has an overlay text option at the top of the plot for displaying any + information. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "Plot Lines", + Height: number? = 0, + Min: number? = min, -- Iris will use the minimum value from the values + Max: number? = max, -- Iris will use the maximum value from the values + TextOverlay: string? = "" + } + Events = { + hovered: () -> boolean + } + States = { + values: State<{number}>?, + hovered: State<{number}>? -- read-only property + } + ``` + ]=] + Iris.PlotLines = wrapper("PlotLines") + + --[=[ + @within Plot + @prop PlotHistogram Iris.PlotHistogram + @tag Widget + @tag HasState + + A hisogram graph for showing values. Includes hovering to see a specific block on the graph, + and automatic scaling. Has an overlay text option at the top of the plot for displaying any + information. Also supports a baseline option, which determines where the blocks start from. + + ```lua + hasChildren = false + hasState = true + Arguments = { + Text: string? = "Plot Histogram", + Height: number? = 0, + Min: number? = min, -- Iris will use the minimum value from the values + Max: number? = max, -- Iris will use the maximum value from the values + TextOverlay: string? = "", + BaseLine: number? = 0 -- by default, blocks swap side at 0 + } + Events = { + hovered: () -> boolean + } + States = { + values: State<{number}>?, + hovered: State<{number}>? -- read-only property + } + ``` + ]=] + Iris.PlotHistogram = wrapper("PlotHistogram") + + --[[ + ---------------------------------- + [SECTION] Table Widget API + ---------------------------------- + ]] + --[=[ + @class Table + Table Widget API + + Example usage for creating a simple table: + ```lua + Iris.Table({ 4, true }) + do + Iris.SetHeaderColumnIndex(1) + + -- for each row + for i = 0, 10 do + + -- for each column + for j = 1, 4 do + if i == 0 then + -- + Iris.Text({ `H: {j}` }) + else + Iris.Text({ `R: {i}, C: {j}` }) + end + + -- move the next column (and row when necessary) + Iris.NextColumn() + end + end + ``` + ]=] + + --[=[ + @within Table + @prop Table Iris.Table + @tag Widget + @tag HasChildren + + A layout widget which allows children to be displayed in configurable columns and rows. Highly configurable for many different + options, with options for custom width columns as configured by the user, or automatically use the best size. + + When Resizable is enabled, the vertical columns can be dragged horizontally to increase or decrease space. This is linked to + the widths state, which controls the width of each column. This is also dependent on whether the FixedWidth argument is enabled. + By default, the columns will scale with the width of the table overall, therefore taking up a percentage, and the widths will be + in the range of 0 to 1 as a float. If FixedWidth is enabled, then the widths will be in pixels and have a value of > 2 as an + integer. + + ProportionalWidth determines whether each column has the same width, or individual. By default, each column will take up an equal + proportion of the total table width. If true, then the columns will be allocated a width proportional to their total content size, + meaning wider columns take up a greater share of the total available space. For a fixed width table, by default each column will + take the max width of all the columns. When true, each column width will the minimum to fit the children within. + + LimitTableWidth is used when FixedWidth is true. It will cut off the table horizontally after the last column. + + :::info + Once the NumColumns is set, it is not possible to change it without some extra code. The best way to do this is by using + `Iris.PushConfig()` and `Iris.PopConfig()` which will automatically redraw the widget when the columns change. + + ```lua + local numColumns = 4 + Iris.PushConfig({ columns = numColumns }) + Iris.Table({ numColumns, ...}) + do + ... + end + Iris.End() + Iris.PopConfig() + ``` + + :::danger Error: nil + Always ensure that the number of elements in the widths state is greater or equal to the + new number of columns when changing the number of columns. + ::: + ::: + + ```lua + hasChildren = true + hasState = false + Arguments = { + NumColumns: number, -- number of columns in the table, cannot be changed + Header: boolean? = false, -- display a header row for each column + RowBackground: boolean? = false, -- alternating row background colours + OuterBorders: boolean? = false, -- outer border on the entire table + InnerBorders: boolean? = false, -- inner bordres on the entire table + Resizable: boolean? = false, -- the columns can be resized by dragging or state + FixedWidth: boolean? = false, -- columns takes up a fixed pixel width, rather than a proportion of the total available + ProportionalWidth: boolean? = false, -- minimises the width of each column individually + LimitTableWidth: boolean? = false, -- when a fixed width, cut of any unused space + } + Events = { + hovered: () -> boolean + } + States = { + widths: State<{ number }>? -- the widths of each column if Resizable + } + ``` + ]=] + Iris.Table = wrapper("Table") + + --[=[ + @within Table + @function NextColumn + + In a table, moves to the next available cell. If the current cell is in the last column, + then moves to the cell in the first column of the next row. + ]=] + Iris.NextColumn = function(): number + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.NextColumn() can only called when directly within a table.") + + local columnIndex = Table._columnIndex + if columnIndex == Table.arguments.NumColumns then + Table._columnIndex = 1 + Table._rowIndex += 1 + else + Table._columnIndex += 1 + end + return Table._columnIndex + end + + --[=[ + @within Table + @function NextRow + + In a table, moves to the cell in the first column of the next row. + ]=] + Iris.NextRow = function(): number + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.NextRow() can only called when directly within a table.") + Table._columnIndex = 1 + Table._rowIndex += 1 + return Table._rowIndex + end + + --[=[ + @within Table + @function SetColumnIndex + @param index number + + In a table, moves to the cell in the given column in the same previous row. + + Will erorr if the given index is not in the range of 1 to NumColumns. + ]=] + Iris.SetColumnIndex = function(index: number): () + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.SetColumnIndex() can only called when directly within a table.") + assert((index >= 1) and (index <= Table.arguments.NumColumns), `The index must be between 1 and {Table.arguments.NumColumns}, inclusive.`) + Table._columnIndex = index + end + + --[=[ + @within Table + @function SetRowIndex + @param index number + + In a table, moves to the cell in the given row with the same previous column. + ]=] + Iris.SetRowIndex = function(index: number): () + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.SetRowIndex() can only called when directly within a table.") + assert(index >= 1, "The index must be greater or equal to 1.") + Table._rowIndex = index + end + + --[=[ + @within Table + @function NextHeaderColumn + + In a table, moves to the cell in the next column in the header row (row index 0). Will loop around + from the last column to the first. + ]=] + Iris.NextHeaderColumn = function(): number + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.NextHeaderColumn() can only called when directly within a table.") + + Table._rowIndex = 0 + Table._columnIndex = (Table._columnIndex % Table.arguments.NumColumns) + 1 + + return Table._columnIndex + end + + --[=[ + @within Table + @function SetHeaderColumnIndex + @param index number + + In a table, moves to the cell in the given column in the header row (row index 0). + + Will erorr if the given index is not in the range of 1 to NumColumns. + ]=] + Iris.SetHeaderColumnIndex = function(index: number): () + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.SetHeaderColumnIndex() can only called when directly within a table.") + assert((index >= 1) and (index <= Table.arguments.NumColumns), `The index must be between 1 and {Table.arguments.NumColumns}, inclusive.`) + + Table._rowIndex = 0 + Table._columnIndex = index + end + + --[=[ + @within Table + @function SetColumnWidth + @param index number + @param width number + + In a table, sets the width of the given column to the given value by changing the + Table's widths state. When the FixedWidth argument is true, the width should be in + pixels >2, otherwise as a float between 0 and 1. + + Will erorr if the given index is not in the range of 1 to NumColumns. + ]=] + Iris.SetColumnWidth = function(index: number, width: number): () + local Table = Iris.Internal._GetParentWidget() :: Types.Table + assert(Table ~= nil, "Iris.SetColumnWidth() can only called when directly within a table.") + assert((index >= 1) and (index <= Table.arguments.NumColumns), `The index must be between 1 and {Table.arguments.NumColumns}, inclusive.`) + + local oldValue = Table.state.widths.value[index] + Table.state.widths.value[index] = width + Table.state.widths:set(Table.state.widths.value, width ~= oldValue) + end +end diff --git a/src/DebuggerUI/Shared/External/iris/Internal.luau b/src/DebuggerUI/Shared/External/iris/Internal.luau new file mode 100644 index 0000000..6d293d4 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/Internal.luau @@ -0,0 +1,908 @@ +local HttpService: HttpService = game:GetService("HttpService") + +local Types = require(script.Parent.Types) + +return function(Iris: Types.Iris): Types.Internal + --[=[ + @class Internal + An internal class within Iris containing all the backend data and functions for Iris to operate. + It is recommended that you don't generally interact with Internal unless you understand what you are doing. + ]=] + local Internal = {} :: Types.Internal + + --[[ + --------------------------------- + [SECTION] Properties + --------------------------------- + ]] + + Internal._version = [[ 2.5.0 ]] + + Internal._started = false -- has Iris.connect been called yet + Internal._shutdown = false + Internal._cycleTick = 0 -- increments for each call to Cycle, used to determine the relative age and freshness of generated widgets + Internal._deltaTime = 0 + + -- Refresh + Internal._globalRefreshRequested = false -- refresh means that all GUI is destroyed and regenerated, usually because a style change was made and needed to be propogated to all UI + Internal._refreshCounter = 0 -- if true, when _Insert is called, the widget called will be regenerated + Internal._refreshLevel = 1 + Internal._refreshStack = table.create(16) + + -- Widgets & Instances + Internal._widgets = {} + Internal._stackIndex = 1 -- Points to the index that IDStack is currently in, when computing cycle + Internal._rootInstance = nil + Internal._rootWidget = { + ID = "R", + type = "Root", + Instance = Internal._rootInstance, + ZIndex = 0, + ZOffset = 0, + } + Internal._lastWidget = Internal._rootWidget -- widget which was most recently rendered + + -- Config + Internal._rootConfig = {} -- root style which all widgets derive from + Internal._config = Internal._rootConfig + + -- ID + Internal._IDStack = { "R" } + Internal._usedIDs = {} -- hash of IDs which are already used in a cycle, value is the # of occurances so that getID can assign a unique ID for each occurance + Internal._pushedIds = {} + Internal._newID = false + Internal._nextWidgetId = nil + + -- State + Internal._states = {} -- Iris.States + + -- Callback + Internal._postCycleCallbacks = {} + Internal._connectedFunctions = {} -- functions which run each Iris cycle, connected by the user + Internal._connections = {} + Internal._initFunctions = {} + + -- Error + Internal._fullErrorTracebacks = game:GetService("RunService"):IsStudio() + + --[=[ + @within Internal + @prop _cycleCoroutine thread + + The thread which handles all connected functions. Each connection is within a pcall statement which prevents + Iris from crashing and instead stopping at the error. + ]=] + Internal._cycleCoroutine = coroutine.create(function() + while Internal._started do + for _, callback: () -> string in Internal._connectedFunctions do + debug.profilebegin("Iris/Connection") + local status: boolean, _error: string = pcall(callback) + debug.profileend() + if not status then + -- any error reserts the _stackIndex for the next frame and yields the error. + Internal._stackIndex = 1 + coroutine.yield(false, _error) + end + end + -- after all callbacks, we yeild so it only runs once a frame. + coroutine.yield(true) + end + end) + + --[[ + ----------------------- + [SECTION] State + ----------------------- + ]] + + --[=[ + @class State + This class wraps a value in getters and setters, its main purpose is to allow primatives to be passed as objects. + Constructors for this class are available in [Iris] + + ```lua + local state = Iris.State(0) -- we initialise the state with a value of 0 + + -- these are equivalent. Ideally you should use `:get()` and ignore `.value`. + print(state:get()) + print(state.value) + + state:set(state:get() + 1) -- increments the state by getting the current value and adding 1. + + state:onChange(function(newValue) + print(`The value of the state is now: {newValue}`) + end) + ``` + + :::caution Caution: Callbacks + Never call `:set()` on a state when inside the `:onChange()` callback of the same state. This will cause a continous callback. + + Never chain states together so that each state changes the value of another state in a cyclic nature. This will cause a continous callback. + ::: + ]=] + local StateClass = {} + StateClass.__index = StateClass + + --[=[ + @within State + @method get + @return T + + Returns the states current value. + ]=] + function StateClass:get(): T -- you can also simply use .value + return self.value + end + + --[=[ + @within State + @method set + @param newValue T + @param force boolean? -- force an update to all connections + @return T + + Allows the caller to assign the state object a new value, and returns the new value. + ]=] + function StateClass:set(newValue: T, force: true?): T + if newValue == self.value and force ~= true then + -- no need to update on no change. + return self.value + end + self.value = newValue + self.lastChangeTick = Iris.Internal._cycleTick + for _, thisWidget: Types.Widget in self.ConnectedWidgets do + if thisWidget.lastCycleTick ~= -1 then + Internal._widgets[thisWidget.type].UpdateState(thisWidget) + end + end + + for _, callback in self.ConnectedFunctions do + callback(newValue) + end + return self.value + end + + --[=[ + @within State + @method onChange + @param callback (newValue: T) -> () + @return () -> () + + Allows the caller to connect a callback which is called when the states value is changed. + + :::caution Caution: Single + Calling `:onChange()` every frame will add a new function every frame. + You must ensure you are only calling `:onChange()` once for each callback for the state's entire lifetime. + ::: + ]=] + function StateClass:onChange(callback: (newValue: T) -> ()): () -> () + local connectionIndex: number = #self.ConnectedFunctions + 1 + self.ConnectedFunctions[connectionIndex] = callback + return function() + self.ConnectedFunctions[connectionIndex] = nil + end + end + + --[=[ + @within State + @method changed + @return boolean + + Returns true if the state was changed on this frame. + ]=] + function StateClass:changed(): boolean + return self.lastChangeTick + 1 == Internal._cycleTick + end + + Internal.StateClass = StateClass + + --[[ + --------------------------- + [SECTION] Functions + --------------------------- + ]] + + --[=[ + @within Internal + @function _cycle + + Called every frame to handle all of the widget management. Any previous frame data is ammended and everything updates. + ]=] + function Internal._cycle(deltaTime: number) + -- debug.profilebegin("Iris/Cycle") + if Iris.Disabled then + return -- Stops all rendering, effectively freezes the current frame with no interaction. + end + + Internal._rootWidget.lastCycleTick = Internal._cycleTick + if Internal._rootInstance == nil or Internal._rootInstance.Parent == nil then + Iris.ForceRefresh() + end + + for _, widget in Internal._lastVDOM do + if widget.lastCycleTick ~= Internal._cycleTick and (widget.lastCycleTick ~= -1) then + -- a widget which used to be rendered was not called last frame, so we discard it. + -- if the cycle tick is -1 we have already discarded it. + Internal._DiscardWidget(widget) + end + end + + -- represents all widgets created last frame. We keep the _lastVDOM to reuse widgets from the previous frame + -- rather than creating a new instance every frame. + setmetatable(Internal._lastVDOM, { __mode = "kv" }) + Internal._lastVDOM = Internal._VDOM + Internal._VDOM = Internal._generateEmptyVDOM() + + -- anything that wnats to run before the frame. + task.spawn(function() + -- debug.profilebegin("Iris/PostCycleCallbacks") + for _, callback in Internal._postCycleCallbacks do + callback() + end + -- debug.profileend() + end) + + if Internal._globalRefreshRequested then + -- rerender every widget + --debug.profilebegin("Iris Refresh") + Internal._generateSelectionImageObject() + Internal._globalRefreshRequested = false + for _, widget in Internal._lastVDOM do + Internal._DiscardWidget(widget) + end + Internal._generateRootInstance() + Internal._lastVDOM = Internal._generateEmptyVDOM() + --debug.profileend() + end + + -- update counters + Internal._cycleTick += 1 + Internal._deltaTime = deltaTime + table.clear(Internal._usedIDs) + + -- if Internal.parentInstance:IsA("GuiBase2d") and math.min(Internal.parentInstance.AbsoluteSize.X, Internal.parentInstance.AbsoluteSize.Y) < 100 then + -- error("Iris Parent Instance is too small") + -- end + 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("The Iris parent instance will not display any GUIs.") + end + + -- if we are running in Studio, we want full error tracebacks, so we don't have + -- any pcall to protect from an error. + if Internal._fullErrorTracebacks then + -- debug.profilebegin("Iris/Cycle/Callback") + for _, callback in Internal._connectedFunctions do + callback() + end + else + -- debug.profilebegin("Iris/Cycle/Coroutine") + + -- each frame we check on our thread status. + local coroutineStatus = coroutine.status(Internal._cycleCoroutine) + if coroutineStatus == "suspended" then + -- suspended means it yielded, either because it was a complete success + -- or it caught an error in the code. We run it again for this frame. + local _, success, result = coroutine.resume(Internal._cycleCoroutine) + if success == false then + -- Connected function code errored + error(result, 0) + end + elseif coroutineStatus == "running" then + -- still running (probably because of an asynchronous method inside a connection). + error("Iris cycleCoroutine took to long to yield. Connected functions should not yield.") + else + -- should never reach this (nothing you can do). + error("unrecoverable state") + end + -- debug.profileend() + end + + if Internal._stackIndex ~= 1 then + -- has to be larger than 1 because of the check that it isnt below 1 in Iris.End + Internal._stackIndex = 1 + error("Too few calls to Iris.End().", 0) + end + + -- Errors if the end user forgot to pop all their ids as they would leak over into the next frame + -- could also just clear, but that might be confusing behaviour. + if #Internal._pushedIds ~= 0 then + error("Too few calls to Iris.PopId().", 0) + end + + -- debug.profileend() + end + + --[=[ + @within Internal + @ignore + @function _NoOp + + A dummy function which does nothing. Used as a placeholder for optional methods in a widget class. + Used in `Internal.WidgetConstructor` + ]=] + function Internal._NoOp() end + + -- Widget + + --[=[ + @within Internal + @function WidgetConstructor + @param type string -- name used to denote the widget class. + @param widgetClass Types.WidgetClass -- table of methods for the new widget. + + For each widget, a widget class is created which handles all the operations of a widget. This removes the class nature + of widgets, and simplifies the available functions which can be applied to any widget. The widgets themselves are + dumb tables containing all the data but no methods to handle any of the data apart from events. + ]=] + function Internal.WidgetConstructor(type: string, widgetClass: Types.WidgetClass) + local Fields: { [string]: { [string]: { string } } } = { + All = { + Required = { + "Generate", -- generates the instance. + "Discard", + "Update", + + -- not methods ! + "Args", + "Events", + "hasChildren", + "hasState", + }, + Optional = {}, + }, + IfState = { + Required = { + "GenerateState", + "UpdateState", + }, + Optional = {}, + }, + IfChildren = { + Required = { + "ChildAdded", -- returns the parent of the child widget. + }, + Optional = { + "ChildDiscarded", + }, + }, + } + + -- we ensure all essential functions and properties are present, otherwise the code will break later. + -- some functions will only be needed if the widget has children or has state. + local thisWidget = {} :: Types.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 + -- assign a dummy function which does nothing. + 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 + + -- an internal table of all widgets to the widget class. + Internal._widgets[type] = thisWidget + -- allowing access to the index for each widget argument. + Iris.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 Iris.Events[index] == nil then + Iris.Events[index] = function() + return Internal._EventCall(Internal._lastWidget, index) + end + end + end + end + + --[=[ + @within Internal + @function _Insert + @param widgetType: string -- name of widget class. + @param arguments { [string]: number } -- arguments of the widget. + @param states { [string]: States }? -- states of the widget. + @return Widget -- the widget. + + Every widget is created through _Insert. An ID is generated based on the line of the calling code and is used to + find the previous frame widget if it exists. If no widget exists, a new one is created. + ]=] + function Internal._Insert(widgetType: string, args: Types.WidgetArguments?, states: Types.WidgetStates?): Types.Widget + local ID: Types.ID = Internal._getID(3) + --debug.profilebegin(ID) + + -- fetch the widget class which contains all the functions for the widget. + local thisWidgetClass: Types.WidgetClass = Internal._widgets[widgetType] + + if Internal._VDOM[ID] then + -- widget already created once this frame, so we can append to it. + return Internal._ContinueWidget(ID, widgetType) + end + + local arguments: Types.Arguments = {} :: Types.Arguments + if args ~= nil then + if type(args) ~= "table" then + args = { args } + end + + -- convert the arguments to a key-value dictionary so arguments can be referred to by their name and not index. + for index: number, argument: Types.Argument in args do + assert(index > 0, `Widget Arguments must be a positive number, not {index} of type {typeof(index)} for {argument}.`) + arguments[thisWidgetClass.ArgNames[index]] = argument + end + end + -- prevents tampering with the arguments which are used to check for changes. + table.freeze(arguments) + + local lastWidget: Types.Widget? = Internal._lastVDOM[ID] + if lastWidget and widgetType == lastWidget.type then + -- found a matching widget from last frame. + if Internal._refreshCounter > 0 then + -- we are redrawing every widget. + Internal._DiscardWidget(lastWidget) + lastWidget = nil + end + end + local thisWidget: Types.Widget = if lastWidget == nil then Internal._GenNewWidget(widgetType, arguments, states, ID) else lastWidget + + local parentWidget: Types.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 + + -- since rows are not instances, but will be removed if not updated, we have to add specific table code. + if parentWidget.type == "Table" then + local Table = parentWidget :: Types.Table + Table._rowCycles[Table._rowIndex] = Internal._cycleTick + end + + if Internal._deepCompare(thisWidget.providedArguments, arguments) == false then + -- the widgets arguments have changed, the widget should update to reflect changes. + -- providedArguments is the frozen table which will not change. + -- the arguments can be altered internally, which happens for the input widgets. + 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 :: Types.ParentWidget + -- a parent widget, so we increase our depth. + thisParent.ZOffset = 0 + thisParent.ZUpdate = false + Internal._stackIndex += 1 + Internal._IDStack[Internal._stackIndex] = thisWidget.ID + end + + Internal._VDOM[ID] = thisWidget + Internal._lastWidget = thisWidget + + --debug.profileend() + + return thisWidget + end + + --[=[ + @within Internal + @function _GenNewWidget + @param widgetType string + @param arguments { [string]: any } -- arguments of the widget. + @param states { [string]: State }? -- states of the widget. + @param ID ID -- id of the new widget. Determined in `Internal._Insert` + @return Widget -- the newly created widget. + + All widgets are created as tables with properties. The widget class contains the functions to create the UI instances and + update the widget or change state. + ]=] + function Internal._GenNewWidget(widgetType: string, arguments: Types.Arguments, states: Types.WidgetStates?, ID: Types.ID): Types.Widget + local parentId: Types.ID = Internal._IDStack[Internal._stackIndex] + local parentWidget: Types.ParentWidget = Internal._VDOM[parentId] + local thisWidgetClass: Types.WidgetClass = Internal._widgets[widgetType] + + -- widgets are just tables with properties. + local thisWidget = {} :: Types.Widget + setmetatable(thisWidget, thisWidget) + + thisWidget.ID = ID + thisWidget.type = widgetType + thisWidget.parentWidget = parentWidget + thisWidget.trackedEvents = {} + thisWidget.UID = HttpService:GenerateGUID(false):sub(0, 8) + + -- widgets have lots of space to ensure they are always visible. + thisWidget.ZIndex = parentWidget.ZOffset + + thisWidget.Instance = thisWidgetClass.Generate(thisWidget) + -- tooltips set their parent in the generation method, so we need to udpate it here + 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 + + -- we can modify the arguments table, but keep a frozen copy to compare for user-end changes. + thisWidget.providedArguments = arguments + thisWidget.arguments = Internal._deepCopy(arguments) + thisWidgetClass.Update(thisWidget) + + local eventMTParent + if thisWidgetClass.hasState then + local stateWidget = thisWidget :: Types.StateWidget + if states then + for index: string, state: Types.State in states do + if not (type(state) == "table" and getmetatable(state :: any) == Internal.StateClass) then + -- generate a new state. + states[index] = Internal._widgetState(stateWidget, index, state) + end + states[index].lastChangeTick = Internal._cycleTick + end + + stateWidget.state = states + for _, state: Types.State in states do + state.ConnectedWidgets[stateWidget.ID] = stateWidget + end + else + stateWidget.state = {} + end + + thisWidgetClass.GenerateState(stateWidget) + thisWidgetClass.UpdateState(stateWidget) + + -- the state MT can't be itself because state has to explicitly only contain stateClass objects + 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 + + --[=[ + @within Internal + @function _ContinueWidget + @param ID ID -- id of the widget. + @param widgetType string + @return Widget -- the widget. + + Since the widget has already been created this frame, we can just add it back to the stack. There is no checking of + arguments or states. + Basically equivalent to the end of `Internal._Insert`. + ]=] + function Internal._ContinueWidget(ID: Types.ID, widgetType: string): Types.Widget + local thisWidgetClass: Types.WidgetClass = Internal._widgets[widgetType] + local thisWidget: Types.Widget = Internal._VDOM[ID] + + if thisWidgetClass.hasChildren then + -- a parent widget so we increase our depth. + Internal._stackIndex += 1 + Internal._IDStack[Internal._stackIndex] = thisWidget.ID + end + + Internal._lastWidget = thisWidget + return thisWidget + end + + --[=[ + @within Internal + @function _DiscardWidget + @param widgetToDiscard Widget + + Destroys the widget instance and updates any parent. This happens if the widget was not called in the + previous frame. There is no code which needs to update any widget tables since they are already reset + at the start before discarding happens. + ]=] + function Internal._DiscardWidget(widgetToDiscard: Types.Widget) + local widgetParent = widgetToDiscard.parentWidget + if widgetParent then + -- if the parent needs to update it's children. + Internal._widgets[widgetParent.type].ChildDiscarded(widgetParent, widgetToDiscard) + end + + -- using the widget class discard function. + Internal._widgets[widgetToDiscard.type].Discard(widgetToDiscard) + + -- mark as discarded + widgetToDiscard.lastCycleTick = -1 + end + + --[=[ + @within Internal + @function _widgetState + @param thisWidget Widget -- widget the state belongs to. + @param stateName string + @param initialValue any + @return State -- the state for the widget. + + Connects the state to the widget. If no state exists then a new one is created. Called for every state in every + widget if the user does not provide a state. + ]=] + function Internal._widgetState(thisWidget: Types.StateWidget, stateName: string, initialValue: any): Types.State + local ID: Types.ID = thisWidget.ID .. stateName + if Internal._states[ID] then + Internal._states[ID].ConnectedWidgets[thisWidget.ID] = thisWidget + Internal._states[ID].lastChangeTick = Internal._cycleTick + return Internal._states[ID] + else + Internal._states[ID] = { + ID = ID, + value = initialValue, + lastChangeTick = Internal._cycleTick, + ConnectedWidgets = { [thisWidget.ID] = thisWidget }, + ConnectedFunctions = {}, + } + setmetatable(Internal._states[ID], Internal.StateClass) + return Internal._states[ID] + end + end + + --[=[ + @within Internal + @function _EventCall + @param thisWidget Widget + @param evetName string + @return boolean -- the value of the event. + + A wrapper for any event on any widget. Automatically, Iris does not initialize events unless they are explicitly + called so in the first frame, the event connections are set up. Every event is a function which returns a boolean. + ]=] + function Internal._EventCall(thisWidget: Types.Widget, eventName: string): boolean + local Events: Types.Events = Internal._widgets[thisWidget.type].Events + local Event: Types.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 + + --[=[ + @within Internal + @function _GetParentWidget + @return Widget -- the parent widget + + Returns the parent widget of the currently active widget, based on the stack depth. + ]=] + function Internal._GetParentWidget(): Types.ParentWidget + return Internal._VDOM[Internal._IDStack[Internal._stackIndex]] + end + + -- Generate + + --[=[ + @ignore + @within Internal + @function _generateEmptyVDOM + @return { [ID]: Widget } + + Creates the VDOM at the start of each frame containing just the root instance. + ]=] + function Internal._generateEmptyVDOM(): { [Types.ID]: Types.Widget } + return { + ["R"] = Internal._rootWidget, + } + end + + --[=[ + @ignore + @within Internal + @function _generateRootInstance + + Creates the root instance. + ]=] + function Internal._generateRootInstance() + -- unsafe to call before Internal.connect + Internal._rootInstance = Internal._widgets["Root"].Generate(Internal._widgets["Root"]) + Internal._rootInstance.Parent = Internal.parentInstance + Internal._rootWidget.Instance = Internal._rootInstance + end + + --[=[ + @ignore + @within Internal + @function _generateSelctionImageObject + + Creates the selection object for buttons. + ]=] + 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 + + -- Utility + + --[=[ + @within Internal + @function _getID + @param levelsToIgnore number -- used to skip over internal calls to `_getID`. + @return ID + + Generates a unique ID for each widget which is based on the line that the widget is + created from. This ensures that the function is heuristic and always returns the same + id for the same widget. + ]=] + function Internal._getID(levelsToIgnore: number): Types.ID + if Internal._nextWidgetId then + local ID: Types.ID = Internal._nextWidgetId + Internal._nextWidgetId = nil + return ID + end + + local i: number = 1 + (levelsToIgnore or 1) + local ID: Types.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 + + local discriminator = Internal._usedIDs[ID] + if discriminator then + Internal._usedIDs[ID] += 1 + discriminator += 1 + else + Internal._usedIDs[ID] = 1 + discriminator = 1 + end + + if #Internal._pushedIds == 0 then + return ID .. ":" .. discriminator + elseif Internal._newID then + Internal._newID = false + return ID .. "::" .. table.concat(Internal._pushedIds, "\\") + else + return ID .. ":" .. discriminator .. ":" .. table.concat(Internal._pushedIds, "\\") + end + end + + --[=[ + @ignore + @within Internal + @function _deepCompare + @param t1 {} + @param t2 {} + @return boolean + + Compares two tables to check if they are the same. It uses a recursive iteration through one table + to compare against the other. Used to determine if the arguments of a widget have changed since last + frame. + ]=] + function Internal._deepCompare(t1: {}, t2: {}): boolean + -- unoptimized ? + 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 + + --[=[ + @ignore + @within Internal + @function _deepCopy + @param t {} + @return {} + + Performs a deep copy of a table so that neither table contains a shared reference. + ]=] + 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 + + -- VDOM + Internal._lastVDOM = Internal._generateEmptyVDOM() + Internal._VDOM = Internal._generateEmptyVDOM() + + Iris.Internal = Internal + Iris._config = Internal._config + return Internal +end diff --git a/src/DebuggerUI/Shared/External/iris/PubTypes.luau b/src/DebuggerUI/Shared/External/iris/PubTypes.luau new file mode 100644 index 0000000..f7fc0ea --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/PubTypes.luau @@ -0,0 +1,43 @@ +local Types = require(script.Parent.Types) + +export type ID = Types.ID +export type State = Types.State + +export type Widget = Types.Widget +export type Root = Types.Root +export type Window = Types.Window +export type Tooltip = Types.Tooltip +export type MenuBar = Types.MenuBar +export type Menu = Types.Menu +export type MenuItem = Types.MenuItem +export type MenuToggle = Types.MenuToggle +export type Separator = Types.Separator +export type Indent = Types.Indent +export type SameLine = Types.SameLine +export type Group = Types.Group +export type Text = Types.Text +export type SeparatorText = Types.SeparatorText +export type Button = Types.Button +export type Checkbox = Types.Checkbox +export type RadioButton = Types.RadioButton +export type Image = Types.Image +export type ImageButton = Types.ImageButton +export type Tree = Types.Tree +export type CollapsingHeader = Types.CollapsingHeader +export type TabBar = Types.TabBar +export type Tab = Types.Tab +export type Input = Types.Input +export type InputColor3 = Types.InputColor3 +export type InputColor4 = Types.InputColor4 +export type InputEnum = Types.InputEnum +export type InputText = Types.InputText +export type Selectable = Types.Selectable +export type Combo = Types.Combo +export type ProgressBar = Types.ProgressBar +export type PlotLines = Types.PlotLines +export type PlotHistogram = Types.PlotHistogram +export type Table = Types.Table + +export type Iris = Types.Iris + +return {} diff --git a/src/DebuggerUI/Shared/External/iris/Types.luau b/src/DebuggerUI/Shared/External/iris/Types.luau new file mode 100644 index 0000000..0c1fead --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/Types.luau @@ -0,0 +1,659 @@ +local WidgetTypes = require(script.Parent.WidgetTypes) + +export type ID = WidgetTypes.ID +export type State = WidgetTypes.State + +export type Hovered = WidgetTypes.Hovered +export type Clicked = WidgetTypes.Clicked +export type RightClicked = WidgetTypes.RightClicked +export type DoubleClicked = WidgetTypes.DoubleClicked +export type CtrlClicked = WidgetTypes.CtrlClicked +export type Active = WidgetTypes.Active +export type Checked = WidgetTypes.Checked +export type Unchecked = WidgetTypes.Unchecked +export type Opened = WidgetTypes.Opened +export type Closed = WidgetTypes.Closed +export type Collapsed = WidgetTypes.Collapsed +export type Uncollapsed = WidgetTypes.Uncollapsed +export type Selected = WidgetTypes.Selected +export type Unselected = WidgetTypes.Unselected +export type Changed = WidgetTypes.Changed +export type NumberChanged = WidgetTypes.NumberChanged +export type TextChanged = WidgetTypes.TextChanged + +export type Widget = WidgetTypes.Widget +export type ParentWidget = WidgetTypes.ParentWidget +export type StateWidget = WidgetTypes.StateWidget + +export type Root = WidgetTypes.Root +export type Window = WidgetTypes.Window +export type Tooltip = WidgetTypes.Tooltip +export type MenuBar = WidgetTypes.MenuBar +export type Menu = WidgetTypes.Menu +export type MenuItem = WidgetTypes.MenuItem +export type MenuToggle = WidgetTypes.MenuToggle +export type Separator = WidgetTypes.Separator +export type Indent = WidgetTypes.Indent +export type SameLine = WidgetTypes.SameLine +export type Group = WidgetTypes.Group +export type Text = WidgetTypes.Text +export type SeparatorText = WidgetTypes.SeparatorText +export type Button = WidgetTypes.Button +export type Checkbox = WidgetTypes.Checkbox +export type RadioButton = WidgetTypes.RadioButton +export type Image = WidgetTypes.Image +export type ImageButton = WidgetTypes.ImageButton +export type Tree = WidgetTypes.Tree +export type CollapsingHeader = WidgetTypes.CollapsingHeader +export type TabBar = WidgetTypes.TabBar +export type Tab = WidgetTypes.Tab +export type Input = WidgetTypes.Input +export type InputColor3 = WidgetTypes.InputColor3 +export type InputColor4 = WidgetTypes.InputColor4 +export type InputEnum = WidgetTypes.InputEnum +export type InputText = WidgetTypes.InputText +export type Selectable = WidgetTypes.Selectable +export type Combo = WidgetTypes.Combo +export type ProgressBar = WidgetTypes.ProgressBar +export type PlotLines = WidgetTypes.PlotLines +export type PlotHistogram = WidgetTypes.PlotHistogram +export type Table = WidgetTypes.Table + +export type InputDataType = number | Vector2 | Vector3 | UDim | UDim2 | Color3 | Rect | { number } + +export type Argument = any +export type Arguments = { + [string]: Argument, + Text: string, + TextHint: string, + TextOverlay: string, + ReadOnly: boolean, + MultiLine: boolean, + Wrapped: boolean, + Color: Color3, + RichText: boolean, + + Increment: InputDataType, + Min: InputDataType, + Max: InputDataType, + Format: { string }, + UseFloats: boolean, + UseHSV: boolean, + UseHex: boolean, + Prefix: { string }, + BaseLine: number, + + Width: number, + Height: number, + VerticalAlignment: Enum.VerticalAlignment, + HorizontalAlignment: Enum.HorizontalAlignment, + Index: any, + Image: string, + Size: UDim2, + Rect: Rect, + ScaleType: Enum.ScaleType, + TileSize: UDim2, + SliceCenter: Rect, + SliceScale: number, + ResampleMode: Enum.ResamplerMode, + + SpanAvailWidth: boolean, + NoIdent: boolean, + NoClick: boolean, + NoButtons: boolean, + NoButton: boolean, + NoPreview: boolean, + + NumColumns: number, + RowBg: boolean, + BordersOuter: boolean, + BordersInner: boolean, + + Title: string, + NoTitleBar: boolean, + NoBackground: boolean, + NoCollapse: boolean, + NoClose: boolean, + NoMove: boolean, + NoScrollbar: boolean, + NoResize: boolean, + NoMenu: boolean, + + KeyCode: Enum.KeyCode, + ModifierKey: Enum.ModifierKey, + Disabled: boolean, +} + +export type States = { + [string]: State, + number: State, + color: State, + transparency: State, + editingText: State, + index: State, + + size: State, + position: State, + progress: State, + scrollDistance: State, + + isChecked: State, + isOpened: State, + isUncollapsed: State, +} + +export type Event = { + Init: (Widget) -> (), + Get: (Widget) -> boolean, +} +export type Events = { [string]: Event } + +-- Widgets + +export type WidgetArguments = { [number]: Argument } +export type WidgetStates = { + [string]: State, + number: State?, + color: State?, + transparency: State?, + editingText: State?, + index: State?, + + size: State?, + position: State?, + progress: State?, + scrollDistance: State?, + values: State?, + + isChecked: State?, + isOpened: State?, + isUncollapsed: State?, +} + +export type WidgetClass = { + Generate: (thisWidget: Widget) -> GuiObject, + Discard: (thisWidget: Widget) -> (), + Update: (thisWidget: Widget, ...any) -> (), + + Args: { [string]: number }, + Events: Events, + hasChildren: boolean, + hasState: boolean, + ArgNames: { [number]: string }, + + GenerateState: (thisWidget: Widget) -> (), + UpdateState: (thisWidget: Widget) -> (), + + ChildAdded: (thisWidget: Widget, thisChild: Widget) -> GuiObject, + ChildDiscarded: (thisWidget: Widget, thisChild: Widget) -> (), +} + +-- Iris + +export type Internal = { + --[[ + -------------- + PROPERTIES + -------------- + ]] + _version: string, + _started: boolean, + _shutdown: boolean, + _cycleTick: number, + _deltaTime: number, + _eventConnection: RBXScriptConnection?, + + -- Refresh + _globalRefreshRequested: boolean, + _refreshCounter: number, + _refreshLevel: number, + _refreshStack: { boolean }, + + -- Widgets & Instances + _widgets: { [string]: WidgetClass }, + _widgetCount: number, + _stackIndex: number, + _rootInstance: GuiObject?, + _rootWidget: ParentWidget, + _lastWidget: Widget, + SelectionImageObject: Frame, + parentInstance: Instance, + _utility: WidgetUtility, + + -- Config + _rootConfig: Config, + _config: Config, + + -- ID + _IDStack: { ID }, + _usedIDs: { [ID]: number }, + _newID: boolean, + _pushedIds: { ID }, + _nextWidgetId: ID?, + + -- VDOM + _lastVDOM: { [ID]: Widget }, + _VDOM: { [ID]: Widget }, + + -- State + _states: { [ID]: State }, + + -- Callback + _postCycleCallbacks: { () -> () }, + _connectedFunctions: { () -> () }, + _connections: { RBXScriptConnection }, + _initFunctions: { () -> () }, + _cycleCoroutine: thread?, + + --[[ + --------- + STATE + --------- + ]] + + StateClass: { + __index: any, + + get: (self: State) -> any, + set: (self: State, newValue: any) -> any, + onChange: (self: State, callback: (newValue: any) -> ()) -> (), + }, + + --[[ + ------------- + FUNCTIONS + ------------- + ]] + _cycle: (deltaTime: number) -> (), + _NoOp: () -> (), + + -- Widget + WidgetConstructor: (type: string, widgetClass: WidgetClass) -> (), + _Insert: (widgetType: string, arguments: WidgetArguments?, states: WidgetStates?) -> Widget, + _GenNewWidget: (widgetType: string, arguments: Arguments, states: WidgetStates?, ID: ID) -> Widget, + _ContinueWidget: (ID: ID, widgetType: string) -> Widget, + _DiscardWidget: (widgetToDiscard: Widget) -> (), + + _widgetState: (thisWidget: Widget, stateName: string, initialValue: any) -> State, + _EventCall: (thisWidget: Widget, eventName: string) -> boolean, + _GetParentWidget: () -> ParentWidget, + SetFocusedWindow: (thisWidget: WidgetTypes.Window?) -> (), + + -- Generate + _generateEmptyVDOM: () -> { [ID]: Widget }, + _generateRootInstance: () -> (), + _generateSelectionImageObject: () -> (), + + -- Utility + _getID: (levelsToIgnore: number) -> ID, + _deepCompare: (t1: {}, t2: {}) -> boolean, + _deepCopy: (t: {}) -> {}, +} + +export type WidgetUtility = { + GuiService: GuiService, + RunService: RunService, + TextService: TextService, + UserInputService: UserInputService, + ContextActionService: ContextActionService, + + getTime: () -> number, + getMouseLocation: () -> Vector2, + + ICONS: { + BLANK_SQUARE: string, + RIGHT_POINTING_TRIANGLE: string, + DOWN_POINTING_TRIANGLE: string, + MULTIPLICATION_SIGN: string, + BOTTOM_RIGHT_CORNER: string, + CHECK_MARK: string, + BORDER: string, + ALPHA_BACKGROUND_TEXTURE: string, + UNKNOWN_TEXTURE: string, + }, + + GuiOffset: Vector2, + MouseOffset: Vector2, + + findBestWindowPosForPopup: (refPos: Vector2, size: Vector2, outerMin: Vector2, outerMax: Vector2) -> Vector2, + getScreenSizeForWindow: (thisWidget: Widget) -> Vector2, + isPosInsideRect: (pos: Vector2, rectMin: Vector2, rectMax: Vector2) -> boolean, + extend: (superClass: WidgetClass, { [any]: any }) -> WidgetClass, + discardState: (thisWidget: Widget) -> (), + + UIPadding: (Parent: GuiObject, PxPadding: Vector2) -> UIPadding, + UIListLayout: (Parent: GuiObject, FillDirection: Enum.FillDirection, Padding: UDim) -> UIListLayout, + UIStroke: (Parent: GuiObject, Thickness: number, Color: Color3, Transparency: number) -> UIStroke, + UICorner: (Parent: GuiObject, PxRounding: number?) -> UICorner, + UISizeConstraint: (Parent: GuiObject, MinSize: Vector2?, MaxSize: Vector2?) -> UISizeConstraint, + + applyTextStyle: (thisInstance: TextLabel | TextButton | TextBox) -> (), + applyInteractionHighlights: (Property: string, Button: GuiButton, Highlightee: GuiObject, Colors: { [string]: any }) -> (), + applyInteractionHighlightsWithMultiHighlightee: (Property: string, Button: GuiButton, Highlightees: { { GuiObject | { [string]: Color3 | number } } }) -> (), + applyFrameStyle: (thisInstance: GuiObject, noPadding: boolean?, noCorner: boolean?) -> (), + + applyButtonClick: (thisInstance: GuiButton, callback: () -> ()) -> (), + applyButtonDown: (thisInstance: GuiButton, callback: (x: number, y: number) -> ()) -> (), + applyMouseEnter: (thisInstance: GuiObject, callback: (x: number, y: number) -> ()) -> (), + applyMouseMoved: (thisInstance: GuiObject, callback: (x: number, y: number) -> ()) -> (), + applyMouseLeave: (thisInstance: GuiObject, callback: (x: number, y: number) -> ()) -> (), + applyInputBegan: (thisInstance: GuiObject, callback: (input: InputObject) -> ()) -> (), + applyInputEnded: (thisInstance: GuiObject, callback: (input: InputObject) -> ()) -> (), + + registerEvent: (event: string, callback: (...any) -> ()) -> (), + + EVENTS: { + hover: (pathToHovered: (thisWidget: Widget & Hovered) -> GuiObject) -> Event, + click: (pathToClicked: (thisWidget: Widget & Clicked) -> GuiButton) -> Event, + rightClick: (pathToClicked: (thisWidget: Widget & RightClicked) -> GuiButton) -> Event, + doubleClick: (pathToClicked: (thisWidget: Widget & DoubleClicked) -> GuiButton) -> Event, + ctrlClick: (pathToClicked: (thisWidget: Widget & CtrlClicked) -> GuiButton) -> Event, + }, + + abstractButton: WidgetClass, +} + +export type Config = { + TextColor: Color3, + TextTransparency: number, + TextDisabledColor: Color3, + TextDisabledTransparency: number, + + BorderColor: Color3, + BorderActiveColor: Color3, + BorderTransparency: number, + BorderActiveTransparency: number, + + WindowBgColor: Color3, + WindowBgTransparency: number, + ScrollbarGrabColor: Color3, + ScrollbarGrabTransparency: number, + PopupBgColor: Color3, + PopupBgTransparency: number, + + TitleBgColor: Color3, + TitleBgTransparency: number, + TitleBgActiveColor: Color3, + TitleBgActiveTransparency: number, + TitleBgCollapsedColor: Color3, + TitleBgCollapsedTransparency: number, + + MenubarBgColor: Color3, + MenubarBgTransparency: number, + + FrameBgColor: Color3, + FrameBgTransparency: number, + FrameBgHoveredColor: Color3, + FrameBgHoveredTransparency: number, + FrameBgActiveColor: Color3, + FrameBgActiveTransparency: number, + + ButtonColor: Color3, + ButtonTransparency: number, + ButtonHoveredColor: Color3, + ButtonHoveredTransparency: number, + ButtonActiveColor: Color3, + ButtonActiveTransparency: number, + + ImageColor: Color3, + ImageTransparency: number, + + SliderGrabColor: Color3, + SliderGrabTransparency: number, + SliderGrabActiveColor: Color3, + SliderGrabActiveTransparency: number, + + HeaderColor: Color3, + HeaderTransparency: number, + HeaderHoveredColor: Color3, + HeaderHoveredTransparency: number, + HeaderActiveColor: Color3, + HeaderActiveTransparency: number, + + TabColor: Color3, + TabTransparency: number, + TabHoveredColor: Color3, + TabHoveredTransparency: number, + TabActiveColor: Color3, + TabActiveTransparency: number, + + SelectionImageObjectColor: Color3, + SelectionImageObjectTransparency: number, + SelectionImageObjectBorderColor: Color3, + SelectionImageObjectBorderTransparency: number, + + TableBorderStrongColor: Color3, + TableBorderStrongTransparency: number, + TableBorderLightColor: Color3, + TableBorderLightTransparency: number, + TableRowBgColor: Color3, + TableRowBgTransparency: number, + TableRowBgAltColor: Color3, + TableRowBgAltTransparency: number, + TableHeaderColor: Color3, + TableHeaderTransparency: number, + + NavWindowingHighlightColor: Color3, + NavWindowingHighlightTransparency: number, + NavWindowingDimBgColor: Color3, + NavWindowingDimBgTransparency: number, + + SeparatorColor: Color3, + SeparatorTransparency: number, + + CheckMarkColor: Color3, + CheckMarkTransparency: number, + + PlotLinesColor: Color3, + PlotLinesTransparency: number, + PlotLinesHoveredColor: Color3, + PlotLinesHoveredTransparency: number, + PlotHistogramColor: Color3, + PlotHistogramTransparency: number, + PlotHistogramHoveredColor: Color3, + PlotHistogramHoveredTransparency: number, + + ResizeGripColor: Color3, + ResizeGripTransparency: number, + ResizeGripHoveredColor: Color3, + ResizeGripHoveredTransparency: number, + ResizeGripActiveColor: Color3, + ResizeGripActiveTransparency: number, + + HoverColor: Color3, + HoverTransparency: number, + + -- Sizes + ItemWidth: UDim, + ContentWidth: UDim, + ContentHeight: UDim, + + WindowPadding: Vector2, + WindowResizePadding: Vector2, + FramePadding: Vector2, + ItemSpacing: Vector2, + ItemInnerSpacing: Vector2, + CellPadding: Vector2, + DisplaySafeAreaPadding: Vector2, + IndentSpacing: number, + SeparatorTextPadding: Vector2, + + TextFont: Font, + TextSize: number, + FrameBorderSize: number, + FrameRounding: number, + GrabRounding: number, + WindowBorderSize: number, + WindowTitleAlign: Enum.LeftRight, + PopupBorderSize: number, + PopupRounding: number, + ScrollbarSize: number, + GrabMinSize: number, + SeparatorTextBorderSize: number, + ImageBorderSize: number, + + UseScreenGUIs: boolean, + IgnoreGuiInset: boolean, + Parent: BasePlayerGui, + RichText: boolean, + TextWrapped: boolean, + DisplayOrderOffset: number, + ZIndexOffset: number, + + MouseDoubleClickTime: number, + MouseDoubleClickMaxDist: number, + MouseDragThreshold: number, +} + +type WidgetCall = (arguments: A, states: S, E...) -> W + +export type Iris = { + --[[ + ----------- + WIDGETS + ----------- + ]] + + End: () -> (), + + -- Window API + Window: WidgetCall, + Tooltip: WidgetCall, + + -- Menu Widget API + MenuBar: () -> Widget, + Menu: WidgetCall, + MenuItem: WidgetCall, + MenuToggle: WidgetCall, + + -- Format Widget API + Separator: () -> Separator, + Indent: (arguments: WidgetArguments?) -> Indent, + SameLine: (arguments: WidgetArguments?) -> SameLine, + Group: () -> Group, + + -- Text Widget API + Text: WidgetCall, + TextWrapped: WidgetCall, + TextColored: WidgetCall, + SeparatorText: WidgetCall, + InputText: WidgetCall, + + -- Basic Widget API + Button: WidgetCall, + SmallButton: WidgetCall, + Checkbox: WidgetCall, + RadioButton: WidgetCall, + + -- Tree Widget API + Tree: WidgetCall, + CollapsingHeader: WidgetCall, + + -- Tab Widget API + TabBar: WidgetCall, + Tab: WidgetCall, + + -- Input Widget API + InputNum: WidgetCall, WidgetArguments, WidgetStates?>, + InputVector2: WidgetCall, WidgetArguments, WidgetStates?>, + InputVector3: WidgetCall, WidgetArguments, WidgetStates?>, + InputUDim: WidgetCall, WidgetArguments, WidgetStates?>, + InputUDim2: WidgetCall, WidgetArguments, WidgetStates?>, + InputRect: WidgetCall, WidgetArguments, WidgetStates?>, + InputColor3: WidgetCall, + InputColor4: WidgetCall, + + -- Drag Widget API + DragNum: WidgetCall, WidgetArguments, WidgetStates?>, + DragVector2: WidgetCall, WidgetArguments, WidgetStates?>, + DragVector3: WidgetCall, WidgetArguments, WidgetStates?>, + DragUDim: WidgetCall, WidgetArguments, WidgetStates?>, + DragUDim2: WidgetCall, WidgetArguments, WidgetStates?>, + DragRect: WidgetCall, WidgetArguments, WidgetStates?>, + + -- Slider Widget API + SliderNum: WidgetCall, WidgetArguments, WidgetStates?>, + SliderVector2: WidgetCall, WidgetArguments, WidgetStates?>, + SliderVector3: WidgetCall, WidgetArguments, WidgetStates?>, + SliderUDim: WidgetCall, WidgetArguments, WidgetStates?>, + SliderUDim2: WidgetCall, WidgetArguments, WidgetStates?>, + SliderRect: WidgetCall, WidgetArguments, WidgetStates?>, + + -- Combo Widget Widget API + Selectable: WidgetCall, + Combo: WidgetCall, + ComboArray: WidgetCall, + ComboEnum: WidgetCall, + InputEnum: WidgetCall, + + ProgressBar: WidgetCall, + PlotLines: WidgetCall, + PlotHistogram: WidgetCall, + + Image: WidgetCall, + ImageButton: WidgetCall, + + -- Table Widget Api + Table: WidgetCall, + NextColumn: () -> number, + NextRow: () -> number, + SetColumnIndex: (index: number) -> (), + SetRowIndex: (index: number) -> (), + NextHeaderColumn: () -> number, + SetHeaderColumnIndex: (index: number) -> (), + SetColumnWidth: (index: number, width: number) -> (), + + --[[ + --------- + STATE + --------- + ]] + + State: (initialValue: T) -> State, + WeakState: (initialValue: T) -> T, + VariableState: (variable: T, callback: (T) -> ()) -> State, + TableState: (tab: { [K]: V }, key: K, callback: ((newValue: V) -> true?)?) -> State, + ComputedState: (firstState: State, onChangeCallback: (firstValue: T) -> U) -> State, + + --[[ + ------------- + FUNCTIONS + ------------- + ]] + + Init: (playerInstance: BasePlayerGui?, eventConnection: (RBXScriptConnection | () -> () | false)?, allowMultipleInits: boolean?) -> Iris, + Shutdown: () -> (), + Connect: (self: Iris, callback: () -> ()) -> () -> (), + Append: (userInstance: GuiObject) -> (), + ForceRefresh: () -> (), + + -- Widget + SetFocusedWindow: (thisWidget: Window?) -> (), + + -- ID API + PushId: (ID: ID) -> (), + PopId: () -> (), + SetNextWidgetID: (ID: ID) -> (), + + -- Config API + UpdateGlobalConfig: (deltaStyle: { [string]: any }) -> (), + PushConfig: (deltaStyle: { [string]: any }) -> (), + PopConfig: () -> (), + + --[[ + -------------- + PROPERTIES + -------------- + ]] + + Internal: Internal, + Disabled: boolean, + Args: { [string]: { [string]: number } }, + Events: { [string]: () -> boolean }, + + TemplateConfig: { [string]: Config }, + _config: Config, + ShowDemoWindow: () -> Window, +} + +return {} diff --git a/src/DebuggerUI/Shared/External/iris/WidgetTypes.luau b/src/DebuggerUI/Shared/External/iris/WidgetTypes.luau new file mode 100644 index 0000000..ec786c6 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/WidgetTypes.luau @@ -0,0 +1,521 @@ +--[=[ + @within Iris + @type ID string +]=] +export type ID = string + +--[=[ + @within State + @type State { ID: ID, value: T, get: (self) -> T, set: (self, newValue: T) -> T, onChange: (self, callback: (newValue: T) -> ()) -> (), ConnectedWidgets: { [ID]: Widget }, ConnectedFunctions: { (newValue: T) -> () } } +]=] +export type State = { + ID: ID, + value: T, + lastChangeTick: number, + ConnectedWidgets: { [ID]: Widget }, + ConnectedFunctions: { (newValue: T) -> () }, + + get: (self: State) -> T, + set: (self: State, newValue: T, force: true?) -> (), + onChange: (self: State, funcToConnect: (newValue: T) -> ()) -> () -> (), + changed: (self: State) -> boolean, +} + +--[=[ + @within Iris + @type Widget { ID: ID, type: string, lastCycleTick: number, parentWidget: Widget, Instance: GuiObject, ZIndex: number, arguments: { [string]: any }} +]=] +export type Widget = { + ID: ID, + type: string, + lastCycleTick: number, + trackedEvents: {}, + parentWidget: ParentWidget, + + arguments: {}, + providedArguments: {}, + + Instance: GuiObject, + ZIndex: number, +} + +export type ParentWidget = Widget & { + ChildContainer: GuiObject, + ZOffset: number, + ZUpdate: boolean, +} + +export type StateWidget = Widget & { + state: { + [string]: State, + }, +} + +-- Events + +export type Hovered = { + isHoveredEvent: boolean, + hovered: () -> boolean, +} + +export type Clicked = { + lastClickedTick: number, + clicked: () -> boolean, +} + +export type RightClicked = { + lastRightClickedTick: number, + rightClicked: () -> boolean, +} + +export type DoubleClicked = { + lastClickedTime: number, + lastClickedPosition: Vector2, + lastDoubleClickedTick: number, + doubleClicked: () -> boolean, +} + +export type CtrlClicked = { + lastCtrlClickedTick: number, + ctrlClicked: () -> boolean, +} + +export type Active = { + active: () -> boolean, +} + +export type Checked = { + lastCheckedTick: number, + checked: () -> boolean, +} + +export type Unchecked = { + lastUncheckedTick: number, + unchecked: () -> boolean, +} + +export type Opened = { + lastOpenedTick: number, + opened: () -> boolean, +} + +export type Closed = { + lastClosedTick: number, + closed: () -> boolean, +} + +export type Collapsed = { + lastCollapsedTick: number, + collapsed: () -> boolean, +} + +export type Uncollapsed = { + lastUncollapsedTick: number, + uncollapsed: () -> boolean, +} + +export type Selected = { + lastSelectedTick: number, + selected: () -> boolean, +} + +export type Unselected = { + lastUnselectedTick: number, + unselected: () -> boolean, +} + +export type Changed = { + lastChangedTick: number, + changed: () -> boolean, +} + +export type NumberChanged = { + lastNumberChangedTick: number, + numberChanged: () -> boolean, +} + +export type TextChanged = { + lastTextChangedTick: number, + textChanged: () -> boolean, +} + +-- Widgets + +-- Window + +export type Root = ParentWidget + +export type Window = ParentWidget & { + usesScreenGuis: boolean, + + arguments: { + Title: string?, + NoTitleBar: boolean?, + NoBackground: boolean?, + NoCollapse: boolean?, + NoClose: boolean?, + NoMove: boolean?, + NoScrollbar: boolean?, + NoResize: boolean?, + NoNav: boolean?, + NoMenu: boolean?, + }, + + state: { + size: State, + position: State, + isUncollapsed: State, + isOpened: State, + scrollDistance: State, + }, +} & Opened & Closed & Collapsed & Uncollapsed & Hovered + +export type Tooltip = Widget & { + arguments: { + Text: string, + }, +} + +-- Menu + +export type MenuBar = ParentWidget + +export type Menu = ParentWidget & { + ButtonColors: { [string]: Color3 | number }, + + arguments: { + Text: string?, + }, + + state: { + isOpened: State, + }, +} & Clicked & Opened & Closed & Hovered + +export type MenuItem = Widget & { + arguments: { + Text: string, + KeyCode: Enum.KeyCode?, + ModifierKey: Enum.ModifierKey?, + }, +} & Clicked & Hovered + +export type MenuToggle = Widget & { + arguments: { + Text: string, + KeyCode: Enum.KeyCode?, + ModifierKey: Enum.ModifierKey?, + }, + + state: { + isChecked: State, + }, +} & Checked & Unchecked & Hovered + +-- Format + +export type Separator = Widget + +export type Indent = ParentWidget & { + arguments: { + Width: number?, + }, +} + +export type SameLine = ParentWidget & { + arguments: { + Width: number?, + VerticalAlignment: Enum.VerticalAlignment?, + HorizontalAlignment: Enum.HorizontalAlignment?, + }, +} + +export type Group = ParentWidget + +-- Text + +export type Text = Widget & { + arguments: { + Text: string, + Wrapped: boolean?, + Color: Color3?, + RichText: boolean?, + }, +} & Hovered + +export type SeparatorText = Widget & { + arguments: { + Text: string, + }, +} & Hovered + +-- Basic + +export type Button = Widget & { + arguments: { + Text: string?, + Size: UDim2?, + }, +} & Clicked & RightClicked & DoubleClicked & CtrlClicked & Hovered + +export type Checkbox = Widget & { + arguments: { + Text: string?, + }, + + state: { + isChecked: State, + }, +} & Unchecked & Checked & Hovered + +export type RadioButton = Widget & { + arguments: { + Text: string?, + Index: any, + }, + + state: { + index: State, + }, + + active: () -> boolean, +} & Selected & Unselected & Active & Hovered + +-- Image + +export type Image = Widget & { + arguments: { + Image: string, + Size: UDim2, + Rect: Rect?, + ScaleType: Enum.ScaleType?, + TileSize: UDim2?, + SliceCenter: Rect?, + SliceScale: number?, + ResampleMode: Enum.ResamplerMode?, + }, +} & Hovered + +-- ooops, may have overriden a Roblox type, and then got a weird type message +-- let's just hope I don't have to use a Roblox ImageButton type anywhere by name in this file +export type ImageButton = Image & Clicked & RightClicked & DoubleClicked & CtrlClicked + +-- Tree + +export type Tree = CollapsingHeader & { + arguments: { + Text: string, + SpanAvailWidth: boolean?, + NoIndent: boolean?, + DefaultOpen: true?, + }, +} + +export type CollapsingHeader = ParentWidget & { + arguments: { + Text: string?, + DefaultOpen: true?, + }, + + state: { + isUncollapsed: State, + }, +} & Collapsed & Uncollapsed & Hovered + +-- Tabs + +export type TabBar = ParentWidget & { + Tabs: { Tab }, + + state: { + index: State, + }, +} + +export type Tab = ParentWidget & { + parentWidget: TabBar, + Index: number, + ButtonColors: { [string]: Color3 | number }, + + arguments: { + Text: string, + Hideable: boolean, + }, + + state: { + index: State, + isOpened: State, + }, +} & Clicked & Opened & Selected & Unselected & Active & Closed & Hovered + +-- Input +export type Input = Widget & { + lastClickedTime: number, + lastClickedPosition: Vector2, + + arguments: { + Text: string?, + Increment: T, + Min: T, + Max: T, + Format: { string }, + Prefix: { string }, + NoButtons: boolean?, + }, + + state: { + number: State, + editingText: State, + }, +} & NumberChanged & Hovered + +export type InputColor3 = Input<{ number }> & { + arguments: { + UseFloats: boolean?, + UseHSV: boolean?, + }, + + state: { + color: State, + editingText: State, + }, +} & NumberChanged & Hovered + +export type InputColor4 = InputColor3 & { + state: { + transparency: State, + }, +} + +export type InputEnum = Input & { + state: { + enumItem: State, + }, +} + +export type InputText = Widget & { + arguments: { + Text: string?, + TextHint: string?, + ReadOnly: boolean?, + MultiLine: boolean?, + }, + + state: { + text: State, + }, +} & TextChanged & Hovered + +-- Combo + +export type Selectable = Widget & { + ButtonColors: { [string]: Color3 | number }, + + arguments: { + Text: string?, + Index: any?, + NoClick: boolean?, + }, + + state: { + index: State, + }, +} & Selected & Unselected & Clicked & RightClicked & DoubleClicked & CtrlClicked & Hovered + +export type Combo = ParentWidget & { + arguments: { + Text: string?, + NoButton: boolean?, + NoPreview: boolean?, + }, + + state: { + index: State, + isOpened: State, + }, + + UIListLayout: UIListLayout, +} & Opened & Closed & Changed & Clicked & Hovered + +-- Plot + +export type ProgressBar = Widget & { + arguments: { + Text: string?, + Format: string?, + }, + + state: { + progress: State, + }, +} & Changed & Hovered + +export type PlotLines = Widget & { + Lines: { Frame }, + HoveredLine: Frame | false, + Tooltip: TextLabel, + + arguments: { + Text: string, + Height: number, + Min: number, + Max: number, + TextOverlay: string, + }, + + state: { + values: State<{ number }>, + hovered: State<{ number }?>, + }, +} & Hovered + +export type PlotHistogram = Widget & { + Blocks: { Frame }, + HoveredBlock: Frame | false, + Tooltip: TextLabel, + + arguments: { + Text: string, + Height: number, + Min: number, + Max: number, + TextOverlay: string, + BaseLine: number, + }, + + state: { + values: State<{ number }>, + hovered: State, + }, +} & Hovered + +export type Table = ParentWidget & { + _columnIndex: number, + _rowIndex: number, + _rowContainer: Frame, + _rowInstances: { Frame }, + _cellInstances: { { Frame } }, + _rowBorders: { Frame }, + _columnBorders: { GuiButton }, + _rowCycles: { number }, + _widths: { UDim }, + _minWidths: { number }, + + arguments: { + NumColumns: number, + Header: boolean, + RowBackground: boolean, + OuterBorders: boolean, + InnerBorders: boolean, + Resizable: boolean, + FixedWidth: boolean, + ProportionalWidth: boolean, + LimitTableWidth: boolean, + }, + + state: { + widths: State<{ number }>, + }, +} & Hovered + +return {} diff --git a/src/DebuggerUI/Shared/External/iris/config.luau b/src/DebuggerUI/Shared/External/iris/config.luau new file mode 100644 index 0000000..6b44e47 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/config.luau @@ -0,0 +1,304 @@ +local 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), + BorderTransparency = 0.5, + BorderActiveColor = Color3.fromRGB(160, 160, 175), -- does not exist in Dear ImGui + BorderActiveTransparency = 0.3, + + WindowBgColor = Color3.fromRGB(15, 15, 15), + WindowBgTransparency = 0.06, + PopupBgColor = Color3.fromRGB(20, 20, 20), + PopupBgTransparency = 0.06, + + ScrollbarGrabColor = Color3.fromRGB(79, 79, 79), + 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, + TableHeaderColor = Color3.fromRGB(48, 48, 51), + TableHeaderTransparency = 0, + + 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, + + PlotLinesColor = Color3.fromRGB(156, 156, 156), + PlotLinesTransparency = 0, + PlotLinesHoveredColor = Color3.fromRGB(255, 110, 89), + PlotLinesHoveredTransparency = 0, + PlotHistogramColor = Color3.fromRGB(230, 179, 0), + PlotHistogramTransparency = 0, + PlotHistogramHoveredColor = Color3.fromRGB(255, 153, 0), + PlotHistogramHoveredTransparency = 0, + + ResizeGripColor = Color3.fromRGB(66, 150, 250), + ResizeGripTransparency = 0.8, + ResizeGripHoveredColor = Color3.fromRGB(66, 150, 250), + ResizeGripHoveredTransparency = 0.33, + ResizeGripActiveColor = Color3.fromRGB(66, 150, 250), + ResizeGripActiveTransparency = 0.05, + }, + colorLight = { -- Dear, ImGui default light + TextColor = Color3.fromRGB(0, 0, 0), + TextTransparency = 0, + TextDisabledColor = Color3.fromRGB(153, 153, 153), + TextDisabledTransparency = 0, + + -- Dear ImGui uses 0, 0, 0, 77 + -- The Roblox window selection highlight is 67, 191, 254 + BorderColor = Color3.fromRGB(64, 64, 64), + BorderActiveColor = Color3.fromRGB(64, 64, 64), -- does not exist in Dear ImGui + + -- BorderTransparency will be problematic for non UIStroke border implimentations + -- will not be implimented because of this + 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(176, 176, 176), + ScrollbarGrabTransparency = 0.2, + + 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, + TableHeaderColor = Color3.fromRGB(199, 222, 250), + TableHeaderTransparency = 0, + + 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, + + PlotLinesColor = Color3.fromRGB(99, 99, 99), + PlotLinesTransparency = 0, + PlotLinesHoveredColor = Color3.fromRGB(255, 110, 89), + PlotLinesHoveredTransparency = 0, + PlotHistogramColor = Color3.fromRGB(230, 179, 0), + PlotHistogramTransparency = 0, + PlotHistogramHoveredColor = Color3.fromRGB(255, 153, 0), + PlotHistogramHoveredTransparency = 0, + + ResizeGripColor = Color3.fromRGB(89, 89, 89), + ResizeGripTransparency = 0.83, + ResizeGripHoveredColor = Color3.fromRGB(66, 150, 250), + ResizeGripHoveredTransparency = 0.33, + ResizeGripActiveColor = Color3.fromRGB(66, 150, 250), + ResizeGripActiveTransparency = 0.05, + }, + + 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, + }, +} + +return TemplateConfig diff --git a/src/DebuggerUI/Shared/External/iris/demoWindow.luau b/src/DebuggerUI/Shared/External/iris/demoWindow.luau new file mode 100644 index 0000000..7e3b258 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/demoWindow.luau @@ -0,0 +1,1911 @@ +local Types = require(script.Parent.Types) + +return function(Iris: Types.Iris) + local showMainWindow = Iris.State(true) + local showRecursiveWindow = Iris.State(false) + local showRuntimeInfo = Iris.State(false) + local showStyleEditor = Iris.State(false) + local showWindowlessDemo = Iris.State(false) + local showMainMenuBarWindow = Iris.State(false) + local showDebugWindow = Iris.State(false) + + local function helpMarker(helpText: string) + Iris.PushConfig({ TextColor = Iris._config.TextDisabledColor }) + local text = Iris.Text({ "(?)" }) + Iris.PopConfig() + + Iris.PushConfig({ ContentWidth = UDim.new(0, 350) }) + if text.hovered() then + Iris.Tooltip({ helpText }) + end + Iris.PopConfig() + end + + local function textAndHelpMarker(text: string, helpText: string) + Iris.SameLine() + do + Iris.Text({ text }) + helpMarker(helpText) + end + Iris.End() + end + + -- shows each widgets functionality + local widgetDemos = { + Basic = function() + Iris.Tree({ "Basic" }) + do + Iris.SeparatorText({ "Basic" }) + + local radioButtonState: Types.State = Iris.State(1) + Iris.Button({ "Button" }) + Iris.SmallButton({ "SmallButton" }) + Iris.Text({ "Text" }) + Iris.TextWrapped({ string.rep("Text Wrapped ", 5) }) + Iris.TextColored({ "Colored Text", Color3.fromRGB(255, 128, 0) }) + Iris.Text({ `Rich Text: bold text italic text underline text strikethrough text red text bigger text`, true, nil, true }) + + Iris.SameLine() + do + Iris.RadioButton({ "Index '1'", 1 }, { index = radioButtonState }) + Iris.RadioButton({ "Index 'two'", "two" }, { index = radioButtonState }) + if Iris.RadioButton({ "Index 'false'", false }, { index = radioButtonState }).active() == false then + if Iris.SmallButton({ "Select last" }).clicked() then + radioButtonState:set(false) + end + end + end + Iris.End() + + Iris.Text({ "The Index is: " .. tostring(radioButtonState.value) }) + + Iris.SeparatorText({ "Inputs" }) + + Iris.InputNum({}) + Iris.DragNum({}) + Iris.SliderNum({}) + end + Iris.End() + end, + + Image = function() + Iris.Tree({ "Image" }) + do + Iris.SeparatorText({ "Image Controls" }) + + local AssetState = Iris.State("rbxasset://textures/ui/common/robux.png") + local SizeState = Iris.State(UDim2.fromOffset(100, 100)) + local RectState = Iris.State(Rect.new(0, 0, 0, 0)) + local ScaleTypeState = Iris.State(Enum.ScaleType.Stretch) + local PixelatedCheckState = Iris.State(false) + local PixelatedState = Iris.ComputedState(PixelatedCheckState, function(check: boolean) + return check and Enum.ResamplerMode.Pixelated or Enum.ResamplerMode.Default + end) + + local ImageColorState = Iris.State(Iris._config.ImageColor) + local ImageTransparencyState = Iris.State(Iris._config.ImageTransparency) + Iris.InputColor4({ "Image Tint" }, { color = ImageColorState, transparency = ImageTransparencyState }) + + Iris.Combo({ "Asset" }, { index = AssetState }) + do + Iris.Selectable({ "Robux Small", "rbxasset://textures/ui/common/robux.png" }, { index = AssetState }) + Iris.Selectable({ "Robux Large", "rbxasset://textures//ui/common/robux@3x.png" }, { index = AssetState }) + Iris.Selectable({ "Loading Texture", "rbxasset://textures//loading/darkLoadingTexture.png" }, { index = AssetState }) + Iris.Selectable({ "Hue-Saturation Gradient", "rbxasset://textures//TagEditor/huesatgradient.png" }, { index = AssetState }) + Iris.Selectable({ "famfamfam.png (WHY?)", "rbxasset://textures//TagEditor/famfamfam.png" }, { index = AssetState }) + end + Iris.End() + + Iris.SliderUDim2({ "Image Size", nil, nil, UDim2.new(1, 240, 1, 240) }, { number = SizeState }) + Iris.SliderRect({ "Image Rect", nil, nil, Rect.new(256, 256, 256, 256) }, { number = RectState }) + + Iris.Combo({ "Scale Type" }, { index = ScaleTypeState }) + do + Iris.Selectable({ "Stretch", Enum.ScaleType.Stretch }, { index = ScaleTypeState }) + Iris.Selectable({ "Fit", Enum.ScaleType.Fit }, { index = ScaleTypeState }) + Iris.Selectable({ "Crop", Enum.ScaleType.Crop }, { index = ScaleTypeState }) + end + + Iris.End() + Iris.Checkbox({ "Pixelated" }, { isChecked = PixelatedCheckState }) + + Iris.PushConfig({ + ImageColor = ImageColorState:get(), + ImageTransparency = ImageTransparencyState:get(), + }) + Iris.Image({ AssetState:get(), SizeState:get(), RectState:get(), ScaleTypeState:get(), PixelatedState:get() }) + Iris.PopConfig() + + Iris.SeparatorText({ "Tile" }) + local TileState = Iris.State(UDim2.fromScale(0.5, 0.5)) + Iris.SliderUDim2({ "Tile Size", nil, nil, UDim2.new(1, 240, 1, 240) }, { number = TileState }) + + Iris.PushConfig({ + ImageColor = ImageColorState:get(), + ImageTransparency = ImageTransparencyState:get(), + }) + Iris.Image({ "rbxasset://textures/grid2.png", SizeState:get(), nil, Enum.ScaleType.Tile, PixelatedState:get(), TileState:get() }) + Iris.PopConfig() + + Iris.SeparatorText({ "Slice" }) + local SliceScaleState = Iris.State(1) + Iris.SliderNum({ "Image Slice Scale", 0.1, 0.1, 5 }, { number = SliceScaleState }) + + Iris.PushConfig({ + ImageColor = ImageColorState:get(), + ImageTransparency = ImageTransparencyState:get(), + }) + Iris.Image({ "rbxasset://textures/ui/chatBubble_blue_notify_bkg.png", SizeState:get(), nil, Enum.ScaleType.Slice, PixelatedState:get(), nil, Rect.new(12, 12, 56, 56), 1 }, SliceScaleState:get()) + Iris.PopConfig() + + Iris.SeparatorText({ "Image Button" }) + local count = Iris.State(0) + + Iris.SameLine() + do + Iris.PushConfig({ + ImageColor = ImageColorState:get(), + ImageTransparency = ImageTransparencyState:get(), + }) + if Iris.ImageButton({ "rbxasset://textures/AvatarCompatibilityPreviewer/add.png", UDim2.fromOffset(20, 20) }).clicked() then + count:set(count.value + 1) + end + Iris.PopConfig() + + Iris.Text({ `Click count: {count.value}` }) + end + Iris.End() + end + Iris.End() + end, + + Selectable = function() + Iris.Tree({ "Selectable" }) + do + local sharedIndex = Iris.State(2) + Iris.Selectable({ "Selectable #1", 1 }, { index = sharedIndex }) + Iris.Selectable({ "Selectable #2", 2 }, { index = sharedIndex }) + if Iris.Selectable({ "Double click Selectable", 3, true }, { index = sharedIndex }).doubleClicked() then + sharedIndex:set(3) + end + + Iris.Selectable({ "Impossible to select", 4, true }, { index = sharedIndex }) + if Iris.Button({ "Select last" }).clicked() then + sharedIndex:set(4) + end + + Iris.Selectable({ "Independent Selectable" }) + end + Iris.End() + end, + + Combo = function() + Iris.Tree({ "Combo" }) + do + Iris.PushConfig({ ContentWidth = UDim.new(1, -200) }) + local sharedComboIndex = Iris.State("No Selection") + + local NoPreview, NoButton + Iris.SameLine() + do + NoPreview = Iris.Checkbox({ "No Preview" }) + NoButton = Iris.Checkbox({ "No Button" }) + if NoPreview.checked() and NoButton.isChecked.value == true then + NoButton.isChecked:set(false) + end + if NoButton.checked() and NoPreview.isChecked.value == true then + NoPreview.isChecked:set(false) + end + end + Iris.End() + + Iris.Combo({ "Basic Usage", NoButton.isChecked:get(), NoPreview.isChecked:get() }, { index = sharedComboIndex }) + do + Iris.Selectable({ "Select 1", "One" }, { index = sharedComboIndex }) + Iris.Selectable({ "Select 2", "Two" }, { index = sharedComboIndex }) + Iris.Selectable({ "Select 3", "Three" }, { index = sharedComboIndex }) + end + Iris.End() + + Iris.ComboArray({ "Using ComboArray" }, { index = "No Selection" }, { "Red", "Green", "Blue" }) + + local heightTestArray = {} + for i = 1, 50 do + table.insert(heightTestArray, tostring(i)) + end + Iris.ComboArray({ "Height Test" }, { index = "1" }, heightTestArray) + + local sharedComboIndex2 = Iris.State("7 AM") + + Iris.Combo({ "Combo with Inner widgets" }, { index = sharedComboIndex2 }) + do + Iris.Tree({ "Morning Shifts" }) + do + Iris.Selectable({ "Shift at 7 AM", "7 AM" }, { index = sharedComboIndex2 }) + Iris.Selectable({ "Shift at 11 AM", "11 AM" }, { index = sharedComboIndex2 }) + Iris.Selectable({ "Shift at 3 PM", "3 PM" }, { index = sharedComboIndex2 }) + end + Iris.End() + Iris.Tree({ "Night Shifts" }) + do + Iris.Selectable({ "Shift at 6 PM", "6 PM" }, { index = sharedComboIndex2 }) + Iris.Selectable({ "Shift at 9 PM", "9 PM" }, { index = sharedComboIndex2 }) + end + Iris.End() + end + Iris.End() + + local ComboEnum = Iris.ComboEnum({ "Using ComboEnum" }, { index = Enum.UserInputState.Begin }, Enum.UserInputState) + Iris.Text({ "Selected: " .. ComboEnum.index:get().Name }) + Iris.PopConfig() + end + Iris.End() + end, + + Tree = function() + Iris.Tree({ "Trees" }) + do + Iris.Tree({ "Tree using SpanAvailWidth", true }) + do + helpMarker("SpanAvailWidth determines if the Tree is selectable from its entire with, or only the text area") + end + Iris.End() + + local tree1 = Iris.Tree({ "Tree with Children" }) + do + Iris.Text({ "Im inside the first tree!" }) + Iris.Button({ "Im a button inside the first tree!" }) + Iris.Tree({ "Im a tree inside the first tree!" }) + do + Iris.Text({ "I am the innermost text!" }) + end + Iris.End() + end + Iris.End() + + Iris.Checkbox({ "Toggle above tree" }, { isChecked = tree1.state.isUncollapsed }) + end + Iris.End() + end, + + CollapsingHeader = function() + Iris.Tree({ "Collapsing Headers" }) + do + Iris.CollapsingHeader({ "A header" }) + do + Iris.Text({ "This is under the first header!" }) + end + Iris.End() + + local secondHeader = Iris.State(false) + Iris.CollapsingHeader({ "Another header" }, { isUncollapsed = secondHeader }) + do + if Iris.Button({ "Shhh... secret button!" }).clicked() then + secondHeader:set(true) + end + end + Iris.End() + end + Iris.End() + end, + + Group = function() + Iris.Tree({ "Groups" }) + do + Iris.SameLine() + do + Iris.Group() + do + Iris.Text({ "I am in group A" }) + Iris.Button({ "Im also in A" }) + end + Iris.End() + + Iris.Separator() + + Iris.Group() + do + Iris.Text({ "I am in group B" }) + Iris.Button({ "Im also in B" }) + Iris.Button({ "Also group B" }) + end + Iris.End() + end + Iris.End() + end + Iris.End() + end, + + Tab = function() + Iris.Tree({ "Tabs" }) + do + Iris.Tree({ "Simple" }) + do + Iris.TabBar() + do + Iris.Tab({ "Apples" }) + do + Iris.Text({ "Who loves apples?" }) + end + Iris.End() + Iris.Tab({ "Broccoli" }) + do + Iris.Text({ "And what about broccoli?" }) + end + Iris.End() + Iris.Tab({ "Carrots" }) + do + Iris.Text({ "But carrots are the best." }) + end + Iris.End() + end + Iris.End() + Iris.Separator() + Iris.Text({ "Very important questions." }) + end + Iris.End() + + Iris.Tree({ "Closable" }) + do + local a = Iris.State(true) + local b = Iris.State(true) + local c = Iris.State(true) + + Iris.TabBar() + do + Iris.Tab({ "🍎", true }, { isOpened = a }) + do + Iris.Text({ "Who loves apples?" }) + if Iris.Button({ "I don't like apples." }).clicked() then + a:set(false) + end + end + Iris.End() + Iris.Tab({ "🥦", true }, { isOpened = b }) + do + Iris.Text({ "And what about broccoli?" }) + if Iris.Button({ "Not for me." }).clicked() then + b:set(false) + end + end + Iris.End() + Iris.Tab({ "🥕", true }, { isOpened = c }) + do + Iris.Text({ "But carrots are the best." }) + if Iris.Button({ "I disagree with you." }).clicked() then + c:set(false) + end + end + Iris.End() + end + Iris.End() + Iris.Separator() + if Iris.Button({ "Actually, let me reconsider it." }).clicked() then + a:set(true) + b:set(true) + c:set(true) + end + end + Iris.End() + end + Iris.End() + end, + + Indent = function() + Iris.Tree({ "Indents" }) + Iris.Text({ "Not Indented" }) + Iris.Indent() + do + Iris.Text({ "Indented" }) + Iris.Indent({ 7 }) + do + Iris.Text({ "Indented by 7 more pixels" }) + Iris.End() + + Iris.Indent({ -7 }) + do + Iris.Text({ "Indented by 7 less pixels" }) + end + Iris.End() + end + Iris.End() + end + Iris.End() + end, + + Input = function() + Iris.Tree({ "Input" }) + do + local NoField, NoButtons, Min, Max, Increment, Format = Iris.State(false), Iris.State(false), Iris.State(0), Iris.State(100), Iris.State(1), Iris.State("%d") + + Iris.PushConfig({ ContentWidth = UDim.new(1, -120) }) + local InputNum = Iris.InputNum({ + [Iris.Args.InputNum.Text] = "Input Number", + -- [Iris.Args.InputNum.NoField] = NoField.value, + [Iris.Args.InputNum.NoButtons] = NoButtons.value, + [Iris.Args.InputNum.Min] = Min.value, + [Iris.Args.InputNum.Max] = Max.value, + [Iris.Args.InputNum.Increment] = Increment.value, + [Iris.Args.InputNum.Format] = { Format.value }, + }) + Iris.PopConfig() + Iris.Text({ "The Value is: " .. InputNum.number.value }) + if Iris.Button({ "Randomize Number" }).clicked() then + InputNum.number:set(math.random(1, 99)) + end + local NoFieldCheckbox = Iris.Checkbox({ "NoField" }, { isChecked = NoField }) + local NoButtonsCheckbox = Iris.Checkbox({ "NoButtons" }, { isChecked = NoButtons }) + if NoFieldCheckbox.checked() and NoButtonsCheckbox.isChecked.value == true then + NoButtonsCheckbox.isChecked:set(false) + end + if NoButtonsCheckbox.checked() and NoFieldCheckbox.isChecked.value == true then + NoFieldCheckbox.isChecked:set(false) + end + + Iris.PushConfig({ ContentWidth = UDim.new(1, -120) }) + Iris.InputVector2({ "InputVector2" }) + Iris.InputVector3({ "InputVector3" }) + Iris.InputUDim({ "InputUDim" }) + Iris.InputUDim2({ "InputUDim2" }) + local UseFloats = Iris.State(false) + local UseHSV = Iris.State(false) + local sharedColor = Iris.State(Color3.new()) + local transparency = Iris.State(0) + Iris.SliderNum({ "Transparency", 0.01, 0, 1 }, { number = transparency }) + Iris.InputColor3({ "InputColor3", UseFloats:get(), UseHSV:get() }, { color = sharedColor }) + Iris.InputColor4({ "InputColor4", UseFloats:get(), UseHSV:get() }, { color = sharedColor, transparency = transparency }) + Iris.SameLine() + Iris.Text({ sharedColor:get():ToHex() }) + Iris.Checkbox({ "Use Floats" }, { isChecked = UseFloats }) + Iris.Checkbox({ "Use HSV" }, { isChecked = UseHSV }) + Iris.End() + + Iris.PopConfig() + + Iris.Separator() + + Iris.SameLine() + do + Iris.Text({ "Slider Numbers" }) + helpMarker("ctrl + click slider number widgets to input a number") + end + Iris.End() + Iris.PushConfig({ ContentWidth = UDim.new(1, -120) }) + Iris.SliderNum({ "Slide Int", 1, 1, 8 }) + Iris.SliderNum({ "Slide Float", 0.01, 0, 100 }) + Iris.SliderNum({ "Small Numbers", 0.001, -2, 1, "%f radians" }) + Iris.SliderNum({ "Odd Ranges", 0.001, -math.pi, math.pi, "%f radians" }) + Iris.SliderNum({ "Big Numbers", 1e4, 1e5, 1e7 }) + Iris.SliderNum({ "Few Numbers", 1, 0, 3 }) + Iris.PopConfig() + + Iris.Separator() + + Iris.SameLine() + do + Iris.Text({ "Drag Numbers" }) + helpMarker("ctrl + click or double click drag number widgets to input a number, hold shift/alt while dragging to increase/decrease speed") + end + Iris.End() + Iris.PushConfig({ ContentWidth = UDim.new(1, -120) }) + Iris.DragNum({ "Drag Int" }) + Iris.DragNum({ "Slide Float", 0.001, -10, 10 }) + Iris.DragNum({ "Percentage", 1, 0, 100, "%d %%" }) + Iris.PopConfig() + end + Iris.End() + end, + + InputText = function() + Iris.Tree({ "Input Text" }) + do + local InputText = Iris.InputText({ "Input Text Test", "Input Text here" }) + Iris.Text({ "The text is: " .. InputText.text.value }) + end + Iris.End() + end, + + MultiInput = function() + Iris.Tree({ "Multi-Component Input" }) + do + local sharedVector2 = Iris.State(Vector2.new()) + local sharedVector3 = Iris.State(Vector3.new()) + local sharedUDim = Iris.State(UDim.new()) + local sharedUDim2 = Iris.State(UDim2.new()) + local sharedColor3 = Iris.State(Color3.new()) + local SharedRect = Iris.State(Rect.new(0, 0, 0, 0)) + + Iris.SeparatorText({ "Input" }) + + Iris.InputVector2({}, { number = sharedVector2 }) + Iris.InputVector3({}, { number = sharedVector3 }) + Iris.InputUDim({}, { number = sharedUDim }) + Iris.InputUDim2({}, { number = sharedUDim2 }) + Iris.InputRect({}, { number = SharedRect }) + + Iris.SeparatorText({ "Drag" }) + + Iris.DragVector2({}, { number = sharedVector2 }) + Iris.DragVector3({}, { number = sharedVector3 }) + Iris.DragUDim({}, { number = sharedUDim }) + Iris.DragUDim2({}, { number = sharedUDim2 }) + Iris.DragRect({}, { number = SharedRect }) + + Iris.SeparatorText({ "Slider" }) + + Iris.SliderVector2({}, { number = sharedVector2 }) + Iris.SliderVector3({}, { number = sharedVector3 }) + Iris.SliderUDim({}, { number = sharedUDim }) + Iris.SliderUDim2({}, { number = sharedUDim2 }) + Iris.SliderRect({}, { number = SharedRect }) + + Iris.SeparatorText({ "Color" }) + + Iris.InputColor3({}, { color = sharedColor3 }) + Iris.InputColor4({}, { color = sharedColor3 }) + end + Iris.End() + end, + + Tooltip = function() + Iris.PushConfig({ ContentWidth = UDim.new(0, 250) }) + Iris.Tree({ "Tooltip" }) + do + if Iris.Text({ "Hover over me to reveal a tooltip" }).hovered() then + Iris.Tooltip({ "I am some helpful tooltip text" }) + end + local dynamicText = Iris.State("Hello ") + local numRepeat = Iris.State(1) + if Iris.InputNum({ "# of repeat", 1, 1, 50 }, { number = numRepeat }).numberChanged() then + dynamicText:set(string.rep("Hello ", numRepeat:get())) + end + if Iris.Checkbox({ "Show dynamic text tooltip" }).state.isChecked.value then + Iris.Tooltip({ dynamicText:get() }) + end + end + Iris.End() + Iris.PopConfig() + end, + + Plotting = function() + Iris.Tree({ "Plotting" }) + do + Iris.SeparatorText({ "Progress" }) + local curTime = os.clock() * 15 + + local Progress = Iris.State(0) + -- formula to cycle between 0 and 100 linearly + local newValue = math.clamp((math.abs(curTime % 100 - 50)) - 7.5, 0, 35) / 35 + Progress:set(newValue) + + Iris.ProgressBar({ "Progress Bar" }, { progress = Progress }) + Iris.ProgressBar({ "Progress Bar", `{math.floor(Progress:get() * 1753)}/1753` }, { progress = Progress }) + + Iris.SeparatorText({ "Graphs" }) + + do + local ValueState = Iris.State({ 0.5, 0.8, 0.2, 0.9, 0.1, 0.6, 0.4, 0.7, 0.3, 0.0 }) + + Iris.PlotHistogram({ "Histogram", 100, 0, 1, "random" }, { values = ValueState }) + Iris.PlotLines({ "Lines", 100, 0, 1, "random" }, { values = ValueState }) + end + + do + local FunctionState = Iris.State("Cos") + local SampleState = Iris.State(37) + local BaseLineState = Iris.State(0) + local ValueState = Iris.State({}) + local TimeState = Iris.State(0) + + local Animated = Iris.Checkbox({ "Animate" }) + local plotFunc = Iris.ComboArray({ "Plotting Function" }, { index = FunctionState }, { "Sin", "Cos", "Tan", "Saw" }) + local samples = Iris.SliderNum({ "Samples", 1, 1, 145, "%d samples" }, { number = SampleState }) + if Iris.SliderNum({ "Baseline", 0.1, -1, 1 }, { number = BaseLineState }).numberChanged() then + ValueState:set(ValueState.value, true) + end + + if Animated.state.isChecked.value or plotFunc.closed() or samples.numberChanged() or #ValueState.value == 0 then + if Animated.state.isChecked.value then + TimeState:set(TimeState.value + Iris.Internal._deltaTime) + end + local offset: number = math.floor(TimeState.value * 30) - 1 + local func: string = FunctionState.value + table.clear(ValueState.value) + for i = 1, SampleState.value do + if func == "Sin" then + ValueState.value[i] = math.sin(math.rad(5 * (i + offset))) + elseif func == "Cos" then + ValueState.value[i] = math.cos(math.rad(5 * (i + offset))) + elseif func == "Tan" then + ValueState.value[i] = math.tan(math.rad(5 * (i + offset))) + elseif func == "Saw" then + ValueState.value[i] = if (i % 2) == (offset % 2) then 1 else -1 + end + end + + ValueState:set(ValueState.value, true) + end + + Iris.PlotHistogram({ "Histogram", 100, -1, 1, "", BaseLineState:get() }, { values = ValueState }) + Iris.PlotLines({ "Lines", 100, -1, 1 }, { values = ValueState }) + end + end + Iris.End() + end, + } + local widgetDemosOrder = { "Basic", "Image", "Selectable", "Combo", "Tree", "CollapsingHeader", "Group", "Tab", "Indent", "Input", "MultiInput", "InputText", "Tooltip", "Plotting" } + + local function recursiveTree() + local theTree = Iris.Tree({ "Recursive Tree" }) + do + if theTree.state.isUncollapsed.value then + recursiveTree() + end + end + Iris.End() + end + + local function recursiveWindow(parentCheckboxState) + local theCheckbox + Iris.Window({ "Recursive Window" }, { size = Iris.State(Vector2.new(175, 100)), isOpened = parentCheckboxState }) + do + theCheckbox = Iris.Checkbox({ "Recurse Again" }) + end + Iris.End() + + if theCheckbox.isChecked.value then + recursiveWindow(theCheckbox.isChecked) + end + end + + -- shows list of runtime widgets and states, including IDs. shows other info about runtime and can show widgets/state info in depth. + local function runtimeInfo() + local runtimeInfoWindow = Iris.Window({ "Runtime Info" }, { isOpened = showRuntimeInfo }) + do + local lastVDOM = Iris.Internal._lastVDOM + local states = Iris.Internal._states + + local numSecondsDisabled = Iris.State(3) + local rollingDT = Iris.State(0) + local lastT = Iris.State(os.clock()) + + Iris.SameLine() + do + Iris.InputNum({ [Iris.Args.InputNum.Text] = "", [Iris.Args.InputNum.Format] = "%d Seconds", [Iris.Args.InputNum.Max] = 10 }, { number = numSecondsDisabled }) + if Iris.Button({ "Disable" }).clicked() then + Iris.Disabled = true + task.delay(numSecondsDisabled:get(), function() + Iris.Disabled = false + end) + end + end + Iris.End() + + local t = os.clock() + local dt = t - lastT.value + rollingDT.value += (dt - rollingDT.value) * 0.2 + lastT.value = t + Iris.Text({ string.format("Average %.3f ms/frame (%.1f FPS)", rollingDT.value * 1000, 1 / rollingDT.value) }) + + Iris.Text({ + string.format("Window Position: (%d, %d), Window Size: (%d, %d)", runtimeInfoWindow.position.value.X, runtimeInfoWindow.position.value.Y, runtimeInfoWindow.size.value.X, runtimeInfoWindow.size.value.Y), + }) + + Iris.SameLine() + do + Iris.Text({ "Enter an ID to learn more about it." }) + helpMarker("every widget and state has an ID which Iris tracks to remember which widget is which. below lists all widgets and states, with their respective IDs") + end + Iris.End() + + Iris.PushConfig({ ItemWidth = UDim.new(1, -150) }) + local enteredText = Iris.InputText({ "ID field" }, { text = Iris.State(runtimeInfoWindow.ID) }).state.text.value + Iris.PopConfig() + + Iris.Indent() + do + local enteredWidget = lastVDOM[enteredText] + local enteredState = states[enteredText] + if enteredWidget then + Iris.Table({ 1 }) + Iris.Text({ string.format('The ID, "%s", is a widget', enteredText) }) + Iris.NextRow() + + Iris.Text({ string.format("Widget is type: %s", enteredWidget.type) }) + Iris.NextRow() + + Iris.Tree({ "Widget has Args:" }, { isUncollapsed = Iris.State(true) }) + for i, v in enteredWidget.arguments do + Iris.Text({ i .. " - " .. tostring(v) }) + end + Iris.End() + Iris.NextRow() + + if enteredWidget.state then + Iris.Tree({ "Widget has State:" }, { isUncollapsed = Iris.State(true) }) + for i, v in enteredWidget.state do + Iris.Text({ i .. " - " .. tostring(v.value) }) + end + Iris.End() + end + Iris.End() + elseif enteredState then + Iris.Table({ 1 }) + Iris.Text({ string.format('The ID, "%s", is a state', enteredText) }) + Iris.NextRow() + + Iris.Text({ string.format("Value is type: %s, Value = %s", typeof(enteredState.value), tostring(enteredState.value)) }) + Iris.NextRow() + + Iris.Tree({ "state has connected widgets:" }, { isUncollapsed = Iris.State(true) }) + for i, v in enteredState.ConnectedWidgets do + Iris.Text({ i .. " - " .. v.type }) + end + Iris.End() + Iris.NextRow() + + Iris.Text({ string.format("state has: %d connected functions", #enteredState.ConnectedFunctions) }) + Iris.End() + else + Iris.Text({ string.format('The ID, "%s", is not a state or widget', enteredText) }) + end + end + Iris.End() + + if Iris.Tree({ "Widgets" }).state.isUncollapsed.value then + local widgetCount = 0 + local widgetStr = "" + for _, v in lastVDOM do + widgetCount += 1 + widgetStr ..= "\n" .. v.ID .. " - " .. v.type + end + + Iris.Text({ "Number of Widgets: " .. widgetCount }) + + Iris.Text({ widgetStr }) + end + Iris.End() + + if Iris.Tree({ "States" }).state.isUncollapsed.value then + local stateCount = 0 + local stateStr = "" + for i, v in states do + stateCount += 1 + stateStr ..= "\n" .. i .. " - " .. tostring(v.value) + end + + Iris.Text({ "Number of States: " .. stateCount }) + + Iris.Text({ stateStr }) + end + Iris.End() + end + Iris.End() + end + + local function debugPanel() + Iris.Window({ "Debug Panel" }, { isOpened = showDebugWindow }) + do + Iris.CollapsingHeader({ "Widgets" }) + do + Iris.SeparatorText({ "GuiService" }) + Iris.Text({ `GuiOffset: {Iris.Internal._utility.GuiOffset}` }) + Iris.Text({ `MouseOffset: {Iris.Internal._utility.MouseOffset}` }) + + Iris.SeparatorText({ "UserInputService" }) + Iris.Text({ `MousePosition: {Iris.Internal._utility.UserInputService:GetMouseLocation()}` }) + Iris.Text({ `MouseLocation: {Iris.Internal._utility.getMouseLocation()}` }) + + Iris.Text({ `Left Control: {Iris.Internal._utility.UserInputService:IsKeyDown(Enum.KeyCode.LeftControl)}` }) + Iris.Text({ `Right Control: {Iris.Internal._utility.UserInputService:IsKeyDown(Enum.KeyCode.RightControl)}` }) + end + Iris.End() + end + Iris.End() + end + + local function recursiveMenu() + if Iris.Menu({ "Recursive" }).state.isOpened.value then + Iris.MenuItem({ "New", Enum.KeyCode.N, Enum.ModifierKey.Ctrl }) + Iris.MenuItem({ "Open", Enum.KeyCode.O, Enum.ModifierKey.Ctrl }) + Iris.MenuItem({ "Save", Enum.KeyCode.S, Enum.ModifierKey.Ctrl }) + Iris.Separator() + Iris.MenuToggle({ "Autosave" }) + Iris.MenuToggle({ "Checked" }) + Iris.Separator() + Iris.Menu({ "Options" }) + Iris.MenuItem({ "Red" }) + Iris.MenuItem({ "Yellow" }) + Iris.MenuItem({ "Green" }) + Iris.MenuItem({ "Blue" }) + Iris.Separator() + recursiveMenu() + Iris.End() + end + Iris.End() + end + + local function mainMenuBar() + Iris.MenuBar() + do + Iris.Menu({ "File" }) + do + Iris.MenuItem({ "New", Enum.KeyCode.N, Enum.ModifierKey.Ctrl }) + Iris.MenuItem({ "Open", Enum.KeyCode.O, Enum.ModifierKey.Ctrl }) + Iris.MenuItem({ "Save", Enum.KeyCode.S, Enum.ModifierKey.Ctrl }) + recursiveMenu() + if Iris.MenuItem({ "Quit", Enum.KeyCode.Q, Enum.ModifierKey.Alt }).clicked() then + showMainWindow:set(false) + end + end + Iris.End() + + Iris.Menu({ "Examples" }) + do + Iris.MenuToggle({ "Recursive Window" }, { isChecked = showRecursiveWindow }) + Iris.MenuToggle({ "Windowless" }, { isChecked = showWindowlessDemo }) + Iris.MenuToggle({ "Main Menu Bar" }, { isChecked = showMainMenuBarWindow }) + end + Iris.End() + + Iris.Menu({ "Tools" }) + do + Iris.MenuToggle({ "Runtime Info" }, { isChecked = showRuntimeInfo }) + Iris.MenuToggle({ "Style Editor" }, { isChecked = showStyleEditor }) + Iris.MenuToggle({ "Debug Panel" }, { isChecked = showDebugWindow }) + end + Iris.End() + end + Iris.End() + end + + local function mainMenuBarExample() + -- local screenSize = Iris.Internal._rootWidget.Instance.PseudoWindowScreenGui.AbsoluteSize + -- Iris.Window( + -- {[Iris.Args.Window.NoBackground] = true, [Iris.Args.Window.NoTitleBar] = true, [Iris.Args.Window.NoMove] = true, [Iris.Args.Window.NoResize] = true}, + -- {size = Iris.State(screenSize), position = Iris.State(Vector2.new(0, 0))} + -- ) + + mainMenuBar() + + --Iris.End() + end + + -- allows users to edit state + local styleEditor + do + styleEditor = function() + local styleList = { + { + "Sizing", + function() + local UpdatedConfig = Iris.State({}) + + Iris.SameLine() + do + if Iris.Button({ "Update" }).clicked() then + Iris.UpdateGlobalConfig(UpdatedConfig.value) + UpdatedConfig:set({}) + end + + helpMarker("Update the global config with these changes.") + end + Iris.End() + + local function SliderInput(input: string, arguments: { any }) + local Input = Iris[input](arguments, { number = Iris.WeakState(Iris._config[arguments[1]]) }) + if Input.numberChanged() then + UpdatedConfig.value[arguments[1]] = Input.number:get() + end + end + + local function BooleanInput(arguments: { any }) + local Input = Iris.Checkbox(arguments, { isChecked = Iris.WeakState(Iris._config[arguments[1]]) }) + if Input.checked() or Input.unchecked() then + UpdatedConfig.value[arguments[1]] = Input.isChecked:get() + end + end + + Iris.SeparatorText({ "Main" }) + SliderInput("SliderVector2", { "WindowPadding", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "WindowResizePadding", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "FramePadding", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "ItemSpacing", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "ItemInnerSpacing", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "CellPadding", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderNum", { "IndentSpacing", 1, 0, 36 }) + SliderInput("SliderNum", { "ScrollbarSize", 1, 0, 20 }) + SliderInput("SliderNum", { "GrabMinSize", 1, 0, 20 }) + + Iris.SeparatorText({ "Borders & Rounding" }) + SliderInput("SliderNum", { "FrameBorderSize", 0.1, 0, 1 }) + SliderInput("SliderNum", { "WindowBorderSize", 0.1, 0, 1 }) + SliderInput("SliderNum", { "PopupBorderSize", 0.1, 0, 1 }) + SliderInput("SliderNum", { "SeparatorTextBorderSize", 1, 0, 20 }) + SliderInput("SliderNum", { "FrameRounding", 1, 0, 12 }) + SliderInput("SliderNum", { "GrabRounding", 1, 0, 12 }) + SliderInput("SliderNum", { "PopupRounding", 1, 0, 12 }) + + Iris.SeparatorText({ "Widgets" }) + SliderInput("SliderVector2", { "DisplaySafeAreaPadding", nil, Vector2.zero, Vector2.new(20, 20) }) + SliderInput("SliderVector2", { "SeparatorTextPadding", nil, Vector2.zero, Vector2.new(36, 36) }) + SliderInput("SliderUDim", { "ItemWidth", nil, UDim.new(), UDim.new(1, 200) }) + SliderInput("SliderUDim", { "ContentWidth", nil, UDim.new(), UDim.new(1, 200) }) + SliderInput("SliderNum", { "ImageBorderSize", 1, 0, 12 }) + local TitleInput = Iris.ComboEnum({ "WindowTitleAlign" }, { index = Iris.WeakState(Iris._config.WindowTitleAlign) }, Enum.LeftRight) + if TitleInput.closed() then + UpdatedConfig.value["WindowTitleAlign"] = TitleInput.index:get() + end + BooleanInput({ "RichText" }) + BooleanInput({ "TextWrapped" }) + + Iris.SeparatorText({ "Config" }) + BooleanInput({ "UseScreenGUIs" }) + SliderInput("DragNum", { "DisplayOrderOffset", 1, 0 }) + SliderInput("DragNum", { "ZIndexOffset", 1, 0 }) + SliderInput("SliderNum", { "MouseDoubleClickTime", 0.1, 0, 5 }) + SliderInput("SliderNum", { "MouseDoubleClickMaxDist", 0.1, 0, 20 }) + end, + }, + { + "Colors", + function() + local UpdatedConfig = Iris.State({}) + + Iris.SameLine() + do + if Iris.Button({ "Update" }).clicked() then + Iris.UpdateGlobalConfig(UpdatedConfig.value) + UpdatedConfig:set({}) + end + helpMarker("Update the global config with these changes.") + end + Iris.End() + + local color4s = { + "Text", + "TextDisabled", + "WindowBg", + "PopupBg", + "Border", + "BorderActive", + "ScrollbarGrab", + "TitleBg", + "TitleBgActive", + "TitleBgCollapsed", + "MenubarBg", + "FrameBg", + "FrameBgHovered", + "FrameBgActive", + "Button", + "ButtonHovered", + "ButtonActive", + "Image", + "SliderGrab", + "SliderGrabActive", + "Header", + "HeaderHovered", + "HeaderActive", + "SelectionImageObject", + "SelectionImageObjectBorder", + "TableBorderStrong", + "TableBorderLight", + "TableRowBg", + "TableRowBgAlt", + "NavWindowingHighlight", + "NavWindowingDimBg", + "Separator", + "CheckMark", + } + + for _, vColor in color4s do + local Input = Iris.InputColor4({ vColor }, { + color = Iris.WeakState(Iris._config[vColor .. "Color"]), + transparency = Iris.WeakState(Iris._config[vColor .. "Transparency"]), + }) + if Input.numberChanged() then + UpdatedConfig.value[vColor .. "Color"] = Input.color:get() + UpdatedConfig.value[vColor .. "Transparency"] = Input.transparency:get() + end + end + end, + }, + { + "Fonts", + function() + local UpdatedConfig = Iris.State({}) + + Iris.SameLine() + do + if Iris.Button({ "Update" }).clicked() then + Iris.UpdateGlobalConfig(UpdatedConfig.value) + UpdatedConfig:set({}) + end + + helpMarker("Update the global config with these changes.") + end + Iris.End() + + local fonts: { [string]: Font } = { + ["Code (default)"] = Font.fromEnum(Enum.Font.Code), + ["Ubuntu (template)"] = Font.fromEnum(Enum.Font.Ubuntu), + ["Arial"] = Font.fromEnum(Enum.Font.Arial), + ["Highway"] = Font.fromEnum(Enum.Font.Highway), + ["Roboto"] = Font.fromEnum(Enum.Font.Roboto), + ["Roboto Mono"] = Font.fromEnum(Enum.Font.RobotoMono), + ["Noto Sans"] = Font.new("rbxassetid://12187370747"), + ["Builder Sans"] = Font.fromEnum(Enum.Font.BuilderSans), + ["Builder Mono"] = Font.new("rbxassetid://16658246179"), + ["Sono"] = Font.new("rbxassetid://12187374537"), + } + + Iris.Text({ `Current Font: {Iris._config.TextFont.Family} Weight: {Iris._config.TextFont.Weight} Style: {Iris._config.TextFont.Style}` }) + Iris.SeparatorText({ "Size" }) + + local TextSize = Iris.SliderNum({ "Font Size", 1, 4, 20 }, { number = Iris.WeakState(Iris._config.TextSize) }) + if TextSize.numberChanged() then + UpdatedConfig.value["TextSize"] = TextSize.state.number:get() + end + + Iris.SeparatorText({ "Properties" }) + + local TextFont = Iris.WeakState(Iris._config.TextFont.Family) + local FontWeight = Iris.ComboEnum({ "Font Weight" }, { index = Iris.WeakState(Iris._config.TextFont.Weight) }, Enum.FontWeight) + local FontStyle = Iris.ComboEnum({ "Font Style" }, { index = Iris.WeakState(Iris._config.TextFont.Style) }, Enum.FontStyle) + + Iris.SeparatorText({ "Fonts" }) + for name: string, font: Font in fonts do + font = Font.new(font.Family, FontWeight.state.index.value, FontStyle.state.index.value) + Iris.SameLine() + do + Iris.PushConfig({ + TextFont = font, + }) + + if Iris.Selectable({ `{name} | "The quick brown fox jumps over the lazy dog."`, font.Family }, { index = TextFont }).selected() then + UpdatedConfig.value["TextFont"] = font + end + Iris.PopConfig() + end + Iris.End() + end + end, + }, + } + + Iris.Window({ "Style Editor" }, { isOpened = showStyleEditor }) + do + Iris.Text({ "Customize the look of Iris in realtime." }) + + local ThemeState = Iris.State("Dark Theme") + if Iris.ComboArray({ "Theme" }, { index = ThemeState }, { "Dark Theme", "Light Theme" }).closed() then + if ThemeState.value == "Dark Theme" then + Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorDark) + elseif ThemeState.value == "Light Theme" then + Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorLight) + end + end + + local SizeState = Iris.State("Classic Size") + if Iris.ComboArray({ "Size" }, { index = SizeState }, { "Classic Size", "Larger Size" }).closed() then + if SizeState.value == "Classic Size" then + Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeDefault) + elseif SizeState.value == "Larger Size" then + Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeClear) + end + end + + Iris.SameLine() + do + if Iris.Button({ "Revert" }).clicked() then + Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorDark) + Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeDefault) + ThemeState:set("Dark Theme") + SizeState:set("Classic Size") + end + + helpMarker("Reset Iris to the default theme and size.") + end + Iris.End() + + Iris.TabBar() + do + for i, v in ipairs(styleList) do + Iris.Tab({ v[1] }) + do + styleList[i][2]() + end + Iris.End() + end + end + Iris.End() + + Iris.Separator() + end + Iris.End() + end + end + + local function widgetEventInteractivity() + Iris.CollapsingHeader({ "Widget Event Interactivity" }) + do + local clickCount = Iris.State(0) + if Iris.Button({ "Click to increase Number" }).clicked() then + clickCount:set(clickCount:get() + 1) + end + Iris.Text({ "The Number is: " .. clickCount:get() }) + + Iris.Separator() + + local showEventText = Iris.State(false) + local selectedEvent = Iris.State("clicked") + + Iris.SameLine() + do + Iris.RadioButton({ "clicked", "clicked" }, { index = selectedEvent }) + Iris.RadioButton({ "rightClicked", "rightClicked" }, { index = selectedEvent }) + Iris.RadioButton({ "doubleClicked", "doubleClicked" }, { index = selectedEvent }) + Iris.RadioButton({ "ctrlClicked", "ctrlClicked" }, { index = selectedEvent }) + end + Iris.End() + + Iris.SameLine() + do + local button = Iris.Button({ selectedEvent:get() .. " to reveal text" }) + if button[selectedEvent:get()]() then + showEventText:set(not showEventText:get()) + end + if showEventText:get() then + Iris.Text({ "Here i am!" }) + end + end + Iris.End() + + Iris.Separator() + + local showTextTimer = Iris.State(0) + Iris.SameLine() + do + if Iris.Button({ "Click to show text for 20 frames" }).clicked() then + showTextTimer:set(20) + end + if showTextTimer:get() > 0 then + Iris.Text({ "Here i am!" }) + end + end + Iris.End() + + showTextTimer:set(math.max(0, showTextTimer:get() - 1)) + Iris.Text({ "Text Timer: " .. showTextTimer:get() }) + + local checkbox0 = Iris.Checkbox({ "Event-tracked checkbox" }) + Iris.Indent() + do + Iris.Text({ "unchecked: " .. tostring(checkbox0.unchecked()) }) + Iris.Text({ "checked: " .. tostring(checkbox0.checked()) }) + end + Iris.End() + + Iris.SameLine() + do + if Iris.Button({ "Hover over me" }).hovered() then + Iris.Text({ "The button is hovered" }) + end + end + Iris.End() + end + Iris.End() + end + + local function widgetStateInteractivity() + Iris.CollapsingHeader({ "Widget State Interactivity" }) + do + local checkbox0 = Iris.Checkbox({ "Widget-Generated State" }) + Iris.Text({ `isChecked: {checkbox0.state.isChecked.value}\n` }) + + local checkboxState0 = Iris.State(false) + local checkbox1 = Iris.Checkbox({ "User-Generated State" }, { isChecked = checkboxState0 }) + Iris.Text({ `isChecked: {checkbox1.state.isChecked.value}\n` }) + + local checkbox2 = Iris.Checkbox({ "Widget Coupled State" }) + local checkbox3 = Iris.Checkbox({ "Coupled to above Checkbox" }, { isChecked = checkbox2.state.isChecked }) + Iris.Text({ `isChecked: {checkbox3.state.isChecked.value}\n` }) + + local checkboxState1 = Iris.State(false) + local _checkbox4 = Iris.Checkbox({ "Widget and Code Coupled State" }, { isChecked = checkboxState1 }) + local Button0 = Iris.Button({ "Click to toggle above checkbox" }) + if Button0.clicked() then + checkboxState1:set(not checkboxState1:get()) + end + Iris.Text({ `isChecked: {checkboxState1.value}\n` }) + + local checkboxState2 = Iris.State(true) + local checkboxState3 = Iris.ComputedState(checkboxState2, function(newValue) + return not newValue + end) + local _checkbox5 = Iris.Checkbox({ "ComputedState (dynamic coupling)" }, { isChecked = checkboxState2 }) + local _checkbox5 = Iris.Checkbox({ "Inverted of above checkbox" }, { isChecked = checkboxState3 }) + Iris.Text({ `isChecked: {checkboxState3.value}\n` }) + end + Iris.End() + end + + local function dynamicStyle() + Iris.CollapsingHeader({ "Dynamic Styles" }) + do + local colorH = Iris.State(0) + Iris.SameLine() + do + if Iris.Button({ "Change Color" }).clicked() then + colorH:set(math.random()) + end + Iris.Text({ "Hue: " .. math.floor(colorH:get() * 255) }) + helpMarker("Using PushConfig with a changing value, this can be done with any config field") + end + Iris.End() + + Iris.PushConfig({ TextColor = Color3.fromHSV(colorH:get(), 1, 1) }) + Iris.Text({ "Text with a unique and changable color" }) + Iris.PopConfig() + end + Iris.End() + end + + local function tablesDemo() + local showTablesTree = Iris.State(false) + + Iris.CollapsingHeader({ "Tables & Columns" }, { isUncollapsed = showTablesTree }) + if showTablesTree.value == false then + -- optimization to skip code which draws GUI which wont be seen. + -- its a trade off because when the tree becomes opened widgets will all have to be generated again. + -- Dear ImGui utilizes the same trick, but its less useful here because the Retained mode Backend + Iris.End() + else + Iris.Tree({ "Basic" }) + do + Iris.SameLine() + do + Iris.Text({ "Table using NextColumn syntax:" }) + helpMarker("calling Iris.NextColumn() in the inner loop,\nwhich automatically goes to the next row at the end.") + end + Iris.End() + + Iris.Table({ 3 }) + do + for i = 1, 4 do + for i2 = 1, 3 do + Iris.Text({ `Row: {i}, Column: {i2}` }) + Iris.NextColumn() + end + end + end + Iris.End() + + Iris.Text({ "" }) + + Iris.SameLine() + do + Iris.Text({ "Table using NextColumn and NextRow syntax:" }) + helpMarker("Calling Iris.NextColumn() in the inner loop and Iris.NextRow() in the outer loop,\nto acehieve a visually identical result. Technically they are not the same.") + end + Iris.End() + + Iris.Table({ 3 }) + do + for j = 1, 4 do + for i = 1, 3 do + Iris.Text({ `Row: {j}, Column: {i}` }) + Iris.NextColumn() + end + Iris.NextRow() + end + end + Iris.End() + end + Iris.End() + + Iris.Tree({ "Headers, borders and backgrounds" }) + do + local Type = Iris.State(0) + local Header = Iris.State(false) + local RowBackgrounds = Iris.State(false) + local OuterBorders = Iris.State(true) + local InnerBorders = Iris.State(true) + + Iris.Checkbox({ "Table header row" }, { isChecked = Header }) + Iris.Checkbox({ "Table row backgrounds" }, { isChecked = RowBackgrounds }) + Iris.Checkbox({ "Table outer border" }, { isChecked = OuterBorders }) + Iris.Checkbox({ "Table inner borders" }, { isChecked = InnerBorders }) + Iris.SameLine() + do + Iris.Text({ "Cell contents" }) + Iris.RadioButton({ "Text", 0 }, { index = Type }) + Iris.RadioButton({ "Fill button", 1 }, { index = Type }) + end + Iris.End() + + Iris.Table({ 3, Header.value, RowBackgrounds.value, OuterBorders.value, InnerBorders.value }) + do + Iris.SetHeaderColumnIndex(1) + for j = 0, 4 do + for i = 1, 3 do + if Type.value == 0 then + Iris.Text({ `Cell ({i}, {j})` }) + else + Iris.Button({ `Cell ({i}, {j})`, UDim2.fromScale(1, 0) }) + end + Iris.NextColumn() + end + end + end + Iris.End() + end + Iris.End() + + Iris.Tree({ "Sizing" }) + do + local Resizable = Iris.State(false) + local LimitWidth = Iris.State(false) + Iris.Checkbox({ "Resizable" }, { isChecked = Resizable }) + Iris.Checkbox({ "Limit Table Width" }, { isChecked = LimitWidth }) + + do + Iris.SeparatorText({ "stretch, equal" }) + Iris.Table({ 3, false, true, true, true, Resizable.value }) + do + for _ = 1, 3 do + for _ = 1, 3 do + Iris.Text({ "stretch" }) + Iris.NextColumn() + end + end + end + Iris.End() + Iris.Table({ 3, false, true, true, true, Resizable.value }) + do + for _ = 1, 3 do + for i = 1, 3 do + Iris.Text({ string.rep(string.char(64 + i), 4 * i) }) + Iris.NextColumn() + end + end + end + Iris.End() + end + + do + Iris.SeparatorText({ "stretch, proportional" }) + Iris.Table({ 3, false, true, true, true, Resizable.value, false, true }) + do + for _ = 1, 3 do + for _ = 1, 3 do + Iris.Text({ "stretch" }) + Iris.NextColumn() + end + end + end + Iris.End() + Iris.Table({ 3, false, true, true, true, Resizable.value, false, true }) + do + for _ = 1, 3 do + for i = 1, 3 do + Iris.Text({ string.rep(string.char(64 + i), 4 * i) }) + Iris.NextColumn() + end + end + end + Iris.End() + end + + do + Iris.SeparatorText({ "fixed, equal" }) + Iris.Table({ 3, false, true, true, true, Resizable.value, true, false, LimitWidth.value }) + do + for _ = 1, 3 do + for _ = 1, 3 do + Iris.Text({ "fixed" }) + Iris.NextColumn() + end + end + end + Iris.End() + Iris.Table({ 3, false, true, true, true, Resizable.value, true, false, LimitWidth.value }) + do + for _ = 1, 3 do + for i = 1, 3 do + Iris.Text({ string.rep(string.char(64 + i), 4 * i) }) + Iris.NextColumn() + end + end + end + Iris.End() + end + + do + Iris.SeparatorText({ "fixed, proportional" }) + Iris.Table({ 3, false, true, true, true, Resizable.value, true, true, LimitWidth.value }) + do + for _ = 1, 3 do + for _ = 1, 3 do + Iris.Text({ "fixed" }) + Iris.NextColumn() + end + end + end + Iris.End() + Iris.Table({ 3, false, true, true, true, Resizable.value, true, true, LimitWidth.value }) + do + for _ = 1, 3 do + for i = 1, 3 do + Iris.Text({ string.rep(string.char(64 + i), 4 * i) }) + Iris.NextColumn() + end + end + end + Iris.End() + end + end + Iris.End() + + Iris.Tree({ "Resizable" }) + do + local NumColumns = Iris.State(4) + local NumRows = Iris.State(3) + local TableUseButtons = Iris.State(false) + + local HeaderState = Iris.State(true) + local BackgroundState = Iris.State(true) + local OuterBorderState = Iris.State(true) + local InnerBorderState = Iris.State(true) + local ResizableState = Iris.State(false) + local FixedWidthState = Iris.State(false) + local ProportionalWidthState = Iris.State(false) + local LimitTableWidthState = Iris.State(false) + + local AddExtra = Iris.State(false) + + local WidthState = Iris.State(table.create(10, 100)) + + Iris.SliderNum({ "Num Columns", 1, 1, 10 }, { number = NumColumns }) + Iris.SliderNum({ "Number of rows", 1, 0, 100 }, { number = NumRows }) + + Iris.SameLine() + do + Iris.RadioButton({ "Buttons", true }, { index = TableUseButtons }) + Iris.RadioButton({ "Text", false }, { index = TableUseButtons }) + end + Iris.End() + + Iris.Table({ 3 }) + do + Iris.Checkbox({ "Show Header Row" }, { isChecked = HeaderState }) + Iris.NextColumn() + Iris.Checkbox({ "Show Row Backgrounds" }, { isChecked = BackgroundState }) + Iris.NextColumn() + Iris.Checkbox({ "Show Outer Border" }, { isChecked = OuterBorderState }) + Iris.NextColumn() + Iris.Checkbox({ "Show Inner Border" }, { isChecked = InnerBorderState }) + Iris.NextColumn() + Iris.Checkbox({ "Resizable" }, { isChecked = ResizableState }) + Iris.NextColumn() + Iris.Checkbox({ "Fixed Width" }, { isChecked = FixedWidthState }) + Iris.NextColumn() + Iris.Checkbox({ "Proportional Width" }, { isChecked = ProportionalWidthState }) + Iris.NextColumn() + Iris.Checkbox({ "Limit Table Width" }, { isChecked = LimitTableWidthState }) + Iris.NextColumn() + Iris.Checkbox({ "Add extra" }, { isChecked = AddExtra }) + Iris.NextColumn() + end + Iris.End() + + for i = 1, NumColumns.value do + local increment = if FixedWidthState.value == true then 1 else 0.05 + local min = if FixedWidthState.value == true then 2 else 0.05 + local max = if FixedWidthState.value == true then 480 else 1 + Iris.SliderNum({ `Column {i} Width`, increment, min, max }, { + number = Iris.TableState(WidthState.value, i, function(value: number) + -- we have to force the state to change, because comparing two tables is equal + WidthState.value[i] = value + WidthState:set(WidthState.value, true) + return false + end), + }) + end + + Iris.PushConfig({ + NumColumns = NumColumns.value, + }) + Iris.Table( + { NumColumns.value, HeaderState.value, BackgroundState.value, OuterBorderState.value, InnerBorderState.value, ResizableState.value, FixedWidthState.value, ProportionalWidthState.value, LimitTableWidthState.value }, + { widths = WidthState } + ) + do + Iris.SetHeaderColumnIndex(1) + for i = 0, NumRows:get() do + for j = 1, NumColumns.value do + if i == 0 then + if TableUseButtons.value then + Iris.Button({ `H: {j}` }) + else + Iris.Text({ `H: {j}` }) + end + else + if TableUseButtons.value then + Iris.Button({ `R: {i}, C: {j}` }) + Iris.Button({ string.rep("...", j) }) + else + Iris.Text({ `R: {i}, C: {j}` }) + Iris.Text({ string.rep("...", j) }) + end + end + Iris.NextColumn() + end + end + + if AddExtra.value then + Iris.Text({ "A really long piece of text!" }) + end + end + Iris.End() + Iris.PopConfig() + end + Iris.End() + + Iris.End() + end + end + + local function layoutDemo() + Iris.CollapsingHeader({ "Widget Layout" }) + do + Iris.Tree({ "Widget Alignment" }) + do + Iris.Text({ "Iris.SameLine has optional argument supporting horizontal and vertical alignments." }) + Iris.Text({ "This allows widgets to be place anywhere on the line." }) + Iris.Separator() + + Iris.SameLine() + do + Iris.Text({ "By default child widgets will be aligned to the left." }) + helpMarker('Iris.SameLine()\n\tIris.Button({ "Button A" })\n\tIris.Button({ "Button B" })\nIris.End()') + end + Iris.End() + + Iris.SameLine() + do + Iris.Button({ "Button A" }) + Iris.Button({ "Button B" }) + end + Iris.End() + + Iris.SameLine() + do + Iris.Text({ "But can be aligned to the center." }) + helpMarker('Iris.SameLine({ nil, nil, Enum.HorizontalAlignment.Center })\n\tIris.Button({ "Button A" })\n\tIris.Button({ "Button B" })\nIris.End()') + end + Iris.End() + + Iris.SameLine({ nil, nil, Enum.HorizontalAlignment.Center }) + do + Iris.Button({ "Button A" }) + Iris.Button({ "Button B" }) + end + Iris.End() + + Iris.SameLine() + do + Iris.Text({ "Or right." }) + helpMarker('Iris.SameLine({ nil, nil, Enum.HorizontalAlignment.Right })\n\tIris.Button({ "Button A" })\n\tIris.Button({ "Button B" })\nIris.End()') + end + Iris.End() + + Iris.SameLine({ nil, nil, Enum.HorizontalAlignment.Right }) + do + Iris.Button({ "Button A" }) + Iris.Button({ "Button B" }) + end + Iris.End() + + Iris.Separator() + + Iris.SameLine() + do + Iris.Text({ "You can also specify the padding." }) + helpMarker('Iris.SameLine({ 0, nil, Enum.HorizontalAlignment.Center })\n\tIris.Button({ "Button A" })\n\tIris.Button({ "Button B" })\nIris.End()') + end + Iris.End() + + Iris.SameLine({ 0, nil, Enum.HorizontalAlignment.Center }) + do + Iris.Button({ "Button A" }) + Iris.Button({ "Button B" }) + end + Iris.End() + end + Iris.End() + + Iris.Tree({ "Widget Sizing" }) + do + Iris.Text({ "Nearly all widgets are the minimum size of the content." }) + Iris.Text({ "For example, text and button widgets will be the size of the text labels." }) + Iris.Text({ "Some widgets, such as the Image and Button have Size arguments will will set the size of them." }) + Iris.Separator() + + textAndHelpMarker("The button takes up the full screen-width.", 'Iris.Button({ "Button", UDim2.fromScale(1, 0) })') + Iris.Button({ "Button", UDim2.fromScale(1, 0) }) + textAndHelpMarker("The button takes up half the screen-width.", 'Iris.Button({ "Button", UDim2.fromScale(0.5, 0) })') + Iris.Button({ "Button", UDim2.fromScale(0.5, 0) }) + + textAndHelpMarker("Combining with SameLine, the buttons can fill the screen width.", "The button will still be larger that the text size.") + local num = Iris.State(2) + Iris.SliderNum({ "Number of Buttons", 1, 1, 8 }, { number = num }) + Iris.SameLine({ 0, nil, Enum.HorizontalAlignment.Center }) + do + for i = 1, num.value do + Iris.Button({ `Button {i}`, UDim2.fromScale(1 / num.value, 0) }) + end + end + Iris.End() + end + Iris.End() + + Iris.Tree({ "Content Width" }) + do + local value = Iris.State(50) + local index = Iris.State(Enum.Axis.X) + + Iris.Text({ "The Content Width is a size property which determines the width of input fields." }) + Iris.SameLine() + do + Iris.Text({ "By default the value is UDim.new(0.65, 0)" }) + helpMarker("This is the default value from Dear ImGui.\nIt is 65% of the window width.") + end + Iris.End() + + Iris.Text({ "This works well, but sometimes we know how wide elements are going to be and want to maximise the space." }) + Iris.Text({ "Therefore, we can use Iris.PushConfig() to change the width" }) + + Iris.Separator() + + Iris.SameLine() + do + Iris.Text({ "Content Width = 150 pixels" }) + helpMarker("UDim.new(0, 150)") + end + Iris.End() + + Iris.PushConfig({ ContentWidth = UDim.new(0, 150) }) + Iris.DragNum({ "number", 1, 0, 100 }, { number = value }) + Iris.InputEnum({ "axis" }, { index = index }, Enum.Axis) + Iris.PopConfig() + + Iris.SameLine() + do + Iris.Text({ "Content Width = 50% window width" }) + helpMarker("UDim.new(0.5, 0)") + end + Iris.End() + + Iris.PushConfig({ ContentWidth = UDim.new(0.5, 0) }) + Iris.DragNum({ "number", 1, 0, 100 }, { number = value }) + Iris.InputEnum({ "axis" }, { index = index }, Enum.Axis) + Iris.PopConfig() + + Iris.SameLine() + do + Iris.Text({ "Content Width = -150 pixels from the right side" }) + helpMarker("UDim.new(1, -150)") + end + Iris.End() + + Iris.PushConfig({ ContentWidth = UDim.new(1, -150) }) + Iris.DragNum({ "number", 1, 0, 100 }, { number = value }) + Iris.InputEnum({ "axis" }, { index = index }, Enum.Axis) + Iris.PopConfig() + end + Iris.End() + + Iris.Tree({ "Content Height" }) + do + local text = Iris.State("a single line") + local value = Iris.State(50) + local index = Iris.State(Enum.Axis.X) + local progress = Iris.State(0) + + -- formula to cycle between 0 and 100 linearly + local newValue = math.clamp((math.abs((os.clock() * 15) % 100 - 50)) - 7.5, 0, 35) / 35 + progress:set(newValue) + + Iris.Text({ "The Content Height is a size property that determines the minimum size of certain widgets." }) + Iris.Text({ "By default the value is UDim.new(0, 0), so there is no minimum height." }) + Iris.Text({ "We use Iris.PushConfig() to change this value." }) + + Iris.Separator() + Iris.SameLine() + do + Iris.Text({ "Content Height = 0 pixels" }) + helpMarker("UDim.new(0, 0)") + end + Iris.End() + + Iris.InputText({ "text" }, { text = text }) + Iris.ProgressBar({ "progress" }, { progress = progress }) + Iris.DragNum({ "number", 1, 0, 100 }, { number = value }) + Iris.ComboEnum({ "axis" }, { index = index }, Enum.Axis) + + Iris.SameLine() + do + Iris.Text({ "Content Height = 60 pixels" }) + helpMarker("UDim.new(0, 60)") + end + Iris.End() + + Iris.PushConfig({ ContentHeight = UDim.new(0, 60) }) + Iris.InputText({ "text", nil, nil, true }, { text = text }) + Iris.ProgressBar({ "progress" }, { progress = progress }) + Iris.DragNum({ "number", 1, 0, 100 }, { number = value }) + Iris.ComboEnum({ "axis" }, { index = index }, Enum.Axis) + Iris.PopConfig() + + Iris.Text({ "This property can be used to force the height of a text box." }) + Iris.Text({ "Just make sure you enable the MultiLine argument." }) + end + Iris.End() + end + Iris.End() + end + + -- showcases how widgets placed outside of a window are placed inside root + local function windowlessDemo() + Iris.PushConfig({ ItemWidth = UDim.new(0, 150) }) + Iris.SameLine() + do + Iris.TextWrapped({ "Windowless widgets" }) + helpMarker("Widgets which are placed outside of a window will appear on the top left side of the screen.") + end + Iris.End() + + Iris.Button({}) + Iris.Tree({}) + do + Iris.InputText({}) + end + Iris.End() + + Iris.PopConfig() + end + + -- main demo window + return function() + local NoTitleBar: Types.State = Iris.State(false) + local NoBackground: Types.State = Iris.State(false) + local NoCollapse: Types.State = Iris.State(false) + local NoClose: Types.State = Iris.State(true) + local NoMove: Types.State = Iris.State(false) + local NoScrollbar: Types.State = Iris.State(false) + local NoResize: Types.State = Iris.State(false) + local NoNav: Types.State = Iris.State(false) + local NoMenu: Types.State = Iris.State(false) + + if showMainWindow.value == false then + Iris.Checkbox({ "Open main window" }, { isChecked = showMainWindow }) + return + end + + debug.profilebegin("Iris/Demo/Window") + local window: Types.Window = Iris.Window({ + [Iris.Args.Window.Title] = "Iris Demo Window", + [Iris.Args.Window.NoTitleBar] = NoTitleBar.value, + [Iris.Args.Window.NoBackground] = NoBackground.value, + [Iris.Args.Window.NoCollapse] = NoCollapse.value, + [Iris.Args.Window.NoClose] = NoClose.value, + [Iris.Args.Window.NoMove] = NoMove.value, + [Iris.Args.Window.NoScrollbar] = NoScrollbar.value, + [Iris.Args.Window.NoResize] = NoResize.value, + [Iris.Args.Window.NoNav] = NoNav.value, + [Iris.Args.Window.NoMenu] = NoMenu.value, + }, { size = Iris.State(Vector2.new(600, 550)), position = Iris.State(Vector2.new(100, 25)), isOpened = showMainWindow }) + + if window.state.isUncollapsed.value and window.state.isOpened.value then + debug.profilebegin("Iris/Demo/MenuBar") + mainMenuBar() + debug.profileend() + + Iris.Text({ "Iris says hello. (" .. Iris.Internal._version .. ")" }) + + debug.profilebegin("Iris/Demo/Options") + Iris.CollapsingHeader({ "Window Options" }) + do + Iris.Table({ 3, false, false, false }) + do + Iris.Checkbox({ "NoTitleBar" }, { isChecked = NoTitleBar }) + Iris.NextColumn() + Iris.Checkbox({ "NoBackground" }, { isChecked = NoBackground }) + Iris.NextColumn() + Iris.Checkbox({ "NoCollapse" }, { isChecked = NoCollapse }) + Iris.NextColumn() + Iris.Checkbox({ "NoClose" }, { isChecked = NoClose }) + Iris.NextColumn() + Iris.Checkbox({ "NoMove" }, { isChecked = NoMove }) + Iris.NextColumn() + Iris.Checkbox({ "NoScrollbar" }, { isChecked = NoScrollbar }) + Iris.NextColumn() + Iris.Checkbox({ "NoResize" }, { isChecked = NoResize }) + Iris.NextColumn() + Iris.Checkbox({ "NoNav" }, { isChecked = NoNav }) + Iris.NextColumn() + Iris.Checkbox({ "NoMenu" }, { isChecked = NoMenu }) + Iris.NextColumn() + end + Iris.End() + end + Iris.End() + debug.profileend() + + debug.profilebegin("Iris/Demo/Events") + widgetEventInteractivity() + debug.profileend() + + debug.profilebegin("Iris/Demo/States") + widgetStateInteractivity() + debug.profileend() + + debug.profilebegin("Iris/Demo/Recursive") + Iris.CollapsingHeader({ "Recursive Tree" }) + recursiveTree() + Iris.End() + debug.profileend() + + debug.profilebegin("Iris/Demo/Style") + dynamicStyle() + debug.profileend() + + Iris.Separator() + + debug.profilebegin("Iris/Demo/Widgets") + Iris.CollapsingHeader({ "Widgets" }) + do + for _, name in widgetDemosOrder do + debug.profilebegin(`Iris/Demo/Widgets/{name}`) + widgetDemos[name]() + debug.profileend() + end + end + Iris.End() + debug.profileend() + + debug.profilebegin("Iris/Demo/Tables") + tablesDemo() + debug.profileend() + + debug.profilebegin("Iris/Demo/Layout") + layoutDemo() + debug.profileend() + end + Iris.End() + debug.profileend() + + if showRecursiveWindow.value then + recursiveWindow(showRecursiveWindow) + end + if showRuntimeInfo.value then + runtimeInfo() + end + if showDebugWindow.value then + debugPanel() + end + if showStyleEditor.value then + styleEditor() + end + if showWindowlessDemo.value then + windowlessDemo() + end + + if showMainMenuBarWindow.value then + mainMenuBarExample() + end + + return window + end +end diff --git a/src/DebuggerUI/Shared/External/iris/init.luau b/src/DebuggerUI/Shared/External/iris/init.luau new file mode 100644 index 0000000..7f55218 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/init.luau @@ -0,0 +1,692 @@ +--!optimize 2 +local Types = require(script.Types) + +--[=[ + @class Iris + + Iris; contains the all user-facing functions and properties. + A set of internal functions can be found in `Iris.Internal` (only use if you understand). + + In its simplest form, users may start Iris by using + ```lua + Iris.Init() + + Iris:Connect(function() + Iris.Window({"My First Window!"}) + Iris.Text({"Hello, World"}) + Iris.Button({"Save"}) + Iris.InputNum({"Input"}) + Iris.End() + end) + ``` +]=] +local Iris = {} :: Types.Iris + +local Internal: Types.Internal = require(script.Internal)(Iris) + +--[=[ + @within Iris + @prop Disabled boolean + + While Iris.Disabled is true, execution of Iris and connected functions will be paused. + The widgets are not destroyed, they are just frozen so no changes will happen to them. +]=] +Iris.Disabled = false + +--[=[ + @within Iris + @prop Args { [string]: { [string]: any } } + + Provides a list of every possible Argument for each type of widget to it's index. + For instance, `Iris.Args.Window.NoResize`. + The Args table is useful for using widget Arguments without remembering their order. + ```lua + Iris.Window({"My Window", [Iris.Args.Window.NoResize] = true}) + ``` +]=] +Iris.Args = {} + +--[=[ + @ignore + @within Iris + @prop Events table + + -todo: work out what this is used for. +]=] +Iris.Events = {} + +--[=[ + @within Iris + @function Init + @param parentInstance Instance? -- where Iris will place widgets UIs under, defaulting to [PlayerGui] + @param eventConnection (RBXScriptSignal | () -> () | false)? -- the event to determine an Iris cycle, defaulting to [Heartbeat] + @param allowMultipleInits boolean? -- allows subsequent calls 'Iris.Init()' to do nothing rather than error about initialising again, defaulting to false + @return Iris + + Initializes Iris and begins rendering. Can only be called once. + See [Iris.Shutdown] to stop Iris, or [Iris.Disabled] to temporarily disable Iris. + + Once initialized, [Iris:Connect] can be used to create a widget. + + If the `eventConnection` is `false` then Iris will not create a cycle loop and the user will need to call [Internal._cycle] every frame. +]=] +function Iris.Init(parentInstance: Instance?, eventConnection: (RBXScriptSignal | (() -> number) | false)?, allowMultipleInits: boolean): Types.Iris + assert(Internal._shutdown == false, "Iris.Init() cannot be called once shutdown.") + assert(Internal._started == false or allowMultipleInits == true, "Iris.Init() can only be called once.") + + if Internal._started then + return Iris + end + + if parentInstance == nil then + -- coalesce to playerGui + parentInstance = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui") + end + if eventConnection == nil then + -- coalesce to Heartbeat + 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 + + -- spawns the connection to call `Internal._cycle()` within. + task.spawn(function() + if typeof(eventConnection) == "function" then + while Internal._started do + local deltaTime: number = eventConnection() + Internal._cycle(deltaTime) + end + elseif eventConnection ~= nil and eventConnection ~= false then + Internal._eventConnection = eventConnection:Connect(function(...) + Internal._cycle(...) + end) + end + end) + + return Iris +end + +--[=[ + @within Iris + @function Shutdown + + Shuts Iris down. This can only be called once, and Iris cannot be started once shut down. +]=] +function Iris.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 + +--[=[ + @within Iris + @method Connect + @param callback () -> () -- the callback containg the Iris code + @return () -> () -- call to disconnect it + + Connects a function which will execute every Iris cycle. [Iris.Init] must be called before connecting. + + A cycle is determined by the `eventConnection` passed to [Iris.Init] (default to [RunService.Heartbeat]). + + Multiple callbacks can be added to Iris from many different scripts or modules. +]=] +function Iris:Connect(callback: () -> ()): () -> () -- this uses method syntax for no reason. + if Internal._started == false then + warn("Iris:Connect() was called before calling Iris.Init(); always initialise Iris first.") + end + local connectionIndex: number = #Internal._connectedFunctions + 1 + Internal._connectedFunctions[connectionIndex] = callback + return function() + Internal._connectedFunctions[connectionIndex] = nil + end +end + +--[=[ + @within Iris + @function Append + @param userInstance GuiObject -- the Roblox [Instance] to insert into Iris + + Inserts any Roblox [Instance] into Iris. + + The parent of the inserted instance can either be determined by the `_config.Parent` + property or by the current parent widget from the stack. +]=] +function Iris.Append(userInstance: GuiObject) + local parentWidget: Types.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" } :: Types.Widget) + end + userInstance.Parent = widgetInstanceParent +end + +--[=[ + @within Iris + @function End + + Marks the end of any widgets which contain children. For example: + ```lua + -- Widgets placed here **will not** be inside the tree + Iris.Text({"Above and outside the tree"}) + + -- A Tree widget can contain children. + -- We must therefore remember to call `Iris.End()` + Iris.Tree({"My First Tree"}) + -- Widgets placed here **will** be inside the tree + Iris.Text({"Tree item 1"}) + Iris.Text({"Tree item 2"}) + Iris.End() + + -- Widgets placed here **will not** be inside the tree + Iris.Text({"Below and outside the tree"}) + ``` + :::caution Caution: Error + Seeing the error `Callback has too few calls to Iris.End()` or `Callback has too many calls to Iris.End()`? + Using the wrong amount of `Iris.End()` calls in your code will lead to an error. + + Each widget called which might have children should be paired with a call to `Iris.End()`, **even if the Widget doesnt currently have any children**. + ::: +]=] +function Iris.End() + if Internal._stackIndex == 1 then + error("Too many calls to Iris.End().", 2) + end + + Internal._IDStack[Internal._stackIndex] = nil + Internal._stackIndex -= 1 +end + +--[[ + ------------------------ + [SECTION] Config + ------------------------ +]] + +--[=[ + @within Iris + @function ForceRefresh + + Destroys and regenerates all instances used by Iris. Useful if you want to propogate state changes. + :::caution Caution: Performance + Because this function Deletes and Initializes many instances, it may cause **performance issues** when used with many widgets. + In **no** case should it be called every frame. + ::: +]=] +function Iris.ForceRefresh() + Internal._globalRefreshRequested = true +end + +--[=[ + @within Iris + @function UpdateGlobalConfig + @param deltaStyle { [string]: any } -- a table containing the changes in style ex: `{ItemWidth = UDim.new(0, 100)}` + + Customizes the configuration which **every** widget will inherit from. + + It can be used along with [Iris.TemplateConfig] to easily swap styles, for example: + ```lua + Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorLight) -- use light theme + ``` + :::caution Caution: Performance + This function internally calls [Iris.ForceRefresh] so that style changes are propogated. + + As such, it may cause **performance issues** when used with many widgets. + In **no** case should it be called every frame. + ::: +]=] +function Iris.UpdateGlobalConfig(deltaStyle: { [string]: any }) + for index, style in deltaStyle do + Internal._rootConfig[index] = style + end + Iris.ForceRefresh() +end + +--[=[ + @within Iris + @function PushConfig + @param deltaStyle { [string]: any } -- a table containing the changes in style ex: `{ItemWidth = UDim.new(0, 100)}` + + Allows cascading of a style by allowing styles to be locally and hierarchically applied. + + Each call to Iris.PushConfig must be paired with a call to [Iris.PopConfig], for example: + ```lua + Iris.Text({"boring text"}) + + Iris.PushConfig({TextColor = Color3.fromRGB(128, 0, 256)}) + Iris.Text({"Colored Text!"}) + Iris.PopConfig() + + Iris.Text({"boring text"}) + ``` +]=] +function Iris.PushConfig(deltaStyle: { [string]: any }) + local ID = Iris.State(-1) + if ID.value == -1 then + ID:set(deltaStyle) + else + -- compare tables + if Internal._deepCompare(ID:get(), deltaStyle) == false then + -- refresh local + ID:set(deltaStyle) + Internal._refreshStack[Internal._refreshLevel] = true + Internal._refreshCounter += 1 + end + end + Internal._refreshLevel += 1 + + Internal._config = setmetatable(deltaStyle, { + __index = Internal._config, + }) :: any +end + +--[=[ + @within Iris + @function PopConfig + + Ends a [Iris.PushConfig] style. + + Each call to [Iris.PopConfig] should match a call to [Iris.PushConfig]. +]=] +function Iris.PopConfig() + Internal._refreshLevel -= 1 + if Internal._refreshStack[Internal._refreshLevel] == true then + Internal._refreshCounter -= 1 + Internal._refreshStack[Internal._refreshLevel] = nil + end + + Internal._config = getmetatable(Internal._config :: any).__index +end + +--[=[ + + @within Iris + @prop TemplateConfig { [string]: { [string]: any } } + + TemplateConfig provides a table of default styles and configurations which you may apply to your UI. +]=] +Iris.TemplateConfig = require(script.config) +Iris.UpdateGlobalConfig(Iris.TemplateConfig.colorDark) -- use colorDark and sizeDefault themes by default +Iris.UpdateGlobalConfig(Iris.TemplateConfig.sizeDefault) +Iris.UpdateGlobalConfig(Iris.TemplateConfig.utilityDefault) +Internal._globalRefreshRequested = false -- UpdatingGlobalConfig changes this to true, leads to Root being generated twice. + +--[[ + -------------------- + [SECTION] ID + -------------------- +]] + +--[=[ + @within Iris + @function PushId + @param id ID -- custom id + + Pushes an id onto the id stack for all future widgets. Use [Iris.PopId] to pop it off the stack. +]=] +function Iris.PushId(ID: Types.ID) + assert(typeof(ID) == "string", "The ID argument to Iris.PushId() to be a string.") + + Internal._newID = true + table.insert(Internal._pushedIds, ID) +end + +--[=[ + @within Iris + @function PopID + + Removes the most recent pushed id from the id stack. +]=] +function Iris.PopId() + if #Internal._pushedIds == 0 then + return + end + + table.remove(Internal._pushedIds) +end + +--[=[ + @within Iris + @function SetNextWidgetID + @param id ID -- custom id. + + Sets the id for the next widget. Useful for using [Iris.Append] on the same widget. + ```lua + Iris.SetNextWidgetId("demo_window") + Iris.Window({ "Window" }) + Iris.Text({ "Text one placed here." }) + Iris.End() + + -- later in the code + + Iris.SetNextWidgetId("demo_window") + Iris.Window() + Iris.Text({ "Text two placed here." }) + Iris.End() + + -- both text widgets will be placed under the same window despite being called separately. + ``` +]=] +function Iris.SetNextWidgetID(ID: Types.ID) + Internal._nextWidgetId = ID +end + +--[[ + ----------------------- + [SECTION] State + ----------------------- +]] + +--[=[ + @within Iris + @function State + @param initialValue T -- the initial value for the state + @return State + @tag State + + Constructs a new [State] object. Subsequent ID calls will return the same object. + :::info + Iris.State allows you to create "references" to the same value while inside your UI drawing loop. + For example: + ```lua + Iris:Connect(function() + local myNumber = 5 + myNumber = myNumber + 1 + Iris.Text({"The number is: " .. myNumber}) + end) + ``` + This is problematic. Each time the function is called, a new myNumber is initialized, instead of retrieving the old one. + The above code will always display 6. + *** + Iris.State solves this problem: + ```lua + Iris:Connect(function() + local myNumber = Iris.State(5) + myNumber:set(myNumber:get() + 1) + Iris.Text({"The number is: " .. myNumber}) + end) + ``` + In this example, the code will work properly, and increment every frame. + ::: +]=] +function Iris.State(initialValue: T): Types.State + local ID: Types.ID = Internal._getID(2) + if Internal._states[ID] then + return Internal._states[ID] + end + Internal._states[ID] = { + ID = ID, + value = initialValue, + lastChangeTick = Iris.Internal._cycleTick, + ConnectedWidgets = {}, + ConnectedFunctions = {}, + } :: any + setmetatable(Internal._states[ID], Internal.StateClass) + return Internal._states[ID] +end + +--[=[ + @within Iris + @function WeakState + @param initialValue T -- the initial value for the state + @return State + @tag State + + Constructs a new state object, subsequent ID calls will return the same object, except all widgets connected to the state are discarded, the state reverts to the passed initialValue +]=] +function Iris.WeakState(initialValue: T): Types.State + local ID: Types.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, + lastChangeTick = Iris.Internal._cycleTick, + ConnectedWidgets = {}, + ConnectedFunctions = {}, + } :: any + setmetatable(Internal._states[ID], Internal.StateClass) + return Internal._states[ID] +end + +--[=[ + @within Iris + @function VariableState + @param variable T -- the variable to track + @param callback (T) -> () -- a function which sets the new variable locally + @return State + @tag State + + Returns a state object linked to a local variable. + + The passed variable is used to check whether the state object should update. The callback method is used to change the local variable when the state changes. + + The existence of such a function is to make working with local variables easier. + Since Iris cannot directly manipulate the memory of the variable, like in C++, it must instead rely on the user updating it through the callback provided. + Additionally, because the state value is not updated when created or called we cannot return the new value back, instead we require a callback for the user to update. + + ```lua + local myNumber = 5 + + local state = Iris.VariableState(myNumber, function(value) + myNumber = value + end) + Iris.DragNum({ "My number" }, { number = state }) + ``` + + This is how Dear ImGui does the same in C++ where we can just provide the memory location to the variable which is then updated directly. + ```cpp + static int myNumber = 5; + ImGui::DragInt("My number", &myNumber); // Here in C++, we can directly pass the variable. + ``` + + :::caution Caution: Update Order + If the variable and state value are different when calling this, the variable value takes precedence. + + Therefore, if you update the state using `state.value = ...` then it will be overwritten by the variable value. + You must use `state:set(...)` if you want the variable to update to the state's value. + ::: +]=] +function Iris.VariableState(variable: T, callback: (T) -> ()): Types.State + local ID: Types.ID = Internal._getID(2) + local state: Types.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, + lastChangeTick = Iris.Internal._cycleTick, + ConnectedWidgets = {}, + ConnectedFunctions = {}, + } :: Types.State + setmetatable(newState, Internal.StateClass) + Internal._states[ID] = newState + + newState:onChange(callback) + + return newState +end + +--[=[ + @within Iris + @function TableState + @param table { [K]: V } -- the table containing the value + @param key K -- the key to the value in table + @param callback ((newValue: V) -> false?)? -- a function called when the state is changed + @return State + @tag State + + Similar to Iris.VariableState but takes a table and key to modify a specific value and a callback to determine whether to update the value. + + The passed table and key are used to check the value. The callback is called when the state changes value and determines whether we update the table. + This is useful if we want to monitor a table value which needs to call other functions when changed. + + Since tables are pass-by-reference, we can modify the table anywhere and it will update all other instances. Therefore, we don't need a callback by default. + ```lua + local data = { + myNumber = 5 + } + + local state = Iris.TableState(data, "myNumber") + Iris.DragNum({ "My number" }, { number = state }) + ``` + + Here the `data._started` should never be updated directly, only through the `toggle` function. However, we still want to monitor the value and be able to change it. + Therefore, we use the callback to toggle the function for us and prevent Iris from updating the table value by returning false. + ```lua + local data = { + _started = false + } + + local function toggle(enabled: boolean) + data._started = enabled + if data._started then + start(...) + else + stop(...) + end + end + + local state = Iris.TableState(data, "_started", function(stateValue: boolean) + toggle(stateValue) + return false + end) + Iris.Checkbox({ "Started" }, { isChecked = state }) + ``` + + :::caution Caution: Update Order + If the table value and state value are different when calling this, the table value value takes precedence. + + Therefore, if you update the state using `state.value = ...` then it will be overwritten by the table value. + You must use `state:set(...)` if you want the table value to update to the state's value. + ::: +]=] +function Iris.TableState(tab: { [K]: V }, key: K, callback: ((newValue: V) -> false?)?): Types.State + local value: V = tab[key] + local ID: Types.ID = Internal._getID(2) + local state: Types.State? = Internal._states[ID] + + -- If the table values changes, then we update the state to match. + if state then + if value ~= state.value then + state:set(value) + end + return state + end + + local newState = { + ID = ID, + value = value, + lastChangeTick = Iris.Internal._cycleTick, + ConnectedWidgets = {}, + ConnectedFunctions = {}, + } :: Types.State + setmetatable(newState, Internal.StateClass) + Internal._states[ID] = newState + + -- When a change happens to the state, we update the table value. + 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 + +--[=[ + @within Iris + @function ComputedState + @param firstState State -- State to bind to. + @param onChangeCallback (firstValue: T) -> U -- callback which should return a value transformed from the firstState value + @return State + + Constructs a new State object, but binds its value to the value of another State. + :::info + A common use case for this constructor is when a boolean State needs to be inverted: + ```lua + Iris.ComputedState(otherState, function(newValue) + return not newValue + end) + ``` + ::: +]=] +function Iris.ComputedState(firstState: Types.State, onChangeCallback: (firstValue: T) -> U): Types.State + local ID: Types.ID = Internal._getID(2) + + if Internal._states[ID] then + return Internal._states[ID] + else + Internal._states[ID] = { + ID = ID, + value = onChangeCallback(firstState.value), + lastChangeTick = Iris.Internal._cycleTick, + ConnectedWidgets = {}, + ConnectedFunctions = {}, + } :: Types.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 + +--[=[ + @within Iris + @function ShowDemoWindow + + ShowDemoWindow is a function which creates a Demonstration window. this window contains many useful utilities for coders, + and serves as a refrence for using each part of the library. Ideally, the DemoWindow should always be available in your UI. + It is the same as any other callback you would connect to Iris using [Iris.Connect] + ```lua + Iris:Connect(Iris.ShowDemoWindow) + ``` +]=] +Iris.ShowDemoWindow = require(script.demoWindow)(Iris) + +require(script.widgets)(Internal) +require(script.API)(Iris) + +return Iris diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Button.luau b/src/DebuggerUI/Shared/External/iris/widgets/Button.luau new file mode 100644 index 0000000..eb1845a --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Button.luau @@ -0,0 +1,93 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local abstractButton = { + hasState = false, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Size"] = 2, + }, + Events = { + ["clicked"] = widgets.EVENTS.click(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Button) + local Button: TextButton = Instance.new("TextButton") + Button.Size = UDim2.fromOffset(0, 0) + Button.BackgroundColor3 = Iris._config.ButtonColor + Button.BackgroundTransparency = Iris._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 = Iris._config.ButtonColor, + Transparency = Iris._config.ButtonTransparency, + HoveredColor = Iris._config.ButtonHoveredColor, + HoveredTransparency = Iris._config.ButtonHoveredTransparency, + ActiveColor = Iris._config.ButtonActiveColor, + ActiveTransparency = Iris._config.ButtonActiveTransparency, + }) + + Button.ZIndex = thisWidget.ZIndex + Button.LayoutOrder = thisWidget.ZIndex + + return Button + end, + Update = function(thisWidget: Types.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: Types.Button) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass + widgets.abstractButton = abstractButton + + --stylua: ignore + Iris.WidgetConstructor("Button", widgets.extend(abstractButton, { + Generate = function(thisWidget: Types.Button) + local Button: TextButton = abstractButton.Generate(thisWidget) + Button.Name = "Iris_Button" + + return Button + end, + } :: Types.WidgetClass) + ) + + --stylua: ignore + Iris.WidgetConstructor("SmallButton", widgets.extend(abstractButton, { + Generate = function(thisWidget: Types.Button) + local SmallButton = abstractButton.Generate(thisWidget) :: TextButton + SmallButton.Name = "Iris_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, + } :: Types.WidgetClass) + ) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau b/src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau new file mode 100644 index 0000000..64440d1 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau @@ -0,0 +1,118 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + --stylua: ignore + Iris.WidgetConstructor("Checkbox", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + }, + Events = { + ["checked"] = { + ["Init"] = function(_thisWidget: Types.Checkbox) end, + ["Get"] = function(thisWidget: Types.Checkbox): boolean + return thisWidget.lastCheckedTick == Iris._cycleTick + end, + }, + ["unchecked"] = { + ["Init"] = function(_thisWidget: Types.Checkbox) end, + ["Get"] = function(thisWidget: Types.Checkbox): boolean + return thisWidget.lastUncheckedTick == Iris._cycleTick + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Checkbox) + local Checkbox: TextButton = Instance.new("TextButton") + Checkbox.Name = "Iris_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, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local checkboxSize: number = Iris._config.TextSize + 2 * Iris._config.FramePadding.Y + + local Box: Frame = Instance.new("Frame") + Box.Name = "Box" + Box.Size = UDim2.fromOffset(checkboxSize, checkboxSize) + Box.BackgroundColor3 = Iris._config.FrameBgColor + Box.BackgroundTransparency = Iris._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 = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._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 = Iris._config.CheckMarkColor + Checkmark.ImageTransparency = Iris._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: Types.Checkbox) + local Checkbox = thisWidget.Instance :: TextButton + Checkbox.TextLabel.Text = thisWidget.arguments.Text or "Checkbox" + end, + Discard = function(thisWidget: Types.Checkbox) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.Checkbox) + if thisWidget.state.isChecked == nil then + thisWidget.state.isChecked = Iris._widgetState(thisWidget, "checked", false) + end + end, + UpdateState = function(thisWidget: Types.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 = Iris._cycleTick + 1 + else + Checkmark.Image = "" + thisWidget.lastUncheckedTick = Iris._cycleTick + 1 + end + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Combo.luau b/src/DebuggerUI/Shared/External/iris/widgets/Combo.luau new file mode 100644 index 0000000..e373440 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Combo.luau @@ -0,0 +1,495 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + --stylua: ignore + Iris.WidgetConstructor("Selectable", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Index"] = 2, + ["NoClick"] = 3, + }, + Events = { + ["selected"] = { + ["Init"] = function(_thisWidget: Types.Selectable) end, + ["Get"] = function(thisWidget: Types.Selectable) + return thisWidget.lastSelectedTick == Iris._cycleTick + end, + }, + ["unselected"] = { + ["Init"] = function(_thisWidget: Types.Selectable) end, + ["Get"] = function(thisWidget: Types.Selectable) + return thisWidget.lastUnselectedTick == Iris._cycleTick + end, + }, + ["active"] = { + ["Init"] = function(_thisWidget: Types.Selectable) end, + ["Get"] = function(thisWidget: Types.Selectable) + return thisWidget.state.index.value == thisWidget.arguments.Index + end, + }, + ["clicked"] = widgets.EVENTS.click(function(thisWidget: Types.Widget) + local Selectable = thisWidget.Instance :: Frame + return Selectable.SelectableButton + end), + ["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Types.Widget) + local Selectable = thisWidget.Instance :: Frame + return Selectable.SelectableButton + end), + ["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Types.Widget) + local Selectable = thisWidget.Instance :: Frame + return Selectable.SelectableButton + end), + ["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Types.Widget) + local Selectable = thisWidget.Instance :: Frame + return Selectable.SelectableButton + end), + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + local Selectable = thisWidget.Instance :: Frame + return Selectable.SelectableButton + end), + }, + Generate = function(thisWidget: Types.Selectable) + local Selectable: Frame = Instance.new("Frame") + Selectable.Name = "Iris_Selectable" + Selectable.Size = UDim2.new(Iris._config.ItemWidth, UDim.new(0, Iris._config.TextSize + 2 * Iris._config.FramePadding.Y - Iris._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, Iris._config.TextSize + 2 * Iris._config.FramePadding.Y) + SelectableButton.Position = UDim2.fromOffset(0, -bit32.rshift(Iris._config.ItemSpacing.Y, 1)) -- divide by 2 + SelectableButton.BackgroundColor3 = Iris._config.HeaderColor + SelectableButton.ClipsDescendants = true + + widgets.applyFrameStyle(SelectableButton) + widgets.applyTextStyle(SelectableButton) + widgets.UISizeConstraint(SelectableButton, Vector2.xAxis) + + thisWidget.ButtonColors = { + Color = Iris._config.HeaderColor, + Transparency = 1, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderActiveColor, + ActiveTransparency = Iris._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: Types.Selectable) + local Selectable = thisWidget.Instance :: Frame + local SelectableButton: TextButton = Selectable.SelectableButton + SelectableButton.Text = thisWidget.arguments.Text or "Selectable" + end, + Discard = function(thisWidget: Types.Selectable) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.Selectable) + if thisWidget.state.index == nil then + if thisWidget.arguments.Index ~= nil then + error("A shared state index is required for Iris.Selectables() with an Index argument.", 5) + end + thisWidget.state.index = Iris._widgetState(thisWidget, "index", false) + end + end, + UpdateState = function(thisWidget: Types.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 = Iris._config.HeaderTransparency + SelectableButton.BackgroundTransparency = Iris._config.HeaderTransparency + thisWidget.lastSelectedTick = Iris._cycleTick + 1 + else + thisWidget.ButtonColors.Transparency = 1 + SelectableButton.BackgroundTransparency = 1 + thisWidget.lastUnselectedTick = Iris._cycleTick + 1 + end + end, + } :: Types.WidgetClass) + + local AnyOpenedCombo: boolean = false + local ComboOpenedTick: number = -1 + local OpenedCombo: Types.Combo? = nil + local CachedContentSize: number = 0 + + local function UpdateChildContainerTransform(thisWidget: Types.Combo) + local Combo = thisWidget.Instance :: Frame + local PreviewContainer = Combo.PreviewContainer :: TextButton + local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame + + local previewPosition: Vector2 = PreviewContainer.AbsolutePosition - widgets.GuiOffset + local previewSize: Vector2 = PreviewContainer.AbsoluteSize + local borderSize: number = Iris._config.PopupBorderSize + local screenSize: Vector2 = ChildContainer.Parent.AbsoluteSize + + local absoluteContentSize = thisWidget.UIListLayout.AbsoluteContentSize.Y + CachedContentSize = absoluteContentSize + + local contentsSize: number = absoluteContentSize + 2 * Iris._config.WindowPadding.Y + + local x: number = previewPosition.X + local y: number = previewPosition.Y + previewSize.Y + borderSize + local anchor: Vector2 = Vector2.zero + local distanceToScreen: number = screenSize.Y - y + + -- Only extend upwards if we cannot fully extend downwards, and we are on the bottom half of the screen. + -- i.e. there is more space upwards than there is downwards. + if contentsSize > distanceToScreen and y > (screenSize.Y / 2) then + y = previewPosition.Y - borderSize + anchor = Vector2.yAxis + distanceToScreen = y -- from 0 to the current position + end + + ChildContainer.AnchorPoint = anchor + ChildContainer.Position = UDim2.fromOffset(x, y) + + local height = math.min(contentsSize, distanceToScreen) + ChildContainer.Size = UDim2.fromOffset(PreviewContainer.AbsoluteSize.X, height) + end + + table.insert(Iris._postCycleCallbacks, function() + if AnyOpenedCombo and OpenedCombo then + local contentSize = OpenedCombo.UIListLayout.AbsoluteContentSize.Y + if contentSize ~= CachedContentSize then + UpdateChildContainerTransform(OpenedCombo) + end + end + end) + + local function UpdateComboState(input: InputObject) + if not Iris._started then + return + end + if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.MouseButton2 and input.UserInputType ~= Enum.UserInputType.Touch and input.UserInputType ~= Enum.UserInputType.MouseWheel then + return + end + if AnyOpenedCombo == false or not OpenedCombo then + return + end + if ComboOpenedTick == Iris._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 + + widgets.registerEvent("InputBegan", UpdateComboState) + + widgets.registerEvent("InputChanged", UpdateComboState) + + --stylua: ignore + Iris.WidgetConstructor("Combo", { + hasState = true, + hasChildren = true, + Args = { + ["Text"] = 1, + ["NoButton"] = 2, + ["NoPreview"] = 3, + }, + Events = { + ["opened"] = { + ["Init"] = function(_thisWidget: Types.Combo) end, + ["Get"] = function(thisWidget: Types.Combo) + return thisWidget.lastOpenedTick == Iris._cycleTick + end, + }, + ["closed"] = { + ["Init"] = function(_thisWidget: Types.Combo) end, + ["Get"] = function(thisWidget: Types.Combo) + return thisWidget.lastClosedTick == Iris._cycleTick + end, + }, + ["changed"] = { + ["Init"] = function(_thisWidget: Types.Combo) end, + ["Get"] = function(thisWidget: Types.Combo) + return thisWidget.lastChangedTick == Iris._cycleTick + end, + }, + ["clicked"] = widgets.EVENTS.click(function(thisWidget: Types.Widget) + local Combo = thisWidget.Instance :: Frame + return Combo.PreviewContainer + end), + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Combo) + local frameHeight: number = Iris._config.TextSize + 2 * Iris._config.FramePadding.Y + + local Combo: Frame = Instance.new("Frame") + Combo.Name = "Iris_Combo" + Combo.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local PreviewContainer: TextButton = Instance.new("TextButton") + PreviewContainer.Name = "PreviewContainer" + PreviewContainer.Size = UDim2.new(Iris._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)) + + PreviewContainer.Parent = Combo + + local PreviewLabel: TextLabel = Instance.new("TextLabel") + PreviewLabel.Name = "PreviewLabel" + PreviewLabel.Size = UDim2.new(UDim.new(1, 0), Iris._config.ContentHeight) + PreviewLabel.AutomaticSize = Enum.AutomaticSize.Y + PreviewLabel.BackgroundColor3 = Iris._config.FrameBgColor + PreviewLabel.BackgroundTransparency = Iris._config.FrameBgTransparency + PreviewLabel.BorderSizePixel = 0 + PreviewLabel.ClipsDescendants = true + + widgets.applyTextStyle(PreviewLabel) + widgets.UIPadding(PreviewLabel, Iris._config.FramePadding) + + PreviewLabel.Parent = PreviewContainer + + local DropdownButton: TextLabel = Instance.new("TextLabel") + DropdownButton.Name = "DropdownButton" + DropdownButton.Size = UDim2.new(0, frameHeight, Iris._config.ContentHeight.Scale, math.max(Iris._config.ContentHeight.Offset, frameHeight)) + DropdownButton.BorderSizePixel = 0 + DropdownButton.BackgroundColor3 = Iris._config.ButtonColor + DropdownButton.BackgroundTransparency = Iris._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 = Iris._config.TextColor + Dropdown.ImageTransparency = Iris._config.TextTransparency + + Dropdown.Parent = DropdownButton + + DropdownButton.Parent = PreviewContainer + + -- for some reason ImGui Combo has no highlights for Active, only hovered. + -- so this deviates from ImGui, but its a good UX change + widgets.applyInteractionHighlightsWithMultiHighlightee("Background", PreviewContainer, { + { + PreviewLabel, + { + Color = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._config.FrameBgActiveTransparency, + }, + }, + { + DropdownButton, + { + Color = Iris._config.ButtonColor, + Transparency = Iris._config.ButtonTransparency, + HoveredColor = Iris._config.ButtonHoveredColor, + HoveredTransparency = Iris._config.ButtonHoveredTransparency, + -- Use hovered for active + ActiveColor = Iris._config.ButtonHoveredColor, + ActiveTransparency = Iris._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.BackgroundColor3 = Iris._config.PopupBgColor + ChildContainer.BackgroundTransparency = Iris._config.PopupBgTransparency + ChildContainer.BorderSizePixel = 0 + + ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y + ChildContainer.ScrollBarImageTransparency = Iris._config.ScrollbarGrabTransparency + ChildContainer.ScrollBarImageColor3 = Iris._config.ScrollbarGrabColor + ChildContainer.ScrollBarThickness = Iris._config.ScrollbarSize + ChildContainer.CanvasSize = UDim2.fromScale(0, 0) + ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar + ChildContainer.TopImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.MidImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.BottomImage = widgets.ICONS.BLANK_SQUARE + + -- appear over everything else + ChildContainer.ClipsDescendants = true + + -- Unfortunatley, ScrollingFrame does not work with UICorner + -- if Iris._config.PopupRounding > 0 then + -- widgets.UICorner(ChildContainer, Iris._config.PopupRounding) + -- end + + widgets.UIStroke(ChildContainer, Iris._config.WindowBorderSize, Iris._config.BorderColor, Iris._config.BorderTransparency) + widgets.UIPadding(ChildContainer, Vector2.new(2, Iris._config.WindowPadding.Y)) + widgets.UISizeConstraint(ChildContainer, Vector2.new(100)) + + local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, Iris._config.ItemSpacing.Y)) + ChildContainerUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top + + local RootPopupScreenGui = Iris._rootInstance and Iris._rootInstance:WaitForChild("PopupScreenGui") :: GuiObject + ChildContainer.Parent = RootPopupScreenGui + + thisWidget.ChildContainer = ChildContainer + thisWidget.UIListLayout = ChildContainerUIListLayout + return Combo + end, + Update = function(thisWidget: Types.Combo) + local Iris_Combo = thisWidget.Instance :: Frame + local PreviewContainer = Iris_Combo.PreviewContainer :: TextButton + local PreviewLabel: TextLabel = PreviewContainer.PreviewLabel + local DropdownButton: TextLabel = PreviewContainer.DropdownButton + local TextLabel: TextLabel = Iris_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 = Iris._config.TextSize + 2 * Iris._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(Iris._config.ContentWidth, Iris._config.ContentHeight) + PreviewContainer.AutomaticSize = Enum.AutomaticSize.Y + end + end, + ChildAdded = function(thisWidget: Types.Combo, _thisChild: Types.Widget) + UpdateChildContainerTransform(thisWidget) + return thisWidget.ChildContainer + end, + GenerateState = function(thisWidget: Types.Combo) + if thisWidget.state.index == nil then + thisWidget.state.index = Iris._widgetState(thisWidget, "index", "No Selection") + end + if thisWidget.state.isOpened == nil then + thisWidget.state.isOpened = Iris._widgetState(thisWidget, "isOpened", false) + end + + thisWidget.state.index:onChange(function() + thisWidget.lastChangedTick = Iris._cycleTick + 1 + if thisWidget.state.isOpened.value then + thisWidget.state.isOpened:set(false) + end + end) + end, + UpdateState = function(thisWidget: Types.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 = Iris._cycleTick + thisWidget.lastOpenedTick = Iris._cycleTick + 1 + + -- ImGui also does not do this, and the Arrow is always facing down + Dropdown.Image = widgets.ICONS.RIGHT_POINTING_TRIANGLE + ChildContainer.Visible = true + + UpdateChildContainerTransform(thisWidget) + else + if AnyOpenedCombo then + AnyOpenedCombo = false + OpenedCombo = nil + thisWidget.lastClosedTick = Iris._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: Types.Combo) + -- If we are discarding the current combo active, we need to hide it + if OpenedCombo and OpenedCombo == thisWidget then + OpenedCombo = nil + AnyOpenedCombo = false + end + + thisWidget.Instance:Destroy() + thisWidget.ChildContainer:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Format.luau b/src/DebuggerUI/Shared/External/iris/widgets/Format.luau new file mode 100644 index 0000000..3fceb41 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Format.luau @@ -0,0 +1,155 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + --stylua: ignore + Iris.WidgetConstructor("Separator", { + hasState = false, + hasChildren = false, + Args = {}, + Events = {}, + Generate = function(thisWidget: Types.Separator) + local Separator: Frame = Instance.new("Frame") + Separator.Name = "Iris_Separator" + Separator.BackgroundColor3 = Iris._config.SeparatorColor + Separator.BackgroundTransparency = Iris._config.SeparatorTransparency + Separator.BorderSizePixel = 0 + if thisWidget.parentWidget.type == "SameLine" then + Separator.Size = UDim2.new(0, 1, Iris._config.ItemWidth.Scale, Iris._config.ItemWidth.Offset) + else + Separator.Size = UDim2.new(Iris._config.ItemWidth.Scale, Iris._config.ItemWidth.Offset, 0, 1) + end + Separator.LayoutOrder = thisWidget.ZIndex + + widgets.UIListLayout(Separator, Enum.FillDirection.Vertical, UDim.new(0, 0)) + -- this is to prevent a bug of AutomaticLayout edge case when its parent has automaticLayout enabled + + return Separator + end, + Update = function(_thisWidget: Types.Separator) end, + Discard = function(thisWidget: Types.Separator) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("Indent", { + hasState = false, + hasChildren = true, + Args = { + ["Width"] = 1, + }, + Events = {}, + Generate = function(thisWidget: Types.Indent) + local Indent: Frame = Instance.new("Frame") + Indent.Name = "Iris_Indent" + Indent.BackgroundTransparency = 1 + Indent.BorderSizePixel = 0 + Indent.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + Indent.AutomaticSize = Enum.AutomaticSize.Y + Indent.LayoutOrder = thisWidget.ZIndex + + widgets.UIListLayout(Indent, Enum.FillDirection.Vertical, UDim.new(0, Iris._config.ItemSpacing.Y)) + widgets.UIPadding(Indent, Vector2.zero) + + return Indent + end, + Update = function(thisWidget: Types.Indent) + local Indent = thisWidget.Instance :: Frame + + local indentWidth: number + if thisWidget.arguments.Width then + indentWidth = thisWidget.arguments.Width + else + indentWidth = Iris._config.IndentSpacing + end + Indent.UIPadding.PaddingLeft = UDim.new(0, indentWidth) + end, + Discard = function(thisWidget: Types.Indent) + thisWidget.Instance:Destroy() + end, + ChildAdded = function(thisWidget: Types.Indent, _thisChild: Types.Widget) + return thisWidget.Instance + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("SameLine", { + hasState = false, + hasChildren = true, + Args = { + ["Width"] = 1, + ["VerticalAlignment"] = 2, + ["HorizontalAlignment"] = 3, + }, + Events = {}, + Generate = function(thisWidget: Types.SameLine) + local SameLine: Frame = Instance.new("Frame") + SameLine.Name = "Iris_SameLine" + SameLine.BackgroundTransparency = 1 + SameLine.BorderSizePixel = 0 + SameLine.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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: Types.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 = Iris._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: Types.SameLine) + thisWidget.Instance:Destroy() + end, + ChildAdded = function(thisWidget: Types.SameLine, _thisChild: Types.Widget) + return thisWidget.Instance + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("Group", { + hasState = false, + hasChildren = true, + Args = {}, + Events = {}, + Generate = function(thisWidget: Types.Group) + local Group: Frame = Instance.new("Frame") + Group.Name = "Iris_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, Iris._config.ItemSpacing.Y)) + + return Group + end, + Update = function(_thisWidget: Types.Group) end, + Discard = function(thisWidget: Types.Group) + thisWidget.Instance:Destroy() + end, + ChildAdded = function(thisWidget: Types.Group, _thisChild: Types.Widget) + return thisWidget.Instance + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Image.luau b/src/DebuggerUI/Shared/External/iris/widgets/Image.luau new file mode 100644 index 0000000..6b159cc --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Image.luau @@ -0,0 +1,157 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.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: Types.Image) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass + + --stylua: ignore + Iris.WidgetConstructor("Image", widgets.extend(abstractImage, { + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Image) + local Image: ImageLabel = Instance.new("ImageLabel") + Image.Name = "Iris_Image" + Image.BackgroundTransparency = 1 + Image.BorderSizePixel = 0 + Image.ImageColor3 = Iris._config.ImageColor + Image.ImageTransparency = Iris._config.ImageTransparency + Image.LayoutOrder = thisWidget.ZIndex + + widgets.applyFrameStyle(Image, true) + + return Image + end, + Update = function(thisWidget: Types.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, + } :: Types.WidgetClass) + ) + + --stylua: ignore + Iris.WidgetConstructor("ImageButton", widgets.extend(abstractImage, { + Events = { + ["clicked"] = widgets.EVENTS.click(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["rightClicked"] = widgets.EVENTS.rightClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["doubleClicked"] = widgets.EVENTS.doubleClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["ctrlClicked"] = widgets.EVENTS.ctrlClick(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.ImageButton) + local Button: ImageButton = Instance.new("ImageButton") + Button.Name = "Iris_ImageButton" + Button.AutomaticSize = Enum.AutomaticSize.XY + Button.BackgroundColor3 = Iris._config.FrameBgColor + Button.BackgroundTransparency = Iris._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(Iris._config.ImageBorderSize, Iris._config.ImageBorderSize)) + + local Image: ImageLabel = Instance.new("ImageLabel") + Image.Name = "ImageLabel" + Image.BackgroundTransparency = 1 + Image.BorderSizePixel = 0 + Image.ImageColor3 = Iris._config.ImageColor + Image.ImageTransparency = Iris._config.ImageTransparency + Image.Parent = Button + + widgets.applyInteractionHighlights("Background", Button, Button, { + Color = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._config.FrameBgActiveTransparency, + }) + + return Button + end, + Update = function(thisWidget: Types.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, + } :: Types.WidgetClass) + ) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Input.luau b/src/DebuggerUI/Shared/External/iris/widgets/Input.luau new file mode 100644 index 0000000..88c0864 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Input.luau @@ -0,0 +1,1390 @@ +local Types = require(script.Parent.Parent.Types) + +type InputDataTypes = "Num" | "Vector2" | "Vector3" | "UDim" | "UDim2" | "Color3" | "Color4" | "Rect" | "Enum" | "" | string + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local numberChanged = { + ["Init"] = function(_thisWidget: Types.Widget) end, + ["Get"] = function(thisWidget: Types.Input) + return thisWidget.lastNumberChangedTick == Iris._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: Types.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) -> Types.WidgetClass + do + local function generateButtons(thisWidget: Types.Input, parent: GuiObject, textHeight: number) + 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(Iris._config.TextSize + 2 * Iris._config.FramePadding.Y, Iris._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 :: Types.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 :: Types.Argument)) + end + if thisWidget.arguments.Max ~= nil then + newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, 1, thisWidget.arguments :: Types.Argument)) + end + thisWidget.state.number:set(newValue) + thisWidget.lastNumberChangedTick = Iris._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(Iris._config.TextSize + 2 * Iris._config.FramePadding.Y, Iris._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 :: Types.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 :: Types.Argument)) + end + if thisWidget.arguments.Max ~= nil then + newValue = math.min(newValue, getValueByIndex(thisWidget.arguments.Max, 1, thisWidget.arguments :: Types.Argument)) + end + thisWidget.state.number:set(newValue) + thisWidget.lastNumberChangedTick = Iris._cycleTick + 1 + end) + + return 2 * Iris._config.ItemInnerSpacing.X + 2 * textHeight + 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: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Input) + local Input: Frame = Instance.new("Frame") + Input.Name = "Iris_Input" .. dataType + Input.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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, Iris._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 = Iris._config.TextSize + 2 * Iris._config.FramePadding.Y + + if components == 1 then + rightPadding = generateButtons(thisWidget :: any, Input, 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(Iris._config.ContentWidth.Scale / components, (Iris._config.ContentWidth.Offset - (Iris._config.ItemInnerSpacing.X * (components - 1)) - rightPadding) / components) + local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (Iris._config.ItemInnerSpacing.X * (components - 1)) + rightPadding) + local lastComponentWidth: UDim = Iris._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, Iris._config.ContentHeight) + else + InputField.Size = UDim2.new(componentWidth, Iris._config.ContentHeight) + end + InputField.AutomaticSize = Enum.AutomaticSize.Y + InputField.BackgroundColor3 = Iris._config.FrameBgColor + InputField.BackgroundTransparency = Iris._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 = Iris._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: Types.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 + local rightPadding: number = if thisWidget.arguments.NoButtons then 0 else (2 * Iris._config.ItemInnerSpacing.X) + (2 * (Iris._config.TextSize + 2 * Iris._config.FramePadding.Y)) + local InputField: TextBox = Input.InputField1 + InputField.Size = UDim2.new(UDim.new(Iris._config.ContentWidth.Scale, Iris._config.ContentWidth.Offset - rightPadding), Iris._config.ContentHeight) + 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: Types.Input) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.Input) + if thisWidget.state.number == nil then + thisWidget.state.number = Iris._widgetState(thisWidget, "number", defaultValue) + end + if thisWidget.state.editingText == nil then + thisWidget.state.editingText = Iris._widgetState(thisWidget, "editingText", 0) + end + end, + UpdateState = function(thisWidget: Types.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) -> Types.WidgetClass + local generateColorDragScalar: (dataType: InputDataTypes, ...any) -> Types.WidgetClass + do + local PreviouseMouseXPosition: number = 0 + local AnyActiveDrag: boolean = false + local ActiveDrag: Types.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: Types.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 = Iris._cycleTick + 1 + end + + local function DragMouseDown(thisWidget: Types.Input, dataTypes: InputDataTypes, index: number, x: number, y: number) + local currentTime: number = widgets.getTime() + local isTimeValid: boolean = currentTime - thisWidget.lastClickedTime < Iris._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 < Iris._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 Iris._started then + return + end + updateActiveDrag() + end) + + widgets.registerEvent("InputEnded", function(inputObject: InputObject) + if not Iris._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: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Input) + thisWidget.lastClickedTime = -1 + thisWidget.lastClickedPosition = Vector2.zero + + local Drag: Frame = Instance.new("Frame") + Drag.Name = "Iris_Drag" .. dataType + Drag.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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, Iris._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 = Iris._config.TextSize + 2 * Iris._config.FramePadding.Y + + if dataType == "Color3" or dataType == "Color4" then + rightPadding += Iris._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(Iris._config.ContentWidth.Scale / components, (Iris._config.ContentWidth.Offset - (Iris._config.ItemInnerSpacing.X * (components - 1)) - rightPadding) / components) + local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (Iris._config.ItemInnerSpacing.X * (components - 1)) + rightPadding) + local lastComponentWidth: UDim = Iris._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, Iris._config.ContentHeight) + else + DragField.Size = UDim2.new(componentWidth, Iris._config.ContentHeight) + end + DragField.AutomaticSize = Enum.AutomaticSize.Y + DragField.BackgroundColor3 = Iris._config.FrameBgColor + DragField.BackgroundTransparency = Iris._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 = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._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: Types.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 = Iris._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: Types.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: Types.Input) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.Input) + if thisWidget.state.number == nil then + thisWidget.state.number = Iris._widgetState(thisWidget, "number", defaultValue) + end + if thisWidget.state.editingText == nil then + thisWidget.state.editingText = Iris._widgetState(thisWidget, "editingText", false) + end + end, + UpdateState = function(thisWidget: Types.Input) + local Drag = thisWidget.Instance :: Frame + + local widget = thisWidget :: any + for index = 1, components do + local state: Types.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 = Iris._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: Types.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: Types.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 + thisWidget.state.color.lastChangeTick = Iris._cycleTick + if dataType == "Color4" then + thisWidget.state.transparency.lastChangeTick = Iris._cycleTick + end + Iris._widgets[thisWidget.type].UpdateState(thisWidget) + end + end, + GenerateState = function(thisWidget: Types.InputColor4) + if thisWidget.state.color == nil then + thisWidget.state.color = Iris._widgetState(thisWidget, "color", defaultValues[1]) + end + if dataType == "Color4" then + if thisWidget.state.transparency == nil then + thisWidget.state.transparency = Iris._widgetState(thisWidget, "transparency", defaultValues[2]) + end + end + if thisWidget.state.editingText == nil then + thisWidget.state.editingText = Iris._widgetState(thisWidget, "editingText", false) + end + end, + }) + end + end + + --[[ + Slider + ]] + local generateSliderScalar: (dataType: InputDataTypes, components: number, defaultValue: any) -> Types.WidgetClass + local generateEnumSliderScalar: (enum: Enum, item: EnumItem) -> Types.WidgetClass + do + local AnyActiveSlider: boolean = false + local ActiveSlider: Types.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 = Iris._cycleTick + 1 + end + + local function SliderMouseDown(thisWidget: Types.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 Iris._started then + return + end + updateActiveSlider() + end) + + widgets.registerEvent("InputEnded", function(inputObject: InputObject) + if not Iris._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: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Input) + local Slider: Frame = Instance.new("Frame") + Slider.Name = "Iris_Slider" .. dataType + Slider.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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, Iris._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(Iris._config.ContentWidth.Scale / components, (Iris._config.ContentWidth.Offset - (Iris._config.ItemInnerSpacing.X * (components - 1))) / components) + local totalWidth: UDim = UDim.new(componentWidth.Scale * (components - 1), (componentWidth.Offset * (components - 1)) + (Iris._config.ItemInnerSpacing.X * (components - 1))) + local lastComponentWidth: UDim = Iris._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, Iris._config.ContentHeight) + else + SliderField.Size = UDim2.new(componentWidth, Iris._config.ContentHeight) + end + SliderField.AutomaticSize = Enum.AutomaticSize.Y + SliderField.BackgroundColor3 = Iris._config.FrameBgColor + SliderField.BackgroundTransparency = Iris._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 = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._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 = Iris._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 = Iris._config.SliderGrabColor + GrabBar.Transparency = Iris._config.SliderGrabTransparency + if Iris._config.GrabRounding > 0 then + widgets.UICorner(GrabBar, Iris._config.GrabRounding) + end + + widgets.UISizeConstraint(GrabBar, Vector2.new(Iris._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: Types.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 = #Iris._postCycleCallbacks + 1 + local desiredCycleTick: number = Iris._cycleTick + 1 + Iris._postCycleCallbacks[callbackIndex] = function() + if Iris._cycleTick >= desiredCycleTick then + if thisWidget.lastCycleTick ~= -1 then + thisWidget.state.number.lastChangeTick = Iris._cycleTick + Iris._widgets[`Slider{dataType}`].UpdateState(thisWidget) + end + Iris._postCycleCallbacks[callbackIndex] = nil + end + end + end, + Discard = function(thisWidget: Types.Input) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.Input) + if thisWidget.state.number == nil then + thisWidget.state.number = Iris._widgetState(thisWidget, "number", defaultValue) + end + if thisWidget.state.editingText == nil then + thisWidget.state.editingText = Iris._widgetState(thisWidget, "editingText", false) + end + end, + UpdateState = function(thisWidget: Types.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: Types.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: Types.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: Types.InputEnum) + if thisWidget.state.number == nil then + thisWidget.state.number = Iris._widgetState(thisWidget, "number", item.Value) + end + if thisWidget.state.enumItem == nil then + thisWidget.state.enumItem = Iris._widgetState(thisWidget, "enumItem", item) + end + if thisWidget.state.editingText == nil then + thisWidget.state.editingText = Iris._widgetState(thisWidget, "editingText", false) + end + end, + }) + end + end + + do + local inputNum: Types.WidgetClass = generateInputScalar("Num", 1, 0) + inputNum.Args["NoButtons"] = 6 + Iris.WidgetConstructor("InputNum", inputNum) + end + Iris.WidgetConstructor("InputVector2", generateInputScalar("Vector2", 2, Vector2.zero)) + Iris.WidgetConstructor("InputVector3", generateInputScalar("Vector3", 3, Vector3.zero)) + Iris.WidgetConstructor("InputUDim", generateInputScalar("UDim", 2, UDim.new())) + Iris.WidgetConstructor("InputUDim2", generateInputScalar("UDim2", 4, UDim2.new())) + Iris.WidgetConstructor("InputRect", generateInputScalar("Rect", 4, Rect.new(0, 0, 0, 0))) + + Iris.WidgetConstructor("DragNum", generateDragScalar("Num", 1, 0)) + Iris.WidgetConstructor("DragVector2", generateDragScalar("Vector2", 2, Vector2.zero)) + Iris.WidgetConstructor("DragVector3", generateDragScalar("Vector3", 3, Vector3.zero)) + Iris.WidgetConstructor("DragUDim", generateDragScalar("UDim", 2, UDim.new())) + Iris.WidgetConstructor("DragUDim2", generateDragScalar("UDim2", 4, UDim2.new())) + Iris.WidgetConstructor("DragRect", generateDragScalar("Rect", 4, Rect.new(0, 0, 0, 0))) + + Iris.WidgetConstructor("InputColor3", generateColorDragScalar("Color3", Color3.fromRGB(0, 0, 0))) + Iris.WidgetConstructor("InputColor4", generateColorDragScalar("Color4", Color3.fromRGB(0, 0, 0), 0)) + + Iris.WidgetConstructor("SliderNum", generateSliderScalar("Num", 1, 0)) + Iris.WidgetConstructor("SliderVector2", generateSliderScalar("Vector2", 2, Vector2.zero)) + Iris.WidgetConstructor("SliderVector3", generateSliderScalar("Vector3", 3, Vector3.zero)) + Iris.WidgetConstructor("SliderUDim", generateSliderScalar("UDim", 2, UDim.new())) + Iris.WidgetConstructor("SliderUDim2", generateSliderScalar("UDim2", 4, UDim2.new())) + Iris.WidgetConstructor("SliderRect", generateSliderScalar("Rect", 4, Rect.new(0, 0, 0, 0))) + -- Iris.WidgetConstructor("SliderEnum", generateSliderScalar("Enum", 4, 0)) + + -- stylua: ignore + Iris.WidgetConstructor("InputText", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["TextHint"] = 2, + ["ReadOnly"] = 3, + ["MultiLine"] = 4, + }, + Events = { + ["textChanged"] = { + ["Init"] = function(thisWidget: Types.InputText) + thisWidget.lastTextChangedTick = 0 + end, + ["Get"] = function(thisWidget: Types.InputText) + return thisWidget.lastTextChangedTick == Iris._cycleTick + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.InputText) + local InputText: Frame = Instance.new("Frame") + InputText.Name = "Iris_InputText" + InputText.AutomaticSize = Enum.AutomaticSize.Y + InputText.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + 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, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local InputField: TextBox = Instance.new("TextBox") + InputField.Name = "InputField" + InputField.Size = UDim2.new(Iris._config.ContentWidth, Iris._config.ContentHeight) + InputField.AutomaticSize = Enum.AutomaticSize.Y + InputField.BackgroundColor3 = Iris._config.FrameBgColor + InputField.BackgroundTransparency = Iris._config.FrameBgTransparency + InputField.Text = "" + InputField.TextYAlignment = Enum.TextYAlignment.Top + InputField.PlaceholderColor3 = Iris._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, Iris._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 = Iris._cycleTick + 1 + end) + + local frameHeight: number = Iris._config.TextSize + 2 * Iris._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: Types.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: Types.InputText) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.InputText) + if thisWidget.state.text == nil then + thisWidget.state.text = Iris._widgetState(thisWidget, "text", "") + end + end, + UpdateState = function(thisWidget: Types.InputText) + local InputText = thisWidget.Instance :: Frame + local InputField: TextBox = InputText.InputField + + InputField.Text = thisWidget.state.text.value + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Menu.luau b/src/DebuggerUI/Shared/External/iris/widgets/Menu.luau new file mode 100644 index 0000000..f3e1df2 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Menu.luau @@ -0,0 +1,606 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local AnyMenuOpen: boolean = false + local ActiveMenu: Types.Menu? = nil + local MenuStack: { Types.Menu } = {} + + local function EmptyMenuStack(menuIndex: number?) + for index = #MenuStack, menuIndex and menuIndex + 1 or 1, -1 do + local widget: Types.Menu = MenuStack[index] + widget.state.isOpened:set(false) + + widget.Instance.BackgroundColor3 = Iris._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: Types.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 = Iris._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 Iris._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: Types.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 + Iris.WidgetConstructor("MenuBar", { + hasState = false, + hasChildren = true, + Args = {}, + Events = {}, + Generate = function(thisWidget: Types.MenuBar) + local MenuBar: Frame = Instance.new("Frame") + MenuBar.Name = "Iris_MenuBar" + MenuBar.Size = UDim2.fromScale(1, 0) + MenuBar.AutomaticSize = Enum.AutomaticSize.Y + MenuBar.BackgroundColor3 = Iris._config.MenubarBgColor + MenuBar.BackgroundTransparency = Iris._config.MenubarBgTransparency + MenuBar.BorderSizePixel = 0 + MenuBar.LayoutOrder = thisWidget.ZIndex + MenuBar.ClipsDescendants = true + + widgets.UIPadding(MenuBar, Vector2.new(Iris._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(_thisWidget: Types.Widget) + + end, + ChildAdded = function(thisWidget: Types.MenuBar, _thisChild: Types.Widget) + return thisWidget.Instance + end, + Discard = function(thisWidget: Types.MenuBar) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("Menu", { + hasState = true, + hasChildren = true, + Args = { + ["Text"] = 1, + }, + 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), + ["opened"] = { + ["Init"] = function(_thisWidget: Types.Menu) end, + ["Get"] = function(thisWidget: Types.Menu) + return thisWidget.lastOpenedTick == Iris._cycleTick + end, + }, + ["closed"] = { + ["Init"] = function(_thisWidget: Types.Menu) end, + ["Get"] = function(thisWidget: Types.Menu) + return thisWidget.lastClosedTick == Iris._cycleTick + end, + }, + }, + Generate = function(thisWidget: Types.Menu) + local Menu: TextButton + thisWidget.ButtonColors = { + Color = Iris._config.HeaderColor, + Transparency = 1, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderHoveredColor, + ActiveTransparency = Iris._config.HeaderHoveredTransparency, + } + if thisWidget.parentWidget.type == "Menu" then + -- this Menu is a sub-Menu + Menu = Instance.new("TextButton") + Menu.Name = "Menu" + Menu.BackgroundColor3 = Iris._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, Iris._config.FramePadding) + UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1) + widgets.UIListLayout(Menu, Enum.FillDirection.Horizontal, UDim.new(0, Iris._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 = Iris._config.TextSize + 2 * Iris._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 = Iris._config.TextColor + Icon.ImageTransparency = Iris._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 = Iris._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(Iris._config.ItemSpacing.X, Iris._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 :: Types.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 = Iris._config.PopupBgColor + ChildContainer.BackgroundTransparency = Iris._config.PopupBgTransparency + ChildContainer.BorderSizePixel = 0 + ChildContainer.Size = UDim2.fromOffset(0, 0) + ChildContainer.AutomaticSize = Enum.AutomaticSize.XY + + ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y + ChildContainer.ScrollBarImageTransparency = Iris._config.ScrollbarGrabTransparency + ChildContainer.ScrollBarImageColor3 = Iris._config.ScrollbarGrabColor + ChildContainer.ScrollBarThickness = Iris._config.ScrollbarSize + ChildContainer.CanvasSize = UDim2.fromScale(0, 0) + ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar + ChildContainer.TopImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.MidImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.BottomImage = widgets.ICONS.BLANK_SQUARE + + ChildContainer.ZIndex = 6 + ChildContainer.LayoutOrder = 6 + ChildContainer.ClipsDescendants = true + + -- Unfortunatley, ScrollingFrame does not work with UICorner + -- if Iris._config.PopupRounding > 0 then + -- widgets.UICorner(ChildContainer, Iris._config.PopupRounding) + -- end + + widgets.UIStroke(ChildContainer, Iris._config.WindowBorderSize, Iris._config.BorderColor, Iris._config.BorderTransparency) + widgets.UIPadding(ChildContainer, Vector2.new(2, Iris._config.WindowPadding.Y - Iris._config.ItemSpacing.Y)) + + local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, 1)) + ChildContainerUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top + + local RootPopupScreenGui = Iris._rootInstance and Iris._rootInstance:FindFirstChild("PopupScreenGui") :: GuiObject + ChildContainer.Parent = RootPopupScreenGui + + + thisWidget.ChildContainer = ChildContainer + return Menu + end, + Update = function(thisWidget: Types.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: Types.Menu, _thisChild: Types.Widget) + UpdateChildContainerTransform(thisWidget) + return thisWidget.ChildContainer + end, + ChildDiscarded = function(thisWidget: Types.Menu, _thisChild: Types.Widget) + UpdateChildContainerTransform(thisWidget) + end, + GenerateState = function(thisWidget: Types.Menu) + if thisWidget.state.isOpened == nil then + thisWidget.state.isOpened = Iris._widgetState(thisWidget, "isOpened", false) + end + end, + UpdateState = function(thisWidget: Types.Menu) + local ChildContainer = thisWidget.ChildContainer :: ScrollingFrame + + if thisWidget.state.isOpened.value then + thisWidget.lastOpenedTick = Iris._cycleTick + 1 + thisWidget.ButtonColors.Transparency = Iris._config.HeaderTransparency + ChildContainer.Visible = true + + UpdateChildContainerTransform(thisWidget) + else + thisWidget.lastClosedTick = Iris._cycleTick + 1 + thisWidget.ButtonColors.Transparency = 1 + ChildContainer.Visible = false + end + end, + Discard = function(thisWidget: Types.Menu) + -- properly handle removing a menu if open and deleted + if AnyMenuOpen then + local parentMenu = thisWidget.parentWidget :: Types.Menu + local parentIndex: number? = table.find(MenuStack, parentMenu) + if parentIndex then + EmptyMenuStack(parentIndex) + if #MenuStack ~= 0 then + ActiveMenu = parentMenu + AnyMenuOpen = true + end + end + end + + thisWidget.Instance:Destroy() + thisWidget.ChildContainer:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("MenuItem", { + hasState = false, + hasChildren = false, + Args = { + Text = 1, + KeyCode = 2, + ModifierKey = 3, + }, + 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), + }, + Generate = function(thisWidget: Types.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, Iris._config.FramePadding) + UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1) + widgets.UIListLayout(MenuItem, Enum.FillDirection.Horizontal, UDim.new(0, Iris._config.ItemInnerSpacing.X)) + + widgets.applyInteractionHighlights("Background", MenuItem, MenuItem, { + Color = Iris._config.HeaderColor, + Transparency = 1, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderHoveredColor, + ActiveTransparency = Iris._config.HeaderHoveredTransparency, + }) + + widgets.applyButtonClick(MenuItem, function() + EmptyMenuStack() + end) + + widgets.applyMouseEnter(MenuItem, function() + local parentMenu = thisWidget.parentWidget :: Types.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 = Iris._config.TextDisabledColor + Shortcut.TextTransparency = Iris._config.TextDisabledTransparency + + Shortcut.Parent = MenuItem + + return MenuItem + end, + Update = function(thisWidget: Types.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: Types.MenuItem) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("MenuToggle", { + hasState = true, + hasChildren = false, + Args = { + Text = 1, + KeyCode = 2, + ModifierKey = 3, + }, + Events = { + ["checked"] = { + ["Init"] = function(_thisWidget: Types.MenuToggle) end, + ["Get"] = function(thisWidget: Types.MenuToggle): boolean + return thisWidget.lastCheckedTick == Iris._cycleTick + end, + }, + ["unchecked"] = { + ["Init"] = function(_thisWidget: Types.MenuToggle) end, + ["Get"] = function(thisWidget: Types.MenuToggle): boolean + return thisWidget.lastUncheckedTick == Iris._cycleTick + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.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, Iris._config.FramePadding) + UIPadding.PaddingTop = UIPadding.PaddingTop - UDim.new(0, 1) + widgets.UIListLayout(MenuItem, Enum.FillDirection.Horizontal, UDim.new(0, Iris._config.ItemInnerSpacing.X)).VerticalAlignment = Enum.VerticalAlignment.Center + + widgets.applyInteractionHighlights("Background", MenuItem, MenuItem, { + Color = Iris._config.HeaderColor, + Transparency = 1, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderHoveredColor, + ActiveTransparency = Iris._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 :: Types.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 = Iris._config.TextDisabledColor + Shortcut.TextTransparency = Iris._config.TextDisabledTransparency + + Shortcut.Parent = MenuItem + + local frameSize: number = Iris._config.TextSize + 2 * Iris._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 = Iris._config.TextColor + Icon.ImageTransparency = Iris._config.TextTransparency + Icon.Image = widgets.ICONS.CHECK_MARK + Icon.LayoutOrder = 2 + + Icon.Parent = MenuItem + + return MenuItem + end, + GenerateState = function(thisWidget: Types.MenuToggle) + if thisWidget.state.isChecked == nil then + thisWidget.state.isChecked = Iris._widgetState(thisWidget, "isChecked", false) + end + end, + Update = function(thisWidget: Types.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: Types.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 = Iris._cycleTick + 1 + else + Icon.Image = "" + thisWidget.lastUncheckedTick = Iris._cycleTick + 1 + end + end, + Discard = function(thisWidget: Types.MenuToggle) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Plot.luau b/src/DebuggerUI/Shared/External/iris/widgets/Plot.luau new file mode 100644 index 0000000..a958c0c --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Plot.luau @@ -0,0 +1,648 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + -- stylua: ignore + Iris.WidgetConstructor("ProgressBar", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Format"] = 2, + }, + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + ["changed"] = { + ["Init"] = function(_thisWidget: Types.ProgressBar) end, + ["Get"] = function(thisWidget: Types.ProgressBar) + return thisWidget.lastChangedTick == Iris._cycleTick + end, + }, + }, + Generate = function(thisWidget: Types.ProgressBar) + local ProgressBar: Frame = Instance.new("Frame") + ProgressBar.Name = "Iris_ProgressBar" + ProgressBar.Size = UDim2.new(Iris._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, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local Bar: Frame = Instance.new("Frame") + Bar.Name = "Bar" + Bar.Size = UDim2.new(Iris._config.ContentWidth, Iris._config.ContentHeight) + Bar.BackgroundColor3 = Iris._config.FrameBgColor + Bar.BackgroundTransparency = Iris._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), Iris._config.ContentHeight) + Progress.BackgroundColor3 = Iris._config.PlotHistogramColor + Progress.BackgroundTransparency = Iris._config.PlotHistogramTransparency + Progress.BorderSizePixel = 0 + + widgets.applyTextStyle(Progress) + widgets.UIPadding(Progress, Iris._config.FramePadding) + widgets.UICorner(Progress, Iris._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), Iris._config.ContentHeight) + Value.BackgroundTransparency = 1 + Value.BorderSizePixel = 0 + Value.ZIndex = 1 + + widgets.applyTextStyle(Value) + widgets.UIPadding(Value, Iris._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, Iris._config.FramePadding) + + TextLabel.Parent = ProgressBar + + return ProgressBar + end, + GenerateState = function(thisWidget: Types.ProgressBar) + if thisWidget.state.progress == nil then + thisWidget.state.progress = Iris._widgetState(thisWidget, "Progress", 0) + end + end, + Update = function(thisWidget: Types.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: Types.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 = Iris._cycleTick + 1 + end, + Discard = function(thisWidget: Types.ProgressBar) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) + + local function createLine(parent: Frame, index: number): Frame + local Block: Frame = Instance.new("Frame") + Block.Name = tostring(index) + Block.AnchorPoint = Vector2.new(0.5, 0.5) + Block.BackgroundColor3 = Iris._config.PlotLinesColor + Block.BackgroundTransparency = Iris._config.PlotLinesTransparency + Block.BorderSizePixel = 0 + + Block.Parent = parent + + return Block + end + + local function clearLine(thisWidget: Types.PlotLines) + if thisWidget.HoveredLine then + thisWidget.HoveredLine.BackgroundColor3 = Iris._config.PlotLinesColor + thisWidget.HoveredLine.BackgroundTransparency = Iris._config.PlotLinesTransparency + thisWidget.HoveredLine = false + thisWidget.state.hovered:set(nil) + end + end + + local function updateLine(thisWidget: Types.PlotLines, silent: true?) + local PlotLines = thisWidget.Instance :: Frame + local Background = PlotLines.Background :: Frame + local Plot = Background.Plot :: Frame + + local mousePosition: Vector2 = widgets.getMouseLocation() + + local position: Vector2 = Plot.AbsolutePosition - widgets.GuiOffset + local scale: number = (mousePosition.X - position.X) / Plot.AbsoluteSize.X + local index: number = math.ceil(scale * #thisWidget.Lines) + local line: Frame? = thisWidget.Lines[index] + + if line then + if line ~= thisWidget.HoveredLine and not silent then + clearLine(thisWidget) + end + local start: number? = thisWidget.state.values.value[index] + local stop: number? = thisWidget.state.values.value[index + 1] + if start and stop then + if math.floor(start) == start and math.floor(stop) == stop then + thisWidget.Tooltip.Text = ("%d: %d\n%d: %d"):format(index, start, index + 1, stop) + else + thisWidget.Tooltip.Text = ("%d: %.3f\n%d: %.3f"):format(index, start, index + 1, stop) + end + end + thisWidget.HoveredLine = line + line.BackgroundColor3 = Iris._config.PlotLinesHoveredColor + line.BackgroundTransparency = Iris._config.PlotLinesHoveredTransparency + if silent then + thisWidget.state.hovered.value = { start, stop } + else + thisWidget.state.hovered:set({ start, stop }) + end + end + end + + -- stylua: ignore + Iris.WidgetConstructor("PlotLines", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Height"] = 2, + ["Min"] = 3, + ["Max"] = 4, + ["TextOverlay"] = 5, + }, + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.PlotLines) + local PlotLines: Frame = Instance.new("Frame") + PlotLines.Name = "Iris_PlotLines" + PlotLines.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + PlotLines.BackgroundTransparency = 1 + PlotLines.BorderSizePixel = 0 + PlotLines.ZIndex = thisWidget.ZIndex + PlotLines.LayoutOrder = thisWidget.ZIndex + + local UIListLayout: UIListLayout = widgets.UIListLayout(PlotLines, Enum.FillDirection.Horizontal, UDim.new(0, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local Background: Frame = Instance.new("Frame") + Background.Name = "Background" + Background.Size = UDim2.new(Iris._config.ContentWidth, UDim.new(1, 0)) + Background.BackgroundColor3 = Iris._config.FrameBgColor + Background.BackgroundTransparency = Iris._config.FrameBgTransparency + widgets.applyFrameStyle(Background) + + Background.Parent = PlotLines + + local Plot: Frame = Instance.new("Frame") + Plot.Name = "Plot" + Plot.Size = UDim2.fromScale(1, 1) + Plot.BackgroundTransparency = 1 + Plot.BorderSizePixel = 0 + Plot.ClipsDescendants = true + + Plot:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() + thisWidget.state.values.lastChangeTick = Iris._cycleTick + Iris._widgets.PlotLines.UpdateState(thisWidget) + end) + + local OverlayText: TextLabel = Instance.new("TextLabel") + OverlayText.Name = "OverlayText" + OverlayText.AutomaticSize = Enum.AutomaticSize.XY + OverlayText.AnchorPoint = Vector2.new(0.5, 0) + OverlayText.Size = UDim2.fromOffset(0, 0) + OverlayText.Position = UDim2.fromScale(0.5, 0) + OverlayText.BackgroundTransparency = 1 + OverlayText.BorderSizePixel = 0 + OverlayText.ZIndex = 2 + + widgets.applyTextStyle(OverlayText) + + OverlayText.Parent = Plot + + local Tooltip: TextLabel = Instance.new("TextLabel") + Tooltip.Name = "Iris_Tooltip" + Tooltip.AutomaticSize = Enum.AutomaticSize.XY + Tooltip.Size = UDim2.fromOffset(0, 0) + Tooltip.BackgroundColor3 = Iris._config.PopupBgColor + Tooltip.BackgroundTransparency = Iris._config.PopupBgTransparency + Tooltip.BorderSizePixel = 0 + Tooltip.Visible = false + + widgets.applyTextStyle(Tooltip) + widgets.UIStroke(Tooltip, Iris._config.PopupBorderSize, Iris._config.BorderActiveColor, Iris._config.BorderActiveTransparency) + widgets.UIPadding(Tooltip, Iris._config.WindowPadding) + if Iris._config.PopupRounding > 0 then + widgets.UICorner(Tooltip, Iris._config.PopupRounding) + end + + local popup: Instance? = Iris._rootInstance and Iris._rootInstance:FindFirstChild("PopupScreenGui") + Tooltip.Parent = popup and popup:FindFirstChild("TooltipContainer") + + thisWidget.Tooltip = Tooltip + + widgets.applyMouseMoved(Plot, function() + updateLine(thisWidget) + end) + + widgets.applyMouseLeave(Plot, function() + clearLine(thisWidget) + end) + + Plot.Parent = Background + + thisWidget.Lines = {} + thisWidget.HoveredLine = false + + local TextLabel: TextLabel = Instance.new("TextLabel") + TextLabel.Name = "TextLabel" + TextLabel.AutomaticSize = Enum.AutomaticSize.XY + TextLabel.Size = UDim2.fromOffset(0, 0) + TextLabel.BackgroundTransparency = 1 + TextLabel.BorderSizePixel = 0 + TextLabel.ZIndex = thisWidget.ZIndex + 3 + TextLabel.LayoutOrder = thisWidget.ZIndex + 3 + + widgets.applyTextStyle(TextLabel) + + TextLabel.Parent = PlotLines + + return PlotLines + end, + GenerateState = function(thisWidget: Types.PlotLines) + if thisWidget.state.values == nil then + thisWidget.state.values = Iris._widgetState(thisWidget, "values", { 0, 1 }) + end + if thisWidget.state.hovered == nil then + thisWidget.state.hovered = Iris._widgetState(thisWidget, "hovered", nil) + end + end, + Update = function(thisWidget: Types.PlotLines) + local PlotLines = thisWidget.Instance :: Frame + local TextLabel: TextLabel = PlotLines.TextLabel + local Background = PlotLines.Background :: Frame + local Plot = Background.Plot :: Frame + local OverlayText: TextLabel = Plot.OverlayText + + TextLabel.Text = thisWidget.arguments.Text or "Plot Lines" + OverlayText.Text = thisWidget.arguments.TextOverlay or "" + PlotLines.Size = UDim2.new(1, 0, 0, thisWidget.arguments.Height or 0) + end, + UpdateState = function(thisWidget: Types.PlotLines) + if thisWidget.state.hovered.lastChangeTick == Iris._cycleTick then + if thisWidget.state.hovered.value then + thisWidget.Tooltip.Visible = true + else + thisWidget.Tooltip.Visible = false + end + end + + if thisWidget.state.values.lastChangeTick == Iris._cycleTick then + local PlotLines = thisWidget.Instance :: Frame + local Background = PlotLines.Background :: Frame + local Plot = Background.Plot :: Frame + + local values: { number } = thisWidget.state.values.value + local count: number = #values - 1 + local numLines: number = #thisWidget.Lines + + local min: number = thisWidget.arguments.Min or math.huge + local max: number = thisWidget.arguments.Max or -math.huge + + if min == nil or max == nil then + for _, value: number in values do + min = math.min(min, value) + max = math.max(max, value) + end + end + + -- add or remove blocks depending on how many are needed + if numLines < count then + for index = numLines + 1, count do + table.insert(thisWidget.Lines, createLine(Plot, index)) + end + elseif numLines > count then + for _ = count + 1, numLines do + local line: Frame? = table.remove(thisWidget.Lines) + if line then + line:Destroy() + end + end + end + + local range: number = max - min + local size: Vector2 = Plot.AbsoluteSize + + for index = 1, count do + local start: number = values[index] + local stop: number = values[index + 1] + local a: Vector2 = size * Vector2.new((index - 1) / count, (max - start) / range) + local b: Vector2 = size * Vector2.new(index / count, (max - stop) / range) + local position: Vector2 = (a + b) / 2 + + thisWidget.Lines[index].Size = UDim2.fromOffset((b - a).Magnitude + 1, 1) + thisWidget.Lines[index].Position = UDim2.fromOffset(position.X, position.Y) + thisWidget.Lines[index].Rotation = math.atan2(b.Y - a.Y, b.X - a.X) * (180 / math.pi) + end + + -- only update the hovered block if it exists. + if thisWidget.HoveredLine then + updateLine(thisWidget, true) + end + end + end, + Discard = function(thisWidget: Types.PlotLines) + thisWidget.Instance:Destroy() + thisWidget.Tooltip:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) + + local function createBlock(parent: Frame, index: number): Frame + local Block: Frame = Instance.new("Frame") + Block.Name = tostring(index) + Block.BackgroundColor3 = Iris._config.PlotHistogramColor + Block.BackgroundTransparency = Iris._config.PlotHistogramTransparency + Block.BorderSizePixel = 0 + + Block.Parent = parent + + return Block + end + + local function clearBlock(thisWidget: Types.PlotHistogram) + if thisWidget.HoveredBlock then + thisWidget.HoveredBlock.BackgroundColor3 = Iris._config.PlotHistogramColor + thisWidget.HoveredBlock.BackgroundTransparency = Iris._config.PlotHistogramTransparency + thisWidget.HoveredBlock = false + thisWidget.state.hovered:set(nil) + end + end + + local function updateBlock(thisWidget: Types.PlotHistogram, silent: true?) + local PlotHistogram = thisWidget.Instance :: Frame + local Background = PlotHistogram.Background :: Frame + local Plot = Background.Plot :: Frame + + local mousePosition: Vector2 = widgets.getMouseLocation() + + local position: Vector2 = Plot.AbsolutePosition - widgets.GuiOffset + local scale: number = (mousePosition.X - position.X) / Plot.AbsoluteSize.X + local index: number = math.ceil(scale * #thisWidget.Blocks) + local block: Frame? = thisWidget.Blocks[index] + + if block then + if block ~= thisWidget.HoveredBlock and not silent then + clearBlock(thisWidget) + end + local value: number? = thisWidget.state.values.value[index] + if value then + thisWidget.Tooltip.Text = if math.floor(value) == value then ("%d: %d"):format(index, value) else ("%d: %.3f"):format(index, value) + end + thisWidget.HoveredBlock = block + block.BackgroundColor3 = Iris._config.PlotHistogramHoveredColor + block.BackgroundTransparency = Iris._config.PlotHistogramHoveredTransparency + if silent then + thisWidget.state.hovered.value = value + else + thisWidget.state.hovered:set(value) + end + end + end + + -- stylua: ignore + Iris.WidgetConstructor("PlotHistogram", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Height"] = 2, + ["Min"] = 3, + ["Max"] = 4, + ["TextOverlay"] = 5, + ["BaseLine"] = 6, + }, + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.PlotHistogram) + local PlotHistogram: Frame = Instance.new("Frame") + PlotHistogram.Name = "Iris_PlotHistogram" + PlotHistogram.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + PlotHistogram.BackgroundTransparency = 1 + PlotHistogram.BorderSizePixel = 0 + PlotHistogram.ZIndex = thisWidget.ZIndex + PlotHistogram.LayoutOrder = thisWidget.ZIndex + + local UIListLayout: UIListLayout = widgets.UIListLayout(PlotHistogram, Enum.FillDirection.Horizontal, UDim.new(0, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local Background: Frame = Instance.new("Frame") + Background.Name = "Background" + Background.Size = UDim2.new(Iris._config.ContentWidth, UDim.new(1, 0)) + Background.BackgroundColor3 = Iris._config.FrameBgColor + Background.BackgroundTransparency = Iris._config.FrameBgTransparency + widgets.applyFrameStyle(Background) + + local UIPadding: UIPadding = (Background :: any).UIPadding + UIPadding.PaddingRight = UDim.new(0, Iris._config.FramePadding.X - 1) + + Background.Parent = PlotHistogram + + local Plot: Frame = Instance.new("Frame") + Plot.Name = "Plot" + Plot.Size = UDim2.fromScale(1, 1) + Plot.BackgroundTransparency = 1 + Plot.BorderSizePixel = 0 + Plot.ClipsDescendants = true + + local OverlayText: TextLabel = Instance.new("TextLabel") + OverlayText.Name = "OverlayText" + OverlayText.AutomaticSize = Enum.AutomaticSize.XY + OverlayText.AnchorPoint = Vector2.new(0.5, 0) + OverlayText.Size = UDim2.fromOffset(0, 0) + OverlayText.Position = UDim2.fromScale(0.5, 0) + OverlayText.BackgroundTransparency = 1 + OverlayText.BorderSizePixel = 0 + OverlayText.ZIndex = 2 + + widgets.applyTextStyle(OverlayText) + + OverlayText.Parent = Plot + + local Tooltip: TextLabel = Instance.new("TextLabel") + Tooltip.Name = "Iris_Tooltip" + Tooltip.AutomaticSize = Enum.AutomaticSize.XY + Tooltip.Size = UDim2.fromOffset(0, 0) + Tooltip.BackgroundColor3 = Iris._config.PopupBgColor + Tooltip.BackgroundTransparency = Iris._config.PopupBgTransparency + Tooltip.BorderSizePixel = 0 + Tooltip.Visible = false + + widgets.applyTextStyle(Tooltip) + widgets.UIStroke(Tooltip, Iris._config.PopupBorderSize, Iris._config.BorderActiveColor, Iris._config.BorderActiveTransparency) + widgets.UIPadding(Tooltip, Iris._config.WindowPadding) + if Iris._config.PopupRounding > 0 then + widgets.UICorner(Tooltip, Iris._config.PopupRounding) + end + + local popup: Instance? = Iris._rootInstance and Iris._rootInstance:FindFirstChild("PopupScreenGui") + Tooltip.Parent = popup and popup:FindFirstChild("TooltipContainer") + + thisWidget.Tooltip = Tooltip + + widgets.applyMouseMoved(Plot, function() + updateBlock(thisWidget) + end) + + widgets.applyMouseLeave(Plot, function() + clearBlock(thisWidget) + end) + + Plot.Parent = Background + + thisWidget.Blocks = {} + thisWidget.HoveredBlock = false + + local TextLabel: TextLabel = Instance.new("TextLabel") + TextLabel.Name = "TextLabel" + TextLabel.AutomaticSize = Enum.AutomaticSize.XY + TextLabel.Size = UDim2.fromOffset(0, 0) + TextLabel.BackgroundTransparency = 1 + TextLabel.BorderSizePixel = 0 + TextLabel.ZIndex = thisWidget.ZIndex + 3 + TextLabel.LayoutOrder = thisWidget.ZIndex + 3 + + widgets.applyTextStyle(TextLabel) + + TextLabel.Parent = PlotHistogram + + return PlotHistogram + end, + GenerateState = function(thisWidget: Types.PlotHistogram) + if thisWidget.state.values == nil then + thisWidget.state.values = Iris._widgetState(thisWidget, "values", { 1 }) + end + if thisWidget.state.hovered == nil then + thisWidget.state.hovered = Iris._widgetState(thisWidget, "hovered", nil) + end + end, + Update = function(thisWidget: Types.PlotHistogram) + local PlotLines = thisWidget.Instance :: Frame + local TextLabel: TextLabel = PlotLines.TextLabel + local Background = PlotLines.Background :: Frame + local Plot = Background.Plot :: Frame + local OverlayText: TextLabel = Plot.OverlayText + + TextLabel.Text = thisWidget.arguments.Text or "Plot Histogram" + OverlayText.Text = thisWidget.arguments.TextOverlay or "" + PlotLines.Size = UDim2.new(1, 0, 0, thisWidget.arguments.Height or 0) + end, + UpdateState = function(thisWidget: Types.PlotHistogram) + if thisWidget.state.hovered.lastChangeTick == Iris._cycleTick then + if thisWidget.state.hovered.value then + thisWidget.Tooltip.Visible = true + else + thisWidget.Tooltip.Visible = false + end + end + + if thisWidget.state.values.lastChangeTick == Iris._cycleTick then + local PlotHistogram = thisWidget.Instance :: Frame + local Background = PlotHistogram.Background :: Frame + local Plot = Background.Plot :: Frame + + local values: { number } = thisWidget.state.values.value + local count: number = #values + local numBlocks: number = #thisWidget.Blocks + + local min: number = thisWidget.arguments.Min or math.huge + local max: number = thisWidget.arguments.Max or -math.huge + local baseline: number = thisWidget.arguments.BaseLine or 0 + + if min == nil or max == nil then + for _, value: number in values do + min = math.min(min or value, value) + max = math.max(max or value, value) + end + end + + -- add or remove blocks depending on how many are needed + if numBlocks < count then + for index = numBlocks + 1, count do + table.insert(thisWidget.Blocks, createBlock(Plot, index)) + end + elseif numBlocks > count then + for _ = count + 1, numBlocks do + local block: Frame? = table.remove(thisWidget.Blocks) + if block then + block:Destroy() + end + end + end + + local range: number = max - min + local width: UDim = UDim.new(1 / count, -1) + for index = 1, count do + local num: number = values[index] + if num >= 0 then + thisWidget.Blocks[index].Size = UDim2.new(width, UDim.new((num - baseline) / range)) + thisWidget.Blocks[index].Position = UDim2.fromScale((index - 1) / count, (max - num) / range) + else + thisWidget.Blocks[index].Size = UDim2.new(width, UDim.new((baseline - num) / range)) + thisWidget.Blocks[index].Position = UDim2.fromScale((index - 1) / count, (max - baseline) / range) + end + end + + -- only update the hovered block if it exists. + if thisWidget.HoveredBlock then + updateBlock(thisWidget, true) + end + end + end, + Discard = function(thisWidget: Types.PlotHistogram) + thisWidget.Instance:Destroy() + thisWidget.Tooltip:Destroy() + widgets.discardState(thisWidget) + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau b/src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau new file mode 100644 index 0000000..fbb7c36 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau @@ -0,0 +1,129 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + --stylua: ignore + Iris.WidgetConstructor("RadioButton", { + hasState = true, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Index"] = 2, + }, + Events = { + ["selected"] = { + ["Init"] = function(_thisWidget: Types.RadioButton) end, + ["Get"] = function(thisWidget: Types.RadioButton) + return thisWidget.lastSelectedTick == Iris._cycleTick + end, + }, + ["unselected"] = { + ["Init"] = function(_thisWidget: Types.RadioButton) end, + ["Get"] = function(thisWidget: Types.RadioButton) + return thisWidget.lastUnselectedTick == Iris._cycleTick + end, + }, + ["active"] = { + ["Init"] = function(_thisWidget: Types.RadioButton) end, + ["Get"] = function(thisWidget: Types.RadioButton) + return thisWidget.state.index.value == thisWidget.arguments.Index + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.RadioButton) + local RadioButton: TextButton = Instance.new("TextButton") + RadioButton.Name = "Iris_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, Iris._config.ItemInnerSpacing.X)) + UIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + local buttonSize: number = Iris._config.TextSize + 2 * (Iris._config.FramePadding.Y - 1) + local Button: Frame = Instance.new("Frame") + Button.Name = "Button" + Button.Size = UDim2.fromOffset(buttonSize, buttonSize) + Button.Parent = RadioButton + Button.BackgroundColor3 = Iris._config.FrameBgColor + Button.BackgroundTransparency = Iris._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 = Iris._config.CheckMarkColor + Circle.BackgroundTransparency = Iris._config.CheckMarkTransparency + widgets.UICorner(Circle) + + widgets.applyInteractionHighlights("Background", RadioButton, Button, { + Color = Iris._config.FrameBgColor, + Transparency = Iris._config.FrameBgTransparency, + HoveredColor = Iris._config.FrameBgHoveredColor, + HoveredTransparency = Iris._config.FrameBgHoveredTransparency, + ActiveColor = Iris._config.FrameBgActiveColor, + ActiveTransparency = Iris._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: Types.RadioButton) + local RadioButton = thisWidget.Instance :: TextButton + local TextLabel: TextLabel = RadioButton.TextLabel + + TextLabel.Text = thisWidget.arguments.Text or "Radio Button" + if thisWidget.state then + thisWidget.state.index.lastChangeTick = Iris._cycleTick + Iris._widgets[thisWidget.type].UpdateState(thisWidget) + end + end, + Discard = function(thisWidget: Types.RadioButton) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + GenerateState = function(thisWidget: Types.RadioButton) + if thisWidget.state.index == nil then + thisWidget.state.index = Iris._widgetState(thisWidget, "index", thisWidget.arguments.Index) + end + end, + UpdateState = function(thisWidget: Types.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 = Iris._config.CheckMarkTransparency + thisWidget.lastSelectedTick = Iris._cycleTick + 1 + else + Circle.BackgroundTransparency = 1 + thisWidget.lastUnselectedTick = Iris._cycleTick + 1 + end + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Root.luau b/src/DebuggerUI/Shared/External/iris/widgets/Root.luau new file mode 100644 index 0000000..941f420 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Root.luau @@ -0,0 +1,141 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local NumNonWindowChildren: number = 0 + + --stylua: ignore + Iris.WidgetConstructor("Root", { + hasState = false, + hasChildren = true, + Args = {}, + Events = {}, + Generate = function(_thisWidget: Types.Root) + local Root: Folder = Instance.new("Folder") + Root.Name = "Iris_Root" + + local PseudoWindowScreenGui + if Iris._config.UseScreenGUIs then + PseudoWindowScreenGui = Instance.new("ScreenGui") + PseudoWindowScreenGui.ResetOnSpawn = false + PseudoWindowScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + PseudoWindowScreenGui.DisplayOrder = Iris._config.DisplayOrderOffset + PseudoWindowScreenGui.IgnoreGuiInset = Iris._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 = Iris._config.DisplayOrderOffset + end + PseudoWindowScreenGui.Name = "PseudoWindowScreenGui" + PseudoWindowScreenGui.Parent = Root + + local PopupScreenGui + if Iris._config.UseScreenGUIs then + PopupScreenGui = Instance.new("ScreenGui") + PopupScreenGui.ResetOnSpawn = false + PopupScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling + PopupScreenGui.DisplayOrder = Iris._config.DisplayOrderOffset + 1024 -- room for 1024 regular windows before overlap + PopupScreenGui.IgnoreGuiInset = Iris._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 = Iris._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, Iris._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 = Iris._config.WindowBgTransparency + PseudoWindow.BackgroundColor3 = Iris._config.WindowBgColor + PseudoWindow.BorderSizePixel = Iris._config.WindowBorderSize + PseudoWindow.BorderColor3 = Iris._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, Iris._config.WindowPadding) + widgets.UIListLayout(PseudoWindow, Enum.FillDirection.Vertical, UDim.new(0, Iris._config.ItemSpacing.Y)) + + PseudoWindow.Parent = PseudoWindowScreenGui + + return Root + end, + Update = function(thisWidget: Types.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: Types.Root) + NumNonWindowChildren = 0 + thisWidget.Instance:Destroy() + end, + ChildAdded = function(thisWidget: Types.Root, thisChild: Types.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: Types.Root, thisChild: Types.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, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Tab.luau b/src/DebuggerUI/Shared/External/iris/widgets/Tab.luau new file mode 100644 index 0000000..06ae9e7 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Tab.luau @@ -0,0 +1,334 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: 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) + if TabBar.state.index.value ~= Index then + return + end + + -- 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 + Iris.WidgetConstructor("TabBar", { + hasState = true, + hasChildren = true, + Args = {}, + Events = {}, + Generate = function(thisWidget: Types.TabBar) + local TabBar: Frame = Instance.new("Frame") + TabBar.Name = "Iris_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, Iris._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 = Iris._config.TabActiveColor + Underline.BackgroundTransparency = Iris._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 Iris.Tab can be parented to Iris.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 = Iris._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 + Iris.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 == Iris._cycleTick + end, + }, + ["unselected"] = { + ["Init"] = function(_thisWidget: Types.Tab) end, + ["Get"] = function(thisWidget: Types.Tab) + return thisWidget.lastUnselectedTick == Iris._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 == Iris._cycleTick + end, + }, + ["closed"] = { + ["Init"] = function(_thisWidget: Types.Tab) end, + ["Get"] = function(thisWidget: Types.Tab) + return thisWidget.lastClosedTick == Iris._cycleTick + end, + }, + }, + Generate = function(thisWidget: Types.Tab) + local Tab = Instance.new("TextButton") + Tab.Name = "Iris_Tab" + Tab.AutomaticSize = Enum.AutomaticSize.XY + Tab.BackgroundColor3 = Iris._config.TabColor + Tab.BackgroundTransparency = Iris._config.TabTransparency + Tab.BorderSizePixel = 0 + Tab.Text = "" + Tab.AutoButtonColor = false + + thisWidget.ButtonColors = { + Color = Iris._config.TabColor, + Transparency = Iris._config.TabTransparency, + HoveredColor = Iris._config.TabHoveredColor, + HoveredTransparency = Iris._config.TabHoveredTransparency, + ActiveColor = Iris._config.TabActiveColor, + ActiveTransparency = Iris._config.TabActiveTransparency, + } + + widgets.UIPadding(Tab, Vector2.new(Iris._config.FramePadding.X, 0)) + widgets.applyFrameStyle(Tab, true, true) + widgets.UIListLayout(Tab, Enum.FillDirection.Horizontal, UDim.new(0, Iris._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, Iris._config.FramePadding.Y)) + + TextLabel.Parent = Tab + + local ButtonSize: number = Iris._config.TextSize + ((Iris._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 = Iris._config.TabColor, + Transparency = 1, + HoveredColor = Iris._config.ButtonHoveredColor, + HoveredTransparency = Iris._config.ButtonHoveredTransparency, + ActiveColor = Iris._config.ButtonActiveColor, + ActiveTransparency = Iris._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 = Iris._config.TextColor, + Transparency = 1, + HoveredColor = Iris._config.TextColor, + HoveredTransparency = Iris._config.TextTransparency, + ActiveColor = Iris._config.TextColor, + ActiveTransparency = Iris._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, Iris._config.ItemSpacing.Y)) + widgets.UIPadding(ChildContainer, Vector2.new(0, Iris._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 = Iris._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.lastChangeTick == Iris._cycleTick then + if thisWidget.state.isOpened.value == true then + thisWidget.lastOpenedTick = Iris._cycleTick + 1 + openTab(thisWidget.parentWidget, thisWidget.Index) + Tab.Visible = true + else + thisWidget.lastClosedTick = Iris._cycleTick + 1 + closeTab(thisWidget.parentWidget, thisWidget.Index) + Tab.Visible = false + end + end + + if thisWidget.state.index.lastChangeTick == Iris._cycleTick then + if thisWidget.state.index.value == thisWidget.Index then + thisWidget.ButtonColors.Color = Iris._config.TabActiveColor + thisWidget.ButtonColors.Transparency = Iris._config.TabActiveTransparency + Tab.BackgroundColor3 = Iris._config.TabActiveColor + Tab.BackgroundTransparency = Iris._config.TabActiveTransparency + Container.Visible = true + thisWidget.lastSelectedTick = Iris._cycleTick + 1 + else + thisWidget.ButtonColors.Color = Iris._config.TabColor + thisWidget.ButtonColors.Transparency = Iris._config.TabTransparency + Tab.BackgroundColor3 = Iris._config.TabColor + Tab.BackgroundTransparency = Iris._config.TabTransparency + Container.Visible = false + thisWidget.lastUnselectedTick = Iris._cycleTick + 1 + end + end + end, + Discard = function(thisWidget: Types.Tab) + if thisWidget.state.isOpened.value == true then + closeTab(thisWidget.parentWidget, thisWidget.Index) + end + + thisWidget.Instance:Destroy() + thisWidget.ChildContainer:Destroy() + widgets.discardState(thisWidget) + end + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Table.luau b/src/DebuggerUI/Shared/External/iris/widgets/Table.luau new file mode 100644 index 0000000..41fc38a --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Table.luau @@ -0,0 +1,634 @@ +local Types = require(script.Parent.Parent.Types) + +-- Tables need an overhaul. + +--[[ + Iris.Table( + { + NumColumns, + Header, + RowBackground, + OuterBorders, + InnerBorders + } + ) + + Config = { + CellPadding: Vector2, + CellSize: UDim2, + } + + Iris.NextColumn() + Iris.NextRow() + Iris.SetColumnIndex(index: number) + Iris.SetRowIndex(index: number) + + Iris.NextHeaderColumn() + Iris.SetHeaderColumnIndex(index: number) + + Iris.SetColumnWidth(index: number, width: number | UDim) +]] + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local Tables: { [Types.ID]: Types.Table } = {} + local TableMinWidths: { [Types.Table]: { boolean } } = {} + local AnyActiveTable = false + local ActiveTable: Types.Table? = nil + local ActiveColumn = 0 + local ActiveLeftWidth = -1 + local ActiveRightWidth = -1 + local MousePositionX = 0 + + local function CalculateMinColumnWidth(thisWidget: Types.Table, index: number) + local width = 0 + for _, row in thisWidget._cellInstances do + local cell = row[index] + for _, child in cell:GetChildren() do + if child:IsA("GuiObject") then + width = math.max(width, child.AbsoluteSize.X) + end + end + end + + thisWidget._minWidths[index] = width + 2 * Iris._config.CellPadding.X + end + + table.insert(Iris._postCycleCallbacks, function() + for _, thisWidget in Tables do + for rowIndex, cycleTick in thisWidget._rowCycles do + if cycleTick < Iris._cycleTick - 1 then + local Row = thisWidget._rowInstances[rowIndex] + local RowBorder = thisWidget._rowBorders[rowIndex - 1] + if Row ~= nil then + Row:Destroy() + end + if RowBorder ~= nil then + RowBorder:Destroy() + end + thisWidget._rowInstances[rowIndex] = nil + thisWidget._rowBorders[rowIndex - 1] = nil + thisWidget._cellInstances[rowIndex] = nil + thisWidget._rowCycles[rowIndex] = nil + end + end + + thisWidget._rowIndex = 1 + thisWidget._columnIndex = 1 + + -- update the border container size to be the same, albeit *every* frame! + local Table = thisWidget.Instance :: Frame + local BorderContainer: Frame = Table.BorderContainer + BorderContainer.Size = UDim2.new(1, 0, 0, thisWidget._rowContainer.AbsoluteSize.Y) + thisWidget._columnBorders[0].Size = UDim2.new(0, 5, 0, thisWidget._rowContainer.AbsoluteSize.Y) + end + + for thisWidget, columns in TableMinWidths do + local refresh = false + for column, _ in columns do + CalculateMinColumnWidth(thisWidget, column) + refresh = true + end + if refresh then + table.clear(columns) + Iris._widgets["Table"].UpdateState(thisWidget) + end + end + end) + + local function UpdateActiveColumn() + if AnyActiveTable == false or ActiveTable == nil then + return + end + + local widths = ActiveTable.state.widths + local NumColumns = ActiveTable.arguments.NumColumns + local Table = ActiveTable.Instance :: Frame + local BorderContainer = Table.BorderContainer :: Frame + local Fixed = ActiveTable.arguments.FixedWidth + local Padding = 2 * Iris._config.CellPadding.X + + if ActiveLeftWidth == -1 then + ActiveLeftWidth = widths.value[ActiveColumn] + if ActiveLeftWidth == 0 then + ActiveLeftWidth = Padding / Table.AbsoluteSize.X + end + ActiveRightWidth = widths.value[ActiveColumn + 1] or -1 + if ActiveRightWidth == 0 then + ActiveRightWidth = Padding / Table.AbsoluteSize.X + end + end + + local BorderX = Table.AbsolutePosition.X + local LeftX: number -- the start of the current column + -- local CurrentX: number = BorderContainer:FindFirstChild(`Border_{ActiveColumn}`).AbsolutePosition.X + 3 - BorderX -- the current column position + local RightX: number -- the end of the next column + if ActiveColumn == 1 then + LeftX = 0 + else + LeftX = math.floor(BorderContainer:FindFirstChild(`Border_{ActiveColumn - 1}`).AbsolutePosition.X + 3 - BorderX) + end + if ActiveColumn >= NumColumns - 1 then + RightX = Table.AbsoluteSize.X + else + RightX = math.floor(BorderContainer:FindFirstChild(`Border_{ActiveColumn + 1}`).AbsolutePosition.X + 3 - BorderX) + end + + local TableX: number = BorderX - widgets.GuiOffset.X + local DeltaX: number = math.clamp(widgets.getMouseLocation().X, LeftX + TableX + Padding, RightX + TableX - Padding) - MousePositionX + local LeftOffset = (MousePositionX - TableX) - LeftX + local LeftRatio = ActiveLeftWidth / LeftOffset + + if Fixed then + widths.value[ActiveColumn] = math.clamp(math.round(ActiveLeftWidth + DeltaX), Padding, Table.AbsoluteSize.X - LeftX) + else + local Change = LeftRatio * DeltaX + widths.value[ActiveColumn] = math.clamp(ActiveLeftWidth + Change, 0, (RightX - LeftX - Padding) / Table.AbsoluteSize.X) + if ActiveColumn < NumColumns then + widths.value[ActiveColumn + 1] = math.clamp(ActiveRightWidth - Change, 0, 1) + end + end + + widths:set(widths.value, true) + end + + local function ColumnMouseDown(thisWidget: Types.Table, index: number) + AnyActiveTable = true + ActiveTable = thisWidget + ActiveColumn = index + ActiveLeftWidth = -1 + ActiveRightWidth = -1 + MousePositionX = widgets.getMouseLocation().X + end + + widgets.registerEvent("InputChanged", function() + if not Iris._started then + return + end + UpdateActiveColumn() + end) + + widgets.registerEvent("InputEnded", function(inputObject: InputObject) + if not Iris._started then + return + end + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and AnyActiveTable then + AnyActiveTable = false + ActiveTable = nil + ActiveColumn = 0 + ActiveLeftWidth = -1 + ActiveRightWidth = -1 + MousePositionX = 0 + end + end) + + local function GenerateCell(_thisWidget: Types.Table, index: number, width: UDim, header: boolean) + local Cell: TextButton + if header then + Cell = Instance.new("TextButton") + Cell.Text = "" + Cell.AutoButtonColor = false + else + Cell = (Instance.new("Frame") :: GuiObject) :: TextButton + end + Cell.Name = `Cell_{index}` + Cell.AutomaticSize = Enum.AutomaticSize.Y + Cell.Size = UDim2.new(width, UDim.new()) + Cell.BackgroundTransparency = 1 + Cell.ZIndex = index + Cell.LayoutOrder = index + Cell.ClipsDescendants = true + + if header then + widgets.applyInteractionHighlights("Background", Cell, Cell, { + Color = Iris._config.HeaderColor, + Transparency = 1, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderActiveColor, + ActiveTransparency = Iris._config.HeaderActiveTransparency, + }) + end + + widgets.UIPadding(Cell, Iris._config.CellPadding) + widgets.UIListLayout(Cell, Enum.FillDirection.Vertical, UDim.new()) + widgets.UISizeConstraint(Cell, Vector2.new(2 * Iris._config.CellPadding.X, 0)) + + return Cell + end + + local function GenerateColumnBorder(thisWidget: Types.Table, index: number, style: "Light" | "Strong") + local Border = Instance.new("ImageButton") + Border.Name = `Border_{index}` + Border.Size = UDim2.new(0, 5, 1, 0) + Border.BackgroundTransparency = 1 + Border.AutoButtonColor = false + Border.Image = "" + Border.ImageTransparency = 1 + Border.ZIndex = index + Border.LayoutOrder = 2 * index + + local offset = if index == thisWidget.arguments.NumColumns then 3 else 2 + + local Line = Instance.new("Frame") + Line.Name = "Line" + Line.Size = UDim2.new(0, 1, 1, 0) + Line.Position = UDim2.fromOffset(offset, 0) + Line.BackgroundColor3 = Iris._config[`TableBorder{style}Color`] + Line.BackgroundTransparency = Iris._config[`TableBorder{style}Transparency`] + Line.BorderSizePixel = 0 + + Line.Parent = Border + + local Hover = Instance.new("Frame") + Hover.Name = "Hover" + Hover.Size = UDim2.new(0, 1, 1, 0) + Hover.Position = UDim2.fromOffset(offset, 0) + Hover.BackgroundColor3 = Iris._config[`TableBorder{style}Color`] + Hover.BackgroundTransparency = Iris._config[`TableBorder{style}Transparency`] + Hover.BorderSizePixel = 0 + + Hover.Visible = thisWidget.arguments.Resizable + + Hover.Parent = Border + + widgets.applyInteractionHighlights("Background", Border, Hover, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyButtonDown(Border, function() + if thisWidget.arguments.Resizable then + ColumnMouseDown(thisWidget, index) + end + end) + + return Border + end + + -- creates a new row and all columns, and adds all to the table's row and cell instance tables, but does not parent + local function GenerateRow(thisWidget: Types.Table, index: number) + local Row: Frame = Instance.new("Frame") + Row.Name = `Row_{index}` + Row.AutomaticSize = Enum.AutomaticSize.Y + Row.Size = UDim2.fromScale(1, 0) + if index == 0 then + Row.BackgroundColor3 = Iris._config.TableHeaderColor + Row.BackgroundTransparency = Iris._config.TableHeaderTransparency + elseif thisWidget.arguments.RowBackground == true then + if (index % 2) == 0 then + Row.BackgroundColor3 = Iris._config.TableRowBgAltColor + Row.BackgroundTransparency = Iris._config.TableRowBgAltTransparency + else + Row.BackgroundColor3 = Iris._config.TableRowBgColor + Row.BackgroundTransparency = Iris._config.TableRowBgTransparency + end + else + Row.BackgroundTransparency = 1 + end + Row.BorderSizePixel = 0 + Row.ZIndex = 2 * index - 1 + Row.LayoutOrder = 2 * index - 1 + Row.ClipsDescendants = true + + widgets.UIListLayout(Row, Enum.FillDirection.Horizontal, UDim.new()) + + thisWidget._cellInstances[index] = table.create(thisWidget.arguments.NumColumns) + for columnIndex = 1, thisWidget.arguments.NumColumns do + local Cell = GenerateCell(thisWidget, columnIndex, thisWidget._widths[columnIndex], index == 0) + Cell.Parent = Row + thisWidget._cellInstances[index][columnIndex] = Cell + end + + thisWidget._rowInstances[index] = Row + + return Row + end + + local function GenerateRowBorder(_thisWidget: Types.Table, index: number, style: "Light" | "Strong") + local Border = Instance.new("Frame") + Border.Name = `Border_{index}` + Border.Size = UDim2.new(1, 0, 0, 0) + Border.BackgroundTransparency = 1 + Border.ZIndex = 2 * index + Border.LayoutOrder = 2 * index + + local Line = Instance.new("Frame") + Line.Name = "Line" + Line.AnchorPoint = Vector2.new(0, 0.5) + Line.Size = UDim2.new(1, 0, 0, 1) + Line.BackgroundColor3 = Iris._config[`TableBorder{style}Color`] + Line.BackgroundTransparency = Iris._config[`TableBorder{style}Transparency`] + Line.BorderSizePixel = 0 + + Line.Parent = Border + + return Border + end + + --stylua: ignore + Iris.WidgetConstructor("Table", { + hasState = true, + hasChildren = true, + Args = { + NumColumns = 1, + Header = 2, + RowBackground = 3, + OuterBorders = 4, + InnerBorders = 5, + Resizable = 6, + FixedWidth = 7, + ProportionalWidth = 8, + LimitTableWidth = 9, + }, + Events = {}, + Generate = function(thisWidget: Types.Table) + Tables[thisWidget.ID] = thisWidget + TableMinWidths[thisWidget] = {} + + local Table = Instance.new("Frame") + Table.Name = "Iris_Table" + Table.AutomaticSize = Enum.AutomaticSize.Y + Table.Size = UDim2.fromScale(1, 0) + Table.BackgroundTransparency = 1 + Table.ZIndex = thisWidget.ZIndex + Table.LayoutOrder = thisWidget.ZIndex + + local RowContainer = Instance.new("Frame") + RowContainer.Name = "RowContainer" + RowContainer.AutomaticSize = Enum.AutomaticSize.Y + RowContainer.Size = UDim2.fromScale(1, 0) + RowContainer.BackgroundTransparency = 1 + RowContainer.ZIndex = 1 + + widgets.UISizeConstraint(RowContainer) + widgets.UIListLayout(RowContainer, Enum.FillDirection.Vertical, UDim.new()) + + RowContainer.Parent = Table + thisWidget._rowContainer = RowContainer + + local BorderContainer = Instance.new("Frame") + BorderContainer.Name = "BorderContainer" + BorderContainer.Size = UDim2.fromScale(1, 1) + BorderContainer.BackgroundTransparency = 1 + BorderContainer.ZIndex = 2 + BorderContainer.ClipsDescendants = true + + widgets.UISizeConstraint(BorderContainer) + widgets.UIListLayout(BorderContainer, Enum.FillDirection.Horizontal, UDim.new()) + widgets.UIStroke(BorderContainer, 1, Iris._config.TableBorderStrongColor, Iris._config.TableBorderStrongTransparency) + + BorderContainer.Parent = Table + + thisWidget._columnIndex = 1 + thisWidget._rowIndex = 1 + thisWidget._rowInstances = {} + thisWidget._cellInstances = {} + thisWidget._rowBorders = {} + thisWidget._columnBorders = {} + thisWidget._rowCycles = {} + + local callbackIndex = #Iris._postCycleCallbacks + 1 + local desiredCycleTick = Iris._cycleTick + 1 + Iris._postCycleCallbacks[callbackIndex] = function() + if Iris._cycleTick >= desiredCycleTick then + if thisWidget.lastCycleTick ~= -1 then + thisWidget.state.widths.lastChangeTick = Iris._cycleTick + Iris._widgets["Table"].UpdateState(thisWidget) + end + Iris._postCycleCallbacks[callbackIndex] = nil + end + end + + return Table + end, + GenerateState = function(thisWidget: Types.Table) + local NumColumns = thisWidget.arguments.NumColumns + if thisWidget.state.widths == nil then + local Widths: { number } = table.create(NumColumns, 1 / NumColumns) + thisWidget.state.widths = Iris._widgetState(thisWidget, "widths", Widths) + end + thisWidget._widths = table.create(NumColumns, UDim.new()) + thisWidget._minWidths = table.create(NumColumns, 0) + + local Table = thisWidget.Instance :: Frame + local BorderContainer: Frame = Table.BorderContainer + + thisWidget._cellInstances[-1] = table.create(NumColumns) + for index = 1, NumColumns do + local Border = GenerateColumnBorder(thisWidget, index, "Light") + Border.Visible = thisWidget.arguments.InnerBorders + thisWidget._columnBorders[index] = Border + Border.Parent = BorderContainer + + local Cell = GenerateCell(thisWidget, index, thisWidget._widths[index], false) + local UISizeConstraint = Cell:FindFirstChild("UISizeConstraint") :: UISizeConstraint + UISizeConstraint.MinSize = Vector2.new( + 2 * Iris._config.CellPadding.X + (if index > 1 then -2 else 0) + (if index < NumColumns then -3 else 0), + 0 + ) + Cell.LayoutOrder = 2 * index - 1 + thisWidget._cellInstances[-1][index] = Cell + Cell.Parent = BorderContainer + end + + local TableColumnBorder = GenerateColumnBorder(thisWidget, NumColumns, "Strong") + thisWidget._columnBorders[0] = TableColumnBorder + TableColumnBorder.Parent = Table + end, + Update = function(thisWidget: Types.Table) + local NumColumns = thisWidget.arguments.NumColumns + assert(NumColumns >= 1, "Iris.Table must have at least one column.") + + if thisWidget._widths ~= nil and #thisWidget._widths ~= NumColumns then + -- disallow changing the number of columns. It's too much effort + thisWidget.arguments.NumColumns = #thisWidget._widths + warn("NumColumns cannot change once set. See documentation.") + end + + for rowIndex, row in thisWidget._rowInstances do + if rowIndex == 0 then + row.BackgroundColor3 = Iris._config.TableHeaderColor + row.BackgroundTransparency = Iris._config.TableHeaderTransparency + elseif thisWidget.arguments.RowBackground == true then + if (rowIndex % 2) == 0 then + row.BackgroundColor3 = Iris._config.TableRowBgAltColor + row.BackgroundTransparency = Iris._config.TableRowBgAltTransparency + else + row.BackgroundColor3 = Iris._config.TableRowBgColor + row.BackgroundTransparency = Iris._config.TableRowBgTransparency + end + else + row.BackgroundTransparency = 1 + end + end + + for _, Border: Frame in thisWidget._rowBorders do + Border.Visible = thisWidget.arguments.InnerBorders + end + + for _, Border: GuiButton in thisWidget._columnBorders do + Border.Visible = thisWidget.arguments.InnerBorders or thisWidget.arguments.Resizable + end + + for _, border in thisWidget._columnBorders do + local hover = border:FindFirstChild("Hover") :: Frame? + if hover then + hover.Visible = thisWidget.arguments.Resizable + end + end + + if thisWidget._columnBorders[NumColumns] ~= nil then + thisWidget._columnBorders[NumColumns].Visible = + not thisWidget.arguments.LimitTableWidth and (thisWidget.arguments.Resizable or thisWidget.arguments.InnerBorders) + thisWidget._columnBorders[0].Visible = + thisWidget.arguments.LimitTableWidth and (thisWidget.arguments.Resizable or thisWidget.arguments.OuterBorders) + end + + -- the header border visibility must be updated after settings all borders + -- visiblity or not + local HeaderRow: Frame? = thisWidget._rowInstances[0] + local HeaderBorder: Frame? = thisWidget._rowBorders[0] + if HeaderRow ~= nil then + HeaderRow.Visible = thisWidget.arguments.Header + end + if HeaderBorder ~= nil then + HeaderBorder.Visible = thisWidget.arguments.Header and thisWidget.arguments.InnerBorders + end + + local Table = thisWidget.Instance :: Frame + local BorderContainer = Table.BorderContainer :: Frame + BorderContainer.UIStroke.Enabled = thisWidget.arguments.OuterBorders + + for index = 1, thisWidget.arguments.NumColumns do + TableMinWidths[thisWidget][index] = true + end + + if thisWidget._widths ~= nil then + Iris._widgets["Table"].UpdateState(thisWidget) + end + end, + UpdateState = function(thisWidget: Types.Table) + local Table = thisWidget.Instance :: Frame + local BorderContainer = Table.BorderContainer :: Frame + local RowContainer = Table.RowContainer :: Frame + local NumColumns = thisWidget.arguments.NumColumns + local ColumnWidths = thisWidget.state.widths.value + local MinWidths = thisWidget._minWidths + + local Fixed = thisWidget.arguments.FixedWidth + local Proportional = thisWidget.arguments.ProportionalWidth + + if not thisWidget.arguments.Resizable then + if Fixed then + if Proportional then + for index = 1, NumColumns do + ColumnWidths[index] = MinWidths[index] + end + else + local maxWidth = 0 + for _, width in MinWidths do + maxWidth = math.max(maxWidth, width) + end + for index = 1, NumColumns do + ColumnWidths[index] = maxWidth + end + end + else + if Proportional then + local TotalWidth = 0 + for _, width in MinWidths do + TotalWidth += width + end + local Ratio = 1 / TotalWidth + for index = 1, NumColumns do + ColumnWidths[index] = Ratio * MinWidths[index] + end + else + local width = 1 / NumColumns + for index = 1, NumColumns do + ColumnWidths[index] = width + end + end + end + end + + local Position = UDim.new() + for index = 1, NumColumns do + local ColumnWidth = ColumnWidths[index] + + local Width = UDim.new( + if Fixed then 0 else math.clamp(ColumnWidth, 0, 1), + if Fixed then math.max(ColumnWidth, 0) else 0 + ) + thisWidget._widths[index] = Width + Position += Width + + for _, row in thisWidget._cellInstances do + row[index].Size = UDim2.new(Width, UDim.new()) + end + + thisWidget._cellInstances[-1][index].Size = UDim2.new(Width + UDim.new(0, + (if index > 1 then -2 else 0) - 3 + ), UDim.new()) + end + + -- if the table has a fixed width and we want to cap it, we calculate the table width necessary + local Width = Position.Offset + if not thisWidget.arguments.FixedWidth or not thisWidget.arguments.LimitTableWidth then + Width = math.huge + end + + BorderContainer.UISizeConstraint.MaxSize = Vector2.new(Width, math.huge) + RowContainer.UISizeConstraint.MaxSize = Vector2.new(Width, math.huge) + thisWidget._columnBorders[0].Position = UDim2.new(0, Width - 3, 0, 0) + end, + ChildAdded = function(thisWidget: Types.Table, _: Types.Widget) + local rowIndex = thisWidget._rowIndex + local columnIndex = thisWidget._columnIndex + -- determine if the row exists yet + local Row = thisWidget._rowInstances[rowIndex] + thisWidget._rowCycles[rowIndex] = Iris._cycleTick + TableMinWidths[thisWidget][columnIndex] = true + + if Row ~= nil then + return thisWidget._cellInstances[rowIndex][columnIndex] + end + + Row = GenerateRow(thisWidget, rowIndex) + if rowIndex == 0 then + Row.Visible = thisWidget.arguments.Header + end + Row.Parent = thisWidget._rowContainer + + if rowIndex > 0 then + local Border = GenerateRowBorder(thisWidget, rowIndex - 1, if rowIndex == 1 then "Strong" else "Light") + Border.Visible = thisWidget.arguments.InnerBorders and (if rowIndex == 1 then (thisWidget.arguments.Header and thisWidget.arguments.InnerBorders) and (thisWidget._rowInstances[0] ~= nil) else true) + thisWidget._rowBorders[rowIndex - 1] = Border + Border.Parent = thisWidget._rowContainer + end + + return thisWidget._cellInstances[rowIndex][columnIndex] + end, + ChildDiscarded = function(thisWidget: Types.Table, thisChild: Types.Widget) + local Cell = thisChild.Instance.Parent + + if Cell ~= nil then + local columnIndex = tonumber(Cell.Name:sub(6)) + + if columnIndex then + TableMinWidths[thisWidget][columnIndex] = true + end + end + end, + Discard = function(thisWidget: Types.Table) + Tables[thisWidget.ID] = nil + TableMinWidths[thisWidget] = nil + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Text.luau b/src/DebuggerUI/Shared/External/iris/widgets/Text.luau new file mode 100644 index 0000000..afca798 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Text.luau @@ -0,0 +1,134 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + --stylua: ignore + Iris.WidgetConstructor("Text", { + hasState = false, + hasChildren = false, + Args = { + ["Text"] = 1, + ["Wrapped"] = 2, + ["Color"] = 3, + ["RichText"] = 4, + }, + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.Text) + local Text: TextLabel = Instance.new("TextLabel") + Text.Name = "Iris_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: Types.Text) + local Text = thisWidget.Instance :: TextLabel + if thisWidget.arguments.Text == nil then + error("Text argument is required for Iris.Text().", 5) + end + if thisWidget.arguments.Wrapped ~= nil then + Text.TextWrapped = thisWidget.arguments.Wrapped + else + Text.TextWrapped = Iris._config.TextWrapped + end + if thisWidget.arguments.Color then + Text.TextColor3 = thisWidget.arguments.Color + else + Text.TextColor3 = Iris._config.TextColor + end + if thisWidget.arguments.RichText ~= nil then + Text.RichText = thisWidget.arguments.RichText + else + Text.RichText = Iris._config.RichText + end + + Text.Text = thisWidget.arguments.Text + end, + Discard = function(thisWidget: Types.Text) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) + + --stylua: ignore + Iris.WidgetConstructor("SeparatorText", { + hasState = false, + hasChildren = false, + Args = { + ["Text"] = 1, + }, + Events = { + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + return thisWidget.Instance + end), + }, + Generate = function(thisWidget: Types.SeparatorText) + local SeparatorText = Instance.new("Frame") + SeparatorText.Name = "Iris_SeparatorText" + SeparatorText.Size = UDim2.new(Iris._config.ItemWidth, UDim.new()) + SeparatorText.BackgroundTransparency = 1 + SeparatorText.BorderSizePixel = 0 + SeparatorText.AutomaticSize = Enum.AutomaticSize.Y + SeparatorText.LayoutOrder = thisWidget.ZIndex + SeparatorText.ClipsDescendants = true + + widgets.UIPadding(SeparatorText, Vector2.new(0, Iris._config.SeparatorTextPadding.Y)) + widgets.UIListLayout(SeparatorText, Enum.FillDirection.Horizontal, UDim.new(0, Iris._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 = Iris._config.SeparatorColor + Left.BackgroundTransparency = Iris._config.SeparatorTransparency + Left.BorderSizePixel = 0 + Left.Size = UDim2.fromOffset(Iris._config.SeparatorTextPadding.X - Iris._config.ItemSpacing.X, Iris._config.SeparatorTextBorderSize) + + Left.Parent = SeparatorText + + local Right: Frame = Instance.new("Frame") + Right.Name = "Right" + Right.AnchorPoint = Vector2.new(1, 0.5) + Right.BackgroundColor3 = Iris._config.SeparatorColor + Right.BackgroundTransparency = Iris._config.SeparatorTransparency + Right.BorderSizePixel = 0 + Right.Size = UDim2.new(1, 0, 0, Iris._config.SeparatorTextBorderSize) + Right.LayoutOrder = 2 + + Right.Parent = SeparatorText + + return SeparatorText + end, + Update = function(thisWidget: Types.SeparatorText) + local SeparatorText = thisWidget.Instance :: Frame + local TextLabel: TextLabel = SeparatorText.TextLabel + if thisWidget.arguments.Text == nil then + error("Text argument is required for Iris.SeparatorText().", 5) + end + TextLabel.Text = thisWidget.arguments.Text + end, + Discard = function(thisWidget: Types.SeparatorText) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Tree.luau b/src/DebuggerUI/Shared/External/iris/widgets/Tree.luau new file mode 100644 index 0000000..a966bbf --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Tree.luau @@ -0,0 +1,296 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local abstractTree = { + hasState = true, + hasChildren = true, + Events = { + ["collapsed"] = { + ["Init"] = function(_thisWidget: Types.CollapsingHeader) end, + ["Get"] = function(thisWidget: Types.CollapsingHeader) + return thisWidget.lastCollapsedTick == Iris._cycleTick + end, + }, + ["uncollapsed"] = { + ["Init"] = function(_thisWidget: Types.CollapsingHeader) end, + ["Get"] = function(thisWidget: Types.CollapsingHeader) + return thisWidget.lastUncollapsedTick == Iris._cycleTick + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget) + return thisWidget.Instance + end), + }, + Discard = function(thisWidget: Types.CollapsingHeader) + thisWidget.Instance:Destroy() + widgets.discardState(thisWidget) + end, + ChildAdded = function(thisWidget: Types.CollapsingHeader, _thisChild: Types.Widget) + local ChildContainer: Frame = thisWidget.ChildContainer :: Frame + + ChildContainer.Visible = thisWidget.state.isUncollapsed.value + + return ChildContainer + end, + UpdateState = function(thisWidget: Types.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 = Iris._cycleTick + 1 + else + thisWidget.lastCollapsedTick = Iris._cycleTick + 1 + end + + ChildContainer.Visible = isUncollapsed + end, + GenerateState = function(thisWidget: Types.CollapsingHeader) + if thisWidget.state.isUncollapsed == nil then + thisWidget.state.isUncollapsed = Iris._widgetState(thisWidget, "isUncollapsed", thisWidget.arguments.DefaultOpen or false) + end + end, + } :: Types.WidgetClass + + --stylua: ignore + Iris.WidgetConstructor( + "Tree", + widgets.extend(abstractTree, { + Args = { + ["Text"] = 1, + ["SpanAvailWidth"] = 2, + ["NoIndent"] = 3, + ["DefaultOpen"] = 4, + }, + Generate = function(thisWidget: Types.Tree) + local Tree: Frame = Instance.new("Frame") + Tree.Name = "Iris_Tree" + Tree.Size = UDim2.new(Iris._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, Iris._config.ItemSpacing.Y)) + local ChildContainerPadding: UIPadding = widgets.UIPadding(ChildContainer, Vector2.zero) + ChildContainerPadding.PaddingTop = UDim.new(0, Iris._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 = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderActiveColor, + ActiveTransparency = Iris._config.HeaderActiveTransparency, + }) + + local ButtonPadding: UIPadding = widgets.UIPadding(Button, Vector2.zero) + ButtonPadding.PaddingLeft = UDim.new(0, Iris._config.FramePadding.X) + local ButtonUIListLayout: UIListLayout = widgets.UIListLayout(Button, Enum.FillDirection.Horizontal, UDim.new(0, Iris._config.FramePadding.X)) + ButtonUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + Button.Parent = Header + + local Arrow: ImageLabel = Instance.new("ImageLabel") + Arrow.Name = "Arrow" + Arrow.Size = UDim2.fromOffset(Iris._config.TextSize, math.floor(Iris._config.TextSize * 0.7)) + Arrow.BackgroundTransparency = 1 + Arrow.BorderSizePixel = 0 + Arrow.ImageColor3 = Iris._config.TextColor + Arrow.ImageTransparency = Iris._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: Types.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, Iris._config.IndentSpacing) + end + end, + }) + ) + + --stylua: ignore + Iris.WidgetConstructor( + "CollapsingHeader", + widgets.extend(abstractTree, { + Args = { + ["Text"] = 1, + ["DefaultOpen"] = 2 + }, + Generate = function(thisWidget: Types.CollapsingHeader) + local CollapsingHeader: Frame = Instance.new("Frame") + CollapsingHeader.Name = "Iris_CollapsingHeader" + CollapsingHeader.Size = UDim2.new(Iris._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, Iris._config.ItemSpacing.Y)) + local ChildContainerPadding: UIPadding = widgets.UIPadding(ChildContainer, Vector2.zero) + ChildContainerPadding.PaddingTop = UDim.new(0, Iris._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 = Iris._config.HeaderColor + Button.BackgroundTransparency = Iris._config.HeaderTransparency + Button.BorderSizePixel = 0 + Button.Text = "" + Button.AutoButtonColor = false + Button.ClipsDescendants = true + + widgets.UIPadding(Button, Iris._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 * Iris._config.FramePadding.X)) + ButtonUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Center + + widgets.applyInteractionHighlights("Background", Button, Button, { + Color = Iris._config.HeaderColor, + Transparency = Iris._config.HeaderTransparency, + HoveredColor = Iris._config.HeaderHoveredColor, + HoveredTransparency = Iris._config.HeaderHoveredTransparency, + ActiveColor = Iris._config.HeaderActiveColor, + ActiveTransparency = Iris._config.HeaderActiveTransparency, + }) + + Button.Parent = Header + + local Arrow: ImageLabel = Instance.new("ImageLabel") + Arrow.Name = "Arrow" + Arrow.Size = UDim2.fromOffset(Iris._config.TextSize, math.ceil(Iris._config.TextSize * 0.8)) + Arrow.AutomaticSize = Enum.AutomaticSize.Y + Arrow.BackgroundTransparency = 1 + Arrow.BorderSizePixel = 0 + Arrow.ImageColor3 = Iris._config.TextColor + Arrow.ImageTransparency = Iris._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: Types.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 diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Window.luau b/src/DebuggerUI/Shared/External/iris/widgets/Window.luau new file mode 100644 index 0000000..f6d6d2f --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/Window.luau @@ -0,0 +1,1078 @@ +local Types = require(script.Parent.Parent.Types) + +return function(Iris: Types.Internal, widgets: Types.WidgetUtility) + local function relocateTooltips() + if Iris._rootInstance == nil then + return + end + local PopupScreenGui = Iris._rootInstance:FindFirstChild("PopupScreenGui") + local TooltipContainer = PopupScreenGui.TooltipContainer + local mouseLocation: Vector2 = widgets.getMouseLocation() + local newPosition: Vector2 = widgets.findBestWindowPosForPopup(mouseLocation, TooltipContainer.AbsoluteSize, Iris._config.DisplaySafeAreaPadding, PopupScreenGui.AbsoluteSize) + TooltipContainer.Position = UDim2.fromOffset(newPosition.X, newPosition.Y) + end + + widgets.registerEvent("InputChanged", function() + if not Iris._started then + return + end + relocateTooltips() + end) + + --stylua: ignore + Iris.WidgetConstructor("Tooltip", { + hasState = false, + hasChildren = false, + Args = { + ["Text"] = 1, + }, + Events = {}, + Generate = function(thisWidget: Types.Tooltip) + thisWidget.parentWidget = Iris._rootWidget -- only allow root as parent + + local Tooltip: Frame = Instance.new("Frame") + Tooltip.Name = "Iris_Tooltip" + Tooltip.Size = UDim2.new(Iris._config.ContentWidth, UDim.new(0, 0)) + Tooltip.AutomaticSize = Enum.AutomaticSize.Y + Tooltip.BorderSizePixel = 0 + Tooltip.BackgroundTransparency = 1 + Tooltip.ZIndex = 1 + + local TooltipText: TextLabel = Instance.new("TextLabel") + TooltipText.Name = "TooltipText" + TooltipText.Size = UDim2.fromOffset(0, 0) + TooltipText.AutomaticSize = Enum.AutomaticSize.XY + TooltipText.BackgroundColor3 = Iris._config.PopupBgColor + TooltipText.BackgroundTransparency = Iris._config.PopupBgTransparency + TooltipText.TextWrapped = Iris._config.TextWrapped + + widgets.applyTextStyle(TooltipText) + widgets.UIStroke(TooltipText, Iris._config.PopupBorderSize, Iris._config.BorderActiveColor, Iris._config.BorderActiveTransparency) + widgets.UIPadding(TooltipText, Iris._config.WindowPadding) + if Iris._config.PopupRounding > 0 then + widgets.UICorner(TooltipText, Iris._config.PopupRounding) + end + + TooltipText.Parent = Tooltip + + return Tooltip + end, + Update = function(thisWidget: Types.Tooltip) + local Tooltip = thisWidget.Instance :: Frame + local TooltipText: TextLabel = Tooltip.TooltipText + if thisWidget.arguments.Text == nil then + error("Text argument is required for Iris.Tooltip().", 5) + end + TooltipText.Text = thisWidget.arguments.Text + relocateTooltips() + end, + Discard = function(thisWidget: Types.Tooltip) + thisWidget.Instance:Destroy() + end, + } :: Types.WidgetClass) + + local windowDisplayOrder: number = 0 -- incremental count which is used for determining focused windows ZIndex + local dragWindow: Types.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: Types.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: Types.Window? -- window with focus, may be nil + local anyFocusedWindow: boolean = false -- is there any focused window? + + local windowWidgets: { [Types.ID]: Types.Window } = {} -- array of widget objects of type window + + local function quickSwapWindows() + -- ctrl + tab swapping functionality + if Iris._config.UseScreenGUIs == false then + return + end + + local lowest: number = 0xFFFF + local lowestWidget: Types.Window + + for _, widget: Types.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 + Iris.SetFocusedWindow(lowestWidget) + end + + local function fitSizeToWindowBounds(thisWidget: Types.Window, intentedSize: Vector2): Vector2 + local windowSize: Vector2 = Vector2.new(thisWidget.state.position.value.X, thisWidget.state.position.value.Y) + local minWindowSize: number = (Iris._config.TextSize + 2 * Iris._config.FramePadding.Y) * 2 + local usableSize: Vector2 = widgets.getScreenSizeForWindow(thisWidget) + local safeAreaPadding: Vector2 = Vector2.new(Iris._config.WindowBorderSize + Iris._config.DisplaySafeAreaPadding.X, Iris._config.WindowBorderSize + Iris._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: Types.Window, intendedPosition: Vector2): Vector2 + local thisWidgetInstance = thisWidget.Instance + local usableSize: Vector2 = widgets.getScreenSizeForWindow(thisWidget) + local safeAreaPadding: Vector2 = Vector2.new(Iris._config.WindowBorderSize + Iris._config.DisplaySafeAreaPadding.X, Iris._config.WindowBorderSize + Iris._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 + + Iris.SetFocusedWindow = function(thisWidget: Types.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 = Iris._config.TitleBgColor + TitleBar.BackgroundTransparency = Iris._config.TitleBgTransparency + else + TitleBar.BackgroundColor3 = Iris._config.TitleBgCollapsedColor + TitleBar.BackgroundTransparency = Iris._config.TitleBgCollapsedTransparency + end + WindowButton.UIStroke.Color = Iris._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 = Iris._config.TitleBgActiveColor + TitleBar.BackgroundTransparency = Iris._config.TitleBgActiveTransparency + WindowButton.UIStroke.Color = Iris._config.BorderActiveColor + + windowDisplayOrder += 1 + if thisWidget.usesScreenGuis then + Window.DisplayOrder = windowDisplayOrder + Iris._config.DisplayOrderOffset + else + Window.ZIndex = windowDisplayOrder + Iris._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 Iris._started then + return + end + if input.UserInputType == Enum.UserInputType.MouseButton1 then + local inWindow: boolean = false + local position: Vector2 = widgets.getMouseLocation() + for _, window: Types.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 - widgets.GuiOffset, ResizeBorder.AbsolutePosition - widgets.GuiOffset + ResizeBorder.AbsoluteSize) then + inWindow = true + break + end + end + + if not inWindow then + Iris.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 Iris._started then + return + end + if not gameProcessedEvent then + Iris.SetFocusedWindow(nil) + end + end) + + widgets.registerEvent("InputChanged", function(input: InputObject) + if not Iris._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 Iris._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 + Iris.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: Types.Window) end, + ["Get"] = function(thisWidget: Types.Window) + return thisWidget.lastClosedTick == Iris._cycleTick + end, + }, + ["opened"] = { + ["Init"] = function(_thisWidget: Types.Window) end, + ["Get"] = function(thisWidget: Types.Window) + return thisWidget.lastOpenedTick == Iris._cycleTick + end, + }, + ["collapsed"] = { + ["Init"] = function(_thisWidget: Types.Window) end, + ["Get"] = function(thisWidget: Types.Window) + return thisWidget.lastCollapsedTick == Iris._cycleTick + end, + }, + ["uncollapsed"] = { + ["Init"] = function(_thisWidget: Types.Window) end, + ["Get"] = function(thisWidget: Types.Window) + return thisWidget.lastUncollapsedTick == Iris._cycleTick + end, + }, + ["hovered"] = widgets.EVENTS.hover(function(thisWidget: Types.Widget) + local Window = thisWidget.Instance :: Frame + return Window.WindowButton + end), + }, + Generate = function(thisWidget: Types.Window) + thisWidget.parentWidget = Iris._rootWidget -- only allow root as parent + + thisWidget.usesScreenGuis = Iris._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 = Iris._config.DisplayOrderOffset + Window.IgnoreGuiInset = Iris._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 = Iris._config.DisplayOrderOffset + end + Window.Name = "Iris_Window" + + local WindowButton: TextButton = Instance.new("TextButton") + WindowButton.Name = "WindowButton" + WindowButton.Size = UDim2.fromOffset(0, 0) + WindowButton.BackgroundColor3 = Iris._config.MenubarBgColor + WindowButton.BackgroundTransparency = Iris._config.MenubarBgTransparency + WindowButton.BorderSizePixel = 0 + WindowButton.Text = "" + WindowButton.ClipsDescendants = false + WindowButton.AutoButtonColor = false + WindowButton.Selectable = false + WindowButton.SelectionImageObject = Iris.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, Iris._config.WindowBorderSize, Iris._config.BorderColor, Iris._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 + Iris.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 = Iris._config.WindowBgColor + ChildContainer.BackgroundTransparency = Iris._config.WindowBgTransparency + ChildContainer.BorderSizePixel = 0 + + ChildContainer.AutomaticCanvasSize = Enum.AutomaticSize.Y + ChildContainer.ScrollBarImageTransparency = Iris._config.ScrollbarGrabTransparency + ChildContainer.ScrollBarImageColor3 = Iris._config.ScrollbarGrabColor + ChildContainer.CanvasSize = UDim2.fromScale(0, 0) + ChildContainer.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar + ChildContainer.TopImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.MidImage = widgets.ICONS.BLANK_SQUARE + ChildContainer.BottomImage = widgets.ICONS.BLANK_SQUARE + + ChildContainer.LayoutOrder = thisWidget.ZIndex + 0xFFFF + ChildContainer.ClipsDescendants = true + + widgets.UIPadding(ChildContainer, Iris._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 + Iris.SetFocusedWindow(thisWidget) + end + end) + + local TerminatingFrame: Frame = Instance.new("Frame") + TerminatingFrame.Name = "TerminatingFrame" + TerminatingFrame.Size = UDim2.fromOffset(0, Iris._config.WindowPadding.Y + Iris._config.FramePadding.Y) + TerminatingFrame.BackgroundTransparency = 1 + TerminatingFrame.BorderSizePixel = 0 + TerminatingFrame.LayoutOrder = 0x7FFFFFF0 + + local ChildContainerUIListLayout: UIListLayout = widgets.UIListLayout(ChildContainer, Enum.FillDirection.Vertical, UDim.new(0, Iris._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(Iris._config.FramePadding.X)) + widgets.UIListLayout(TitleBar, Enum.FillDirection.Horizontal, UDim.new(0, Iris._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 = Iris._config.TextSize + ((Iris._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 = Iris._config.ButtonColor, + Transparency = 1, + HoveredColor = Iris._config.ButtonHoveredColor, + HoveredTransparency = Iris._config.ButtonHoveredTransparency, + ActiveColor = Iris._config.ButtonActiveColor, + ActiveTransparency = Iris._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 = Iris._config.TextColor + CollapseArrow.ImageTransparency = Iris._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 = Iris._config.ButtonColor, + Transparency = 1, + HoveredColor = Iris._config.ButtonHoveredColor, + HoveredTransparency = Iris._config.ButtonHoveredTransparency, + ActiveColor = Iris._config.ButtonActiveColor, + ActiveTransparency = Iris._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 = Iris._config.TextColor + CloseIcon.ImageTransparency = Iris._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, Iris._config.FramePadding.Y)) + widgets.applyTextStyle(Title) + Title.TextXAlignment = Enum.TextXAlignment[Iris._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 = Iris._config.TextSize + Iris._config.FramePadding.X + + local LeftResizeGrip = Instance.new("ImageButton") + LeftResizeGrip.Name = "LeftResizeGrip" + LeftResizeGrip.AnchorPoint = Vector2.yAxis + LeftResizeGrip.Rotation = 180 + LeftResizeGrip.Size = UDim2.fromOffset(ResizeButtonSize, ResizeButtonSize) + LeftResizeGrip.Position = UDim2.fromScale(0, 1) + LeftResizeGrip.BackgroundTransparency = 1 + LeftResizeGrip.BorderSizePixel = 0 + LeftResizeGrip.Image = widgets.ICONS.BOTTOM_RIGHT_CORNER + LeftResizeGrip.ImageColor3 = Iris._config.ResizeGripColor + LeftResizeGrip.ImageTransparency = 1 + LeftResizeGrip.AutoButtonColor = false + LeftResizeGrip.ZIndex = 3 + LeftResizeGrip.Parent = WindowButton + + widgets.applyInteractionHighlights("Image", LeftResizeGrip, LeftResizeGrip, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyButtonDown(LeftResizeGrip, function() + if not anyFocusedWindow or not (focusedWindow == thisWidget) then + Iris.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.Left + resizeWindow = thisWidget + end) + + -- each border uses an image, allowing it to have a visible borde which is larger than the UI + local RightResizeGrip = Instance.new("ImageButton") + RightResizeGrip.Name = "RightResizeGrip" + RightResizeGrip.AnchorPoint = Vector2.one + RightResizeGrip.Rotation = 90 + RightResizeGrip.Size = UDim2.fromOffset(ResizeButtonSize, ResizeButtonSize) + RightResizeGrip.Position = UDim2.fromScale(1, 1) + RightResizeGrip.BackgroundTransparency = 1 + RightResizeGrip.BorderSizePixel = 0 + RightResizeGrip.Image = widgets.ICONS.BOTTOM_RIGHT_CORNER + RightResizeGrip.ImageColor3 = Iris._config.ResizeGripColor + RightResizeGrip.ImageTransparency = Iris._config.ResizeGripTransparency + RightResizeGrip.AutoButtonColor = false + RightResizeGrip.ZIndex = 3 + RightResizeGrip.Parent = WindowButton + + widgets.applyInteractionHighlights("Image", RightResizeGrip, RightResizeGrip, { + Color = Iris._config.ResizeGripColor, + Transparency = Iris._config.ResizeGripTransparency, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyButtonDown(RightResizeGrip, function() + if not anyFocusedWindow or not (focusedWindow == thisWidget) then + Iris.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 LeftResizeBorder: ImageButton = Instance.new("ImageButton") + LeftResizeBorder.Name = "LeftResizeBorder" + LeftResizeBorder.AnchorPoint = Vector2.new(1, .5) + LeftResizeBorder.Position = UDim2.fromScale(0, .5) + LeftResizeBorder.Size = UDim2.new(0, Iris._config.WindowResizePadding.X, 1, 2 * Iris._config.WindowBorderSize) + LeftResizeBorder.Transparency = 1 + LeftResizeBorder.Image = widgets.ICONS.BORDER + LeftResizeBorder.ResampleMode = Enum.ResamplerMode.Pixelated + LeftResizeBorder.ScaleType = Enum.ScaleType.Slice + LeftResizeBorder.SliceCenter = Rect.new(0, 0, 1, 1) + LeftResizeBorder.ImageRectOffset = Vector2.new(2, 2) + LeftResizeBorder.ImageRectSize = Vector2.new(2, 1) + LeftResizeBorder.ImageTransparency = 1 + LeftResizeBorder.ZIndex = 4 + LeftResizeBorder.AutoButtonColor = false + + LeftResizeBorder.Parent = WindowButton + + local RightResizeBorder: ImageButton = Instance.new("ImageButton") + RightResizeBorder.Name = "RightResizeBorder" + RightResizeBorder.AnchorPoint = Vector2.new(0, .5) + RightResizeBorder.Position = UDim2.fromScale(1, .5) + RightResizeBorder.Size = UDim2.new(0, Iris._config.WindowResizePadding.X, 1, 2 * Iris._config.WindowBorderSize) + RightResizeBorder.Transparency = 1 + RightResizeBorder.Image = widgets.ICONS.BORDER + RightResizeBorder.ResampleMode = Enum.ResamplerMode.Pixelated + RightResizeBorder.ScaleType = Enum.ScaleType.Slice + RightResizeBorder.SliceCenter = Rect.new(1, 0, 2, 1) + RightResizeBorder.ImageRectOffset = Vector2.new(1, 2) + RightResizeBorder.ImageRectSize = Vector2.new(2, 1) + RightResizeBorder.ImageTransparency = 1 + RightResizeBorder.ZIndex = 4 + RightResizeBorder.AutoButtonColor = false + + RightResizeBorder.Parent = WindowButton + + local TopResizeBorder: ImageButton = Instance.new("ImageButton") + TopResizeBorder.Name = "TopResizeBorder" + TopResizeBorder.AnchorPoint = Vector2.new(.5, 1) + TopResizeBorder.Position = UDim2.fromScale(.5, 0) + TopResizeBorder.Size = UDim2.new(1, 2 * Iris._config.WindowBorderSize, 0, Iris._config.WindowResizePadding.Y) + TopResizeBorder.Transparency = 1 + TopResizeBorder.Image = widgets.ICONS.BORDER + TopResizeBorder.ResampleMode = Enum.ResamplerMode.Pixelated + TopResizeBorder.ScaleType = Enum.ScaleType.Slice + TopResizeBorder.SliceCenter = Rect.new(0, 0, 1, 1) + TopResizeBorder.ImageRectOffset = Vector2.new(2, 2) + TopResizeBorder.ImageRectSize = Vector2.new(1, 2) + TopResizeBorder.ImageTransparency = 1 + TopResizeBorder.ZIndex = 4 + TopResizeBorder.AutoButtonColor = false + + TopResizeBorder.Parent = WindowButton + + local BottomResizeBorder: ImageButton = Instance.new("ImageButton") + BottomResizeBorder.Name = "BottomResizeBorder" + BottomResizeBorder.AnchorPoint = Vector2.new(.5, 0) + BottomResizeBorder.Position = UDim2.fromScale(.5, 1) + BottomResizeBorder.Size = UDim2.new(1, 2 * Iris._config.WindowBorderSize, 0, Iris._config.WindowResizePadding.Y) + BottomResizeBorder.Transparency = 1 + BottomResizeBorder.Image = widgets.ICONS.BORDER + BottomResizeBorder.ResampleMode = Enum.ResamplerMode.Pixelated + BottomResizeBorder.ScaleType = Enum.ScaleType.Slice + BottomResizeBorder.SliceCenter = Rect.new(0, 1, 1, 2) + BottomResizeBorder.ImageRectOffset = Vector2.new(2, 1) + BottomResizeBorder.ImageRectSize = Vector2.new(1, 2) + BottomResizeBorder.ImageTransparency = 1 + BottomResizeBorder.ZIndex = 4 + BottomResizeBorder.AutoButtonColor = false + + BottomResizeBorder.Parent = WindowButton + + widgets.applyInteractionHighlights("Image", LeftResizeBorder, LeftResizeBorder, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyInteractionHighlights("Image", RightResizeBorder, RightResizeBorder, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyInteractionHighlights("Image", TopResizeBorder, TopResizeBorder, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + widgets.applyInteractionHighlights("Image", BottomResizeBorder, BottomResizeBorder, { + Color = Iris._config.ResizeGripColor, + Transparency = 1, + HoveredColor = Iris._config.ResizeGripHoveredColor, + HoveredTransparency = Iris._config.ResizeGripHoveredTransparency, + ActiveColor = Iris._config.ResizeGripActiveColor, + ActiveTransparency = Iris._config.ResizeGripActiveTransparency, + }) + + local ResizeBorder: Frame = Instance.new("Frame") + ResizeBorder.Name = "ResizeBorder" + ResizeBorder.Size = UDim2.new(1, Iris._config.WindowResizePadding.X * 2, 1, Iris._config.WindowResizePadding.Y * 2) + ResizeBorder.Position = UDim2.fromOffset(-Iris._config.WindowResizePadding.X, -Iris._config.WindowResizePadding.Y) + ResizeBorder.BackgroundTransparency = 1 + ResizeBorder.BorderSizePixel = 0 + ResizeBorder.Active = false + 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.applyInputBegan(ResizeBorder, function(input: InputObject) + if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Keyboard then + return + end + if thisWidget.state.isUncollapsed.value then + Iris.SetFocusedWindow(thisWidget) + 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: Types.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("Iris_MenuBar") + local LeftResizeGrip: TextButton = WindowButton.LeftResizeGrip + local RightResizeGrip: TextButton = WindowButton.RightResizeGrip + local LeftResizeBorder: Frame = WindowButton.LeftResizeBorder + local RightResizeBorder: Frame = WindowButton.RightResizeBorder + local TopResizeBorder: Frame = WindowButton.TopResizeBorder + local BottomResizeBorder: Frame = WindowButton.BottomResizeBorder + + if thisWidget.arguments.NoResize ~= true then + LeftResizeGrip.Visible = true + RightResizeGrip.Visible = true + LeftResizeBorder.Visible = true + RightResizeBorder.Visible = true + TopResizeBorder.Visible = true + BottomResizeBorder.Visible = true + else + LeftResizeGrip.Visible = false + RightResizeGrip.Visible = false + LeftResizeBorder.Visible = false + RightResizeBorder.Visible = false + TopResizeBorder.Visible = false + BottomResizeBorder.Visible = false + end + if thisWidget.arguments.NoScrollbar then + ChildContainer.ScrollBarThickness = 0 + else + ChildContainer.ScrollBarThickness = Iris._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 = Iris._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: Types.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: Types.Window, thisChid: Types.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: Types.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("Iris_MenuBar") + local LeftResizeGrip: TextButton = WindowButton.LeftResizeGrip + local RightResizeGrip: TextButton = WindowButton.RightResizeGrip + local LeftResizeBorder: Frame = WindowButton.LeftResizeBorder + local RightResizeBorder: Frame = WindowButton.RightResizeBorder + local TopResizeBorder: Frame = WindowButton.TopResizeBorder + local BottomResizeBorder: Frame = WindowButton.BottomResizeBorder + + 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 = Iris._cycleTick + 1 + else + if thisWidget.usesScreenGuis then + Window.Enabled = false + WindowButton.Visible = false + else + Window.Visible = false + WindowButton.Visible = false + end + thisWidget.lastClosedTick = Iris._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 + LeftResizeGrip.Visible = true + RightResizeGrip.Visible = true + LeftResizeBorder.Visible = true + RightResizeBorder.Visible = true + TopResizeBorder.Visible = true + BottomResizeBorder.Visible = true + end + WindowButton.AutomaticSize = Enum.AutomaticSize.None + thisWidget.lastUncollapsedTick = Iris._cycleTick + 1 + else + local collapsedHeight: number = TitleBar.AbsoluteSize.Y -- Iris._config.TextSize + Iris._config.FramePadding.Y * 2 + TitleBar.CollapseButton.Arrow.Image = widgets.ICONS.RIGHT_POINTING_TRIANGLE + + if MenuBar then + MenuBar.Visible = false + end + ChildContainer.Visible = false + LeftResizeGrip.Visible = false + RightResizeGrip.Visible = false + LeftResizeBorder.Visible = false + RightResizeBorder.Visible = false + TopResizeBorder.Visible = false + BottomResizeBorder.Visible = false + WindowButton.Size = UDim2.fromOffset(stateSize.X, collapsedHeight) + thisWidget.lastCollapsedTick = Iris._cycleTick + 1 + end + + if stateIsOpened and stateIsUncollapsed then + Iris.SetFocusedWindow(thisWidget) + else + TitleBar.BackgroundColor3 = Iris._config.TitleBgCollapsedColor + TitleBar.BackgroundTransparency = Iris._config.TitleBgCollapsedTransparency + WindowButton.UIStroke.Color = Iris._config.BorderColor + + Iris.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 = #Iris._postCycleCallbacks + 1 + local desiredCycleTick: number = Iris._cycleTick + 1 + Iris._postCycleCallbacks[callbackIndex] = function() + if Iris._cycleTick >= desiredCycleTick then + if thisWidget.lastCycleTick ~= -1 then + ChildContainer.CanvasPosition = Vector2.new(0, stateScrollDistance) + end + Iris._postCycleCallbacks[callbackIndex] = nil + end + end + end + end, + GenerateState = function(thisWidget: Types.Window) + if thisWidget.state.size == nil then + thisWidget.state.size = Iris._widgetState(thisWidget, "size", Vector2.new(400, 300)) + end + if thisWidget.state.position == nil then + thisWidget.state.position = Iris._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 = Iris._widgetState(thisWidget, "isUncollapsed", true) + end + if thisWidget.state.isOpened == nil then + thisWidget.state.isOpened = Iris._widgetState(thisWidget, "isOpened", true) + end + if thisWidget.state.scrollDistance == nil then + thisWidget.state.scrollDistance = Iris._widgetState(thisWidget, "scrollDistance", 0) + end + end, + } :: Types.WidgetClass) +end diff --git a/src/DebuggerUI/Shared/External/iris/widgets/init.luau b/src/DebuggerUI/Shared/External/iris/widgets/init.luau new file mode 100644 index 0000000..ce8f2c4 --- /dev/null +++ b/src/DebuggerUI/Shared/External/iris/widgets/init.luau @@ -0,0 +1,448 @@ +local Types = require(script.Parent.Types) + +local widgets = {} :: Types.WidgetUtility + +return function(Iris: Types.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 = { + BLANK_SQUARE = "rbxasset://textures/SurfacesDefault.png", + 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", -- best approximation for a close X which roblox supports, needs to be scaled about 2x + BOTTOM_RIGHT_CORNER = "rbxasset://textures/ui/InspectMenu/gr-item-selector-triangle.png", -- used in window resize icon in bottom right + CHECK_MARK = "rbxasset://textures/AnimationEditor/icon_checkmark.png", + BORDER = "rbxasset://textures/ui/InspectMenu/gr-item-selector.png", + ALPHA_BACKGROUND_TEXTURE = "rbxasset://textures/meshPartFallback.png", -- used for color4 alpha + UNKNOWN_TEXTURE = "rbxasset://textures/ui/GuiImagePlaceholder.png", + } + + widgets.IS_STUDIO = widgets.RunService:IsStudio() + function widgets.getTime(): number + -- time() always returns 0 in the context of plugins + if widgets.IS_STUDIO then + return os.clock() + else + return time() + end + end + + -- acts as an offset where the absolute position of the base frame is not zero, such as IgnoreGuiInset or for stories + widgets.GuiOffset = if Iris._config.IgnoreGuiInset then -widgets.GuiService:GetGuiInset() else Vector2.zero + -- the registered mouse position always ignores the topbar, so needs a separate variable offset + widgets.MouseOffset = if Iris._config.IgnoreGuiInset then Vector2.zero else widgets.GuiService:GetGuiInset() + + -- the topbar inset changes updates a frame later. + local connection: RBXScriptConnection + connection = widgets.GuiService:GetPropertyChangedSignal("TopbarInset"):Once(function() + widgets.MouseOffset = if Iris._config.IgnoreGuiInset then Vector2.zero else widgets.GuiService:GetGuiInset() + widgets.GuiOffset = if Iris._config.IgnoreGuiInset then -widgets.GuiService:GetGuiInset() else Vector2.zero + connection:Disconnect() + end) + -- in case the topbar doesn't change, we cancel the event. + task.delay(5, function() + connection:Disconnect() + end) + + 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 + -- placed to the top + refPos += Vector2.new(0, -(CURSOR_OFFSET_DIST + size.Y)) + else + -- placed to the bottom + refPos += Vector2.new(0, CURSOR_OFFSET_DIST) + end + else + -- placed to the right + 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: Types.Widget): Vector2 -- possible parents are GuiBase2d, CoreGui, PlayerGui + 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: Types.WidgetClass, subClass: Types.WidgetClass): Types.WidgetClass + local newClass: Types.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 -- made these optional + UISizeConstraintInstance.MaxSize = MaxSize or UISizeConstraintInstance.MaxSize + UISizeConstraintInstance.Parent = Parent + return UISizeConstraintInstance + end + + -- below uses Iris + + function widgets.applyTextStyle(thisInstance: TextLabel & TextButton & TextBox) + thisInstance.FontFace = Iris._config.TextFont + thisInstance.TextSize = Iris._config.TextSize + thisInstance.TextColor3 = Iris._config.TextColor + thisInstance.TextTransparency = Iris._config.TextTransparency + thisInstance.TextXAlignment = Enum.TextXAlignment.Left + thisInstance.TextYAlignment = Enum.TextYAlignment.Center + thisInstance.RichText = Iris._config.RichText + thisInstance.TextWrapped = Iris._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 = Iris.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 = Iris.SelectionImageObject + end + + function widgets.applyFrameStyle(thisInstance: GuiObject, noPadding: boolean?, noCorner: boolean?) + -- padding, border, and rounding + -- optimized to only use what instances are needed, based on style + local FrameBorderSize: number = Iris._config.FrameBorderSize + local FrameRounding: number = Iris._config.FrameRounding + thisInstance.BorderSizePixel = 0 + + if FrameBorderSize > 0 then + widgets.UIStroke(thisInstance, FrameBorderSize, Iris._config.BorderColor, Iris._config.BorderTransparency) + end + if FrameRounding > 0 and not noCorner then + widgets.UICorner(thisInstance, FrameRounding) + end + if not noPadding then + widgets.UIPadding(thisInstance, Iris._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(x: number, y: number) + local position: Vector2 = Vector2.new(x, y) - widgets.MouseOffset + callback(position.X, position.Y) + end) + end + + function widgets.applyMouseEnter(thisInstance: GuiObject, callback: (x: number, y: number) -> ()) + thisInstance.MouseEnter:Connect(function(x: number, y: number) + local position: Vector2 = Vector2.new(x, y) - widgets.MouseOffset + callback(position.X, position.Y) + end) + end + + function widgets.applyMouseMoved(thisInstance: GuiObject, callback: (x: number, y: number) -> ()) + thisInstance.MouseMoved:Connect(function(x: number, y: number) + local position: Vector2 = Vector2.new(x, y) - widgets.MouseOffset + callback(position.X, position.Y) + end) + end + + function widgets.applyMouseLeave(thisInstance: GuiObject, callback: (x: number, y: number) -> ()) + thisInstance.MouseLeave:Connect(function(x: number, y: number) + local position: Vector2 = Vector2.new(x, y) - widgets.MouseOffset + callback(position.X, position.Y) + 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: Types.StateWidget) + for _, state: Types.State in thisWidget.state do + state.ConnectedWidgets[thisWidget.ID] = nil + end + end + + function widgets.registerEvent(event: string, callback: (...any) -> ()) + table.insert(Iris._initFunctions, function() + table.insert(Iris._connections, widgets.UserInputService[event]:Connect(callback)) + end) + end + + widgets.EVENTS = { + hover = function(pathToHovered: (thisWidget: Types.Widget) -> GuiObject) + return { + ["Init"] = function(thisWidget: Types.Widget & Types.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: Types.Widget & Types.Hovered): boolean + return thisWidget.isHoveredEvent + end, + } + end, + + click = function(pathToClicked: (thisWidget: Types.Widget) -> GuiButton) + return { + ["Init"] = function(thisWidget: Types.Widget & Types.Clicked) + local clickedGuiObject: GuiButton = pathToClicked(thisWidget) + thisWidget.lastClickedTick = -1 + + widgets.applyButtonClick(clickedGuiObject, function() + thisWidget.lastClickedTick = Iris._cycleTick + 1 + end) + end, + ["Get"] = function(thisWidget: Types.Widget & Types.Clicked): boolean + return thisWidget.lastClickedTick == Iris._cycleTick + end, + } + end, + + rightClick = function(pathToClicked: (thisWidget: Types.Widget) -> GuiButton) + return { + ["Init"] = function(thisWidget: Types.Widget & Types.RightClicked) + local clickedGuiObject: GuiButton = pathToClicked(thisWidget) + thisWidget.lastRightClickedTick = -1 + + clickedGuiObject.MouseButton2Click:Connect(function() + thisWidget.lastRightClickedTick = Iris._cycleTick + 1 + end) + end, + ["Get"] = function(thisWidget: Types.Widget & Types.RightClicked): boolean + return thisWidget.lastRightClickedTick == Iris._cycleTick + end, + } + end, + + doubleClick = function(pathToClicked: (thisWidget: Types.Widget) -> GuiButton) + return { + ["Init"] = function(thisWidget: Types.Widget & Types.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 < Iris._config.MouseDoubleClickTime + if isTimeValid and (Vector2.new(x, y) - thisWidget.lastClickedPosition).Magnitude < Iris._config.MouseDoubleClickMaxDist then + thisWidget.lastDoubleClickedTick = Iris._cycleTick + 1 + else + thisWidget.lastClickedTime = currentTime + thisWidget.lastClickedPosition = Vector2.new(x, y) + end + end) + end, + ["Get"] = function(thisWidget: Types.Widget & Types.DoubleClicked): boolean + return thisWidget.lastDoubleClickedTick == Iris._cycleTick + end, + } + end, + + ctrlClick = function(pathToClicked: (thisWidget: Types.Widget) -> GuiButton) + return { + ["Init"] = function(thisWidget: Types.Widget & Types.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 = Iris._cycleTick + 1 + end + end) + end, + ["Get"] = function(thisWidget: Types.Widget & Types.CtrlClicked): boolean + return thisWidget.lastCtrlClickedTick == Iris._cycleTick + end, + } + end, + } + + Iris._utility = widgets + + require(script.Root)(Iris, widgets) + require(script.Window)(Iris, widgets) + + require(script.Menu)(Iris, widgets) + + require(script.Format)(Iris, widgets) + + require(script.Text)(Iris, widgets) + require(script.Button)(Iris, widgets) + require(script.Checkbox)(Iris, widgets) + require(script.RadioButton)(Iris, widgets) + require(script.Image)(Iris, widgets) + + require(script.Tree)(Iris, widgets) + require(script.Tab)(Iris, widgets) + + require(script.Input)(Iris, widgets) + require(script.Combo)(Iris, widgets) + require(script.Plot)(Iris, widgets) + + require(script.Table)(Iris, widgets) +end From 54aea439cb75afb6df2e1c1a6ee9740476899022 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:17:57 +0700 Subject: [PATCH 04/22] Add FastCastEventsModule folder and Defualt.luau --- .../Shared/FastCastEventsModule/Default.luau | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/DebuggerUI/Shared/FastCastEventsModule/Default.luau diff --git a/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau new file mode 100644 index 0000000..41fd537 --- /dev/null +++ b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau @@ -0,0 +1,54 @@ +-- Services +local Rep = game:GetService("ReplicatedStorage") + +-- Modules + +local FastCast2 = Rep:WaitForChild("FastCast2") + +-- Requires +local TypeDef = require(FastCast2:WaitForChild("TypeDefinitions")) + +-- Module + +local module: TypeDef.FastCastEvents = {} + +local debounce = false +local debounce_time = 0.2 + +module.LengthChanged = function(cast : TypeDef.ActiveCast) + if not debounce then + debounce = true + print("OnLengthChanged Test") + task.delay(debounce_time, function() + debounce = false + end) + end +end + +module.CastFire = function() + print("CastFire!") +end + +module.CastTerminating = function() + print("CastTerminating!") +end + +module.RayHit = function() + print("Hit!") +end + +module.CanPierce = function(cast, resultOfCast : RaycastResult, segmentVelocity, CosmeticBulletObject) + local CanPierce = false + if resultOfCast.Instance:GetAttribute("CanPierce") == true then + CanPierce = true + end + print(CanPierce) + return CanPierce +end + +module.Pierced = function() + print("Pierced!") +end + + +return module From 6dafd61b416f5647ad8fc24e7621ac3d4d3b4f03 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:18:41 +0700 Subject: [PATCH 05/22] Add ShotTests to Tests folder --- .../Shared/Tests/Client ShotTest.luau | 71 +++++++++++++++++++ .../Shared/Tests/Server ShotTest.luau | 64 +++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/DebuggerUI/Shared/Tests/Client ShotTest.luau create mode 100644 src/DebuggerUI/Shared/Tests/Server ShotTest.luau diff --git a/src/DebuggerUI/Shared/Tests/Client ShotTest.luau b/src/DebuggerUI/Shared/Tests/Client ShotTest.luau new file mode 100644 index 0000000..3a5ff3e --- /dev/null +++ b/src/DebuggerUI/Shared/Tests/Client ShotTest.luau @@ -0,0 +1,71 @@ +-- Services +local UIS = game:GetService("UserInputService") +local Rep = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +-- Modules +local FastCast2 = Rep:WaitForChild("FastCast2") + +-- Requires +local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) + +-- Variables +local currentBehavior: FastCastTypes.FastCastBehavior = nil +local currentVelocity: number = 50 +local currentCaster: FastCastTypes.Caster = nil + +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() + +local Head: BasePart = character:WaitForChild("Head") + +local mouse = player:GetMouse() + +local debounce = false +local debounce_time = 0.01 + +local connection: RBXScriptConnection = nil + +-- Module + +local module = {} + +function module.Start(IntCountEvent: RBXScriptConnection, newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) + currentBehavior = newBehavior + currentVelocity = newVelocity + currentCaster = newCaster + + connection = UIS.InputBegan:Connect(function(Input: InputObject, gp: boolean) + if gp then return end + if debounce then return end + + if Input.UserInputType == Enum.UserInputType.MouseButton1 then + debounce = true + + local Origin = Head.Position + local Direction = (mouse.Hit.Position - Origin).Unit + + newCaster:RaycastFire(Origin, Direction, currentVelocity, currentBehavior) + IntCountEvent:Fire(1) + + task.wait(debounce_time) + debounce = false + end + end) +end + +function module.Update(newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) + currentCaster = newCaster + currentVelocity = newVelocity + currentBehavior = newBehavior +end + +function module.Stop() + if connection then + connection:Disconnect() + connection = nil + end + debounce = false +end + +return module diff --git a/src/DebuggerUI/Shared/Tests/Server ShotTest.luau b/src/DebuggerUI/Shared/Tests/Server ShotTest.luau new file mode 100644 index 0000000..67a5d75 --- /dev/null +++ b/src/DebuggerUI/Shared/Tests/Server ShotTest.luau @@ -0,0 +1,64 @@ +-- Services +local UIS = game:GetService("UserInputService") +local Rep = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +-- Modules +local FastCast2 = Rep:WaitForChild("FastCast2") + +-- Requires +local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) +local Jolt = require(Rep:WaitForChild("Jolt")) + +-- Variables +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() + +local Head: BasePart = character:WaitForChild("Head") + +local mouse = player:GetMouse() + +local debounce = false +local debounce_time = 0.01 + +local connection: RBXScriptConnection = nil + +-- Events +local ServerProjectile = Jolt.Client("ServerProjectile") :: Jolt.Client + +-- Module + +local module = {} + +function module.Start() + connection = UIS.InputBegan:Connect(function(Input: InputObject, gp: boolean) + if gp then return end + if debounce then return end + + if Input.UserInputType == Enum.UserInputType.MouseButton1 then + debounce = true + + local Origin = Head.Position + local Direction = (mouse.Hit.Position - Origin).Unit + + ServerProjectile:Fire(Origin, Direction) + + task.wait(debounce_time) + debounce = false + end + end) +end + +function module.Update(newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) + +end + +function module.Stop() + if connection then + connection:Disconnect() + connection = nil + end + debounce = false +end + +return module From e1595ee9b7bcc765df00e67903750c8c74008bd5 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:19:39 +0700 Subject: [PATCH 06/22] Add IrisServer --- src/DebuggerUI/Server/IrisServer.server.luau | 145 +++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/DebuggerUI/Server/IrisServer.server.luau diff --git a/src/DebuggerUI/Server/IrisServer.server.luau b/src/DebuggerUI/Server/IrisServer.server.luau new file mode 100644 index 0000000..c960b1b --- /dev/null +++ b/src/DebuggerUI/Server/IrisServer.server.luau @@ -0,0 +1,145 @@ +-- Services +local Rep = game:GetService("ReplicatedStorage") +local SSS = game:GetService("ServerScriptService") + +-- Modules +local FastCast2 = Rep:WaitForChild("FastCast2") + +-- Requires +local FastCastM = require(FastCast2) +local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) +local Jolt = require(Rep:WaitForChild("Jolt")) + +-- Types +type RequestLogDataType = "FastCastBehavior" | "Setting" + +-- Variables +local currentVelocity = 50 +local ServerProjectileLimit = 1000 +local scriptController: LocalScript = nil + +local debounce_lc = false +local debounce_lc_time = 1.5 + +local ServerProjectileCount = 0 + +-- Events +local CastBehaviorServerUpdate = Jolt.Server("CastBehaviorServerUpdate") :: Jolt.Server +local LoggingServer = Jolt.Server("LoggingServer") :: Jolt.Server +local ServerSettingUpdate = Jolt.Server("ServerSettingUpdate") :: Jolt.Server<{velocity: number, projectileLimit: number}> +local ServerProjectile = Jolt.Server("ServerProjectile") :: Jolt.Server +local ServerProjectileCountEvent = Jolt.Server("ServerProjectileCount") :: Jolt.Server +local ServerCastModuleUpdate = Jolt.Server("ServerCastModuleUpdate") :: Jolt.Server + + +-- Behavior +local CastBehaviorServer: FastCastTypes.FastCastBehavior = FastCastM.newBehavior() +CastBehaviorServer.VisualizeCasts = false -- Explictly set to false to avoid confusion. +CastBehaviorServer.Acceleration = Vector3.new() +CastBehaviorServer.AutoIgnoreContainer = true +CastBehaviorServer.MaxDistance = 1000 +CastBehaviorServer.HighFidelitySegmentSize = 1 + +CastBehaviorServer.FastCastEventsConfig = { + UseLengthChanged = false, + UseRayHit = true, + UseCastTerminating = true, + UseCastFire = false, + UseRayPierced = false +} + +CastBehaviorServer.FastCastEventsModuleConfig = { + UseLengthChanged = false, + UseRayHit = true, + UseCastTerminating = true, + UseCastFire = false, + UseRayPierced = false, + UseCanRayPierce = false +} + +-- Local functions + +local function IntCount(amount: number) + ServerProjectileCount += amount + ServerProjectileCountEvent:FireAllUnreliable(ServerProjectileCount) +end + +-- Caster +local Caster = FastCastM.new() +Caster:Init( + 4, + SSS, + "CastVMs", + SSS, + "VMContainer", + "CastVM" +) + +Caster.CastTerminating = function() + IntCount(-1) +end +Caster.CastFire = function() + print("CastFire Test!") +end +Caster.Hit = function(cast, result) + print("RayHit Test!") +end +Caster.LengthChanged = function() + if not debounce_lc then + debounce_lc = true + print("OnLengthChanged Test!") + task.delay(debounce_lc_time, function() + debounce_lc = false + end) + end +end +Caster.Pierced = function() + print("Ray pierced Test!") +end + +-- Connections + +LoggingServer:Connect(function(player: Player, RequestLogDataType: RequestLogDataType) + if RequestLogDataType == "FastCastBehavior" then + print(CastBehaviorServer) + end + + if RequestLogDataType == "Setting" then + print({ + velocity = currentVelocity, + projectileLimit = ServerProjectileLimit + }) + end +end) + +CastBehaviorServerUpdate:Connect(function(player: Player, newBehavior: FastCastTypes.FastCastBehavior) + for key, value in newBehavior do + CastBehaviorServer[key] = value + end + if CastBehaviorServer.CosmeticBulletTemplate then + CastBehaviorServer.CosmeticBulletTemplate = nil + end + if not CastBehaviorServer.RaycastParams then + local CastParams = RaycastParams.new() + CastParams.IgnoreWater = true + CastParams.FilterType = Enum.RaycastFilterType.Exclude + CastParams.FilterDescendantsInstances = {player.Character, workspace:WaitForChild("Projectiles")} + end +end) + +ServerSettingUpdate:Connect(function(player: Player, data) + currentVelocity = data.velocity + ServerProjectileLimit = data.projectileLimit +end) + +ServerProjectile:Connect(function(player: Player, Origin: Vector3, Direction: Vector3, velocity: number?) + if ServerProjectileCount >= ServerProjectileLimit then + return + end + Caster:RaycastFire(Origin, Direction, velocity or currentVelocity, CastBehaviorServer) + IntCount(1) +end) + +ServerCastModuleUpdate:Connect(function(player: Player, moduleScript: ModuleScript) + Caster:SetFastCastEventsModule(moduleScript) +end) \ No newline at end of file From 291f62ab21cb4ac3707791d19e7f37a16a9b2d20 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sun, 8 Mar 2026 22:46:59 +0700 Subject: [PATCH 07/22] Add Remotes.meta.json to Jolt --- .../Shared/External/Jolt/Utils/Remotes.meta.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json b/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json new file mode 100644 index 0000000..45d0f10 --- /dev/null +++ b/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json @@ -0,0 +1,10 @@ +{ + "children": { + "Jolt_Reliable": { + "className": "RemoteEvent" + }, + "Jolt_Unreliable": { + "className": "UnreliableRemoteEvent" + } + } +} \ No newline at end of file From 9ef808c1e4bc2439acc5772b152ec7a81d166e0a Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 15:54:20 +0700 Subject: [PATCH 08/22] Add RemoteTableLight --- .../External/RemoteTableLight/Client.luau | 324 ++++++++ .../External/RemoteTableLight/Server.luau | 433 +++++++++++ .../RemoteTableLight/Shared/LICENSE.luau | 167 +++++ .../Shared/Packet/Signal.luau | 101 +++ .../RemoteTableLight/Shared/Packet/Task.luau | 46 ++ .../Shared/Packet/Types/Characters.luau | 6 + .../Shared/Packet/Types/Enums.luau | 11 + .../Shared/Packet/Types/Static1.luau | 8 + .../Shared/Packet/Types/Static2.luau | 8 + .../Shared/Packet/Types/Static3.luau | 8 + .../Shared/Packet/Types/init.luau | 705 ++++++++++++++++++ .../RemoteTableLight/Shared/Packet/init.luau | 395 ++++++++++ .../RemoteTableLight/Shared/Packets.luau | 34 + .../RemoteTableLight/Shared/PromiseLight.luau | 88 +++ .../Shared/TokenRegistry.luau | 128 ++++ .../RemoteTableLight/Shared/Util.luau | 141 ++++ .../RemoteTableLight/Shared/VERSIONS.luau | 7 + .../RemoteTableLight/Shared/Zignal.luau | 105 +++ .../External/RemoteTableLight/init.luau | 15 + 19 files changed, 2730 insertions(+) create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau create mode 100644 src/DebuggerUI/Shared/External/RemoteTableLight/init.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau new file mode 100644 index 0000000..c5e3a55 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau @@ -0,0 +1,324 @@ +--!strict +--!optimize 2 + +if game:GetService("RunService"):IsServer() then + error("Can't require client module from server.") +end + +local Client = {} + +local Shared = script.Parent.Shared +local Util = require(Shared.Util) +local Signal = require(Shared.Zignal) +local Packets = require(Shared.Packets) +local PromiseLight = require(Shared.PromiseLight) +local TokenRegistry = require(Shared.TokenRegistry) + +local OpCodes = Util.OpCodes +local AppendPath = Util.AppendPath +local ToPathString = Util.ToPathString + +type Id = Util.Id +type Key = Util.Key +type Path = Util.Path +type Table = Util.Table +type Token = Util.Token +type TokenName = Util.TokenName + +type SignalType = "ChildAdded" | "ChildRemoved" | "Changed" + +type SignalGroup = { + Changed: Signal.Signal?, + ChildAdded: Signal.Signal?, + ChildRemoved: Signal.Signal?, +} +local DataSignals = {} :: {[Token]: {[Path]: SignalGroup}} + +local VALID_SIGNAL_TYPES = { + Changed = true, + ChildAdded = true, + ChildRemoved = true, +} + +local TableById = {} :: {[Id]: Table} +local IdByTable = {} :: {[Table]: Id} +local PathByTable = {} :: {[Table]: Path} +local RemoteTables = {} :: {[Token]: Table} +local TableReadyPromises = {} :: {[TokenName]: PromiseLight.PromiseLight} + +local function TriggerSignal(signal_type: SignalType, token: Token, path: Path, ...: any) + local signals = DataSignals[token][path] + if not signals then return end + + local signal = signals[signal_type] + if signal then + task.defer(signal.Fire, signal, ...) + end +end + +local function UnregisterValue(token: Token, parent: any, key: Key) + local value = parent[key] + if typeof(value) ~= "table" then return end + + -- Unregister the value + local id = IdByTable[value] + IdByTable[value] = nil + TableById[id] = nil + PathByTable[value] = nil + + local parent_path = PathByTable[parent] + TriggerSignal("ChildRemoved", token, parent_path, value, key) + + for k, v in value do + UnregisterValue(token, value, k) + end +end + +local function NewRemoteTable(token: Token, id: Id, write_end: boolean) + if write_end then + local root = RemoteTables[token] + local token_name = TokenRegistry.GetTokenName(token) + local promise = TableReadyPromises[token_name] + promise:Resolve("Success", root) + else + local root = {} + RemoteTables[token] = root + + -- Register root + IdByTable[root] = id + TableById[id] = root + PathByTable[root] = "" + DataSignals[token] = {} + end +end + +local function DestroyRemoteTable(token: Token) + for path, signals in DataSignals[token] do + if signals.Changed then + signals.Changed:DisconnectAll() + end + if signals.ChildAdded then + signals.ChildAdded:DisconnectAll() + end + if signals.ChildRemoved then + signals.ChildRemoved:DisconnectAll() + end + end + + local root = RemoteTables[token] + for k, v in root do + UnregisterValue(token, root, k) + end + + DataSignals[token] = nil + RemoteTables[token] = nil +end + +local function NewTable(token: Token, id: Id, key: Key, assigned_id: Id) + local parent = TableById[id] + local parent_path = PathByTable[parent] + UnregisterValue(token, parent, key) + + local new_table = {} + TriggerSignal("ChildAdded", token, parent_path, new_table, key) + + -- Register new table + local new_table_path = AppendPath(parent_path, key) + IdByTable[new_table] = assigned_id + TableById[assigned_id] = new_table + PathByTable[new_table] = new_table_path + + local previous_value = parent[key] + parent[key] = new_table + TriggerSignal("Changed", token, new_table_path, new_table, previous_value) +end + +local function Set(token: Token, id: Id, key: Key, value: any) + local parent = TableById[id] + local parent_path = PathByTable[parent] + UnregisterValue(token, parent, key) + + local value_path = AppendPath(parent_path, key) + local previous_value = parent[key] + parent[key] = value + TriggerSignal("Changed", token, value_path, value, previous_value) + + if previous_value ~= nil then + TriggerSignal("ChildRemoved", token, parent_path, previous_value, key) + end + + if value ~= nil then + TriggerSignal("ChildAdded", token, parent_path, value, key) + end +end + +local function Insert(token: Token, id: Id, value: any) + local parent = TableById[id] + local parent_path = PathByTable[parent] + + table.insert(parent, value) + TriggerSignal("ChildAdded", token, parent_path, value, #parent) +end + +local function InsertAt(token: Token, id: Id, index: number, value: any) + local parent = TableById[id] + local parent_path = PathByTable[parent] + + table.insert(parent, index, value) + TriggerSignal("ChildAdded", token, parent_path, value, index) +end + +local function Remove(token: Token, id: Id, index: number) + local parent = TableById[id] + local parent_path = PathByTable[parent] + + local value = table.remove(parent, index) + TriggerSignal("ChildRemoved", token, parent_path, value, index) +end + +local function SwapRemove(token: Token, id: Id, index: number) + local parent = TableById[id] + local parent_path = PathByTable[parent] + + local value = Util.SwapRemove(parent, index) + TriggerSignal("ChildRemoved", token, parent_path, value, index) +end + +local function Clear(token: Token, id: Id, index: number) + local parent = TableById[id] + local parent_path = PathByTable[parent] + + for k, v in parent do + TriggerSignal("ChildRemoved", token, parent_path, v, k) + end + table.clear(parent) +end + +local EventFunctions = { + NewRemoteTable = NewRemoteTable, + DestroyRemoteTable = DestroyRemoteTable, + NewTable = NewTable, + Set = Set, + Insert = Insert, + InsertAt = InsertAt, + Remove = Remove, + SwapRemove = SwapRemove, + Clear = Clear, +} :: {[string]: (Token, ...any) -> ()} + +Packets.SendEventStream.OnClientEvent:Connect(function(package: buffer) + local package_len = buffer.len(package) + local offset = 0 + + -- Token header + local token = buffer.readu16(package, offset) + offset += 2 + + local deserialized + while offset < package_len do + local op_code = buffer.readu8(package, offset) + offset += 1 + + local event = OpCodes[op_code] + + offset, deserialized = Packets[event]:DeserializeReturnOffset(package, offset) + EventFunctions[event](token, table.unpack(deserialized)) + end +end) + +--[[ + Checks if a remote table with a specific token is ready + @param token_name: String alias of the token + @return boolean +]] +function Client.IsRemoteTableReady(token_name: TokenName): boolean + if not TokenRegistry.IsTokenRegistered(token_name) then return false end + local token = TokenRegistry.GetToken(token_name) + return RemoteTables[token] ~= nil +end + + +--[[ + Returns the table if available, waits for it if not. + @param token_name: String alias of the token + @param timeout: Timeout in seconds. Returns nil after timing out + @return data: Ready-only replicated table. +]] +function Client.WaitForTable(token_name: TokenName, timeout: number?): any + local token_name = Util.SanitizeForAttributeName(token_name) + local token = TokenRegistry.WaitForToken(token_name, timeout) + if not token then return nil end + if RemoteTables[token] then return RemoteTables[token] end + + local promise = TableReadyPromises[token_name] + if promise then return select(2, promise:Await()) end + + local promise = PromiseLight.new(timeout) + TableReadyPromises[token_name] = promise + + promise.PreResolve = function(status, token) + if status == "Timeout" then + warn("[RemoteTableLight]: WaitForTable timed out.") + elseif status == "Cancel" then + warn("[RemoteTableLight]: Token unregistered before connection.") + end + TableReadyPromises[token_name] = nil + end + + Packets.ConnectionRequest:Fire(token) + + return select(2, promise:Await()) +end + +--[[ + Gets the apropriate data signal + @param token_name: String alias of the token + @param signal_type: "Changed" | "ChildAdded" | "ChildRemoved" + @param path_list: A string array representing the desired path + @return Signal: Signal that fires (new, old) data or (value, key) for child signals +]] +function Client.GetSignal(token_name: TokenName, signal_type: SignalType, path_list: {Key}): Signal.Signal + assert(type(path_list)=="table", "Path list must be a table.") + assert(VALID_SIGNAL_TYPES[signal_type], "Not a valid signal type.") + Client.WaitForTable(token_name) + + local token = TokenRegistry.GetToken(token_name) + local path_string = ToPathString(path_list) + local signals = DataSignals[token][path_string] + if not signals then + signals = {} + DataSignals[token][path_string] = signals + end + + local signal = signals[signal_type] + if not signal then + signal = Signal.new() + signals[signal_type] = signal + end + + return signal +end + +--[[ + Cleans up the specified signal + @param token_name: String alias of the token + @param signal_type: "Changed" | "ChildAdded" | "ChildRemoved" + @param path_list: A string array representing the desired path +]] +function Client.DestroySignal(token_name: TokenName, signal_type: SignalType, path_list: {Key}) + assert(type(path_list)=="table", "Path list must be a table.") + assert(VALID_SIGNAL_TYPES[signal_type], "Not a valid signal type.") + if not Client.IsRemoteTableReady(token_name) then return end + + local token = TokenRegistry.GetToken(token_name) + local path_string = ToPathString(path_list) + local signals = DataSignals[token][path_string] + if not signals then return end + + if signals[signal_type] then + signals[signal_type]:Destroy() + signals[signal_type] = nil + end +end + +return Client \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau new file mode 100644 index 0000000..4c8dfaa --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau @@ -0,0 +1,433 @@ +--!strict + +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +assert(RunService:IsServer(), "Unable to require server module from client.") + +local Server = {} + +local Shared = script.Parent.Shared +local Util = require(Shared.Util) +local Packets = require(Shared.Packets) +local TokenRegistry = require(Shared.TokenRegistry) + +local DeepCopy = Util.DeepCopy +local BufferAppend = Util.BufferAppend +local UtilGetTableId = Util.GetTableId +local BufferWriteU8 = Util.BufferWriteU8 +local BufferWriteU16 = Util.BufferWriteU16 +local BufferTruncate = Util.BufferTruncate +local IsValidValueType = Util.IsValidValueType + +type Id = Util.Id +type Key = Util.Key +type Table = Util.Table +type Token = Util.Token +type TokenName = Util.TokenName +type ListenerTable = {[Player]: boolean} + +local SET_OPCODE = Util.OpCodeLookup["Set"] +local NEW_TABLE_OPCODE = Util.OpCodeLookup["NewTable"] +local NEW_REMOTE_TABLE_OPCODE = Util.OpCodeLookup["NewRemoteTable"] +local DESTROY_REMOTE_TABLE_OPCODE = Util.OpCodeLookup["DestroyRemoteTable"] + +local SetTablePacket = Packets.Set +local NewTablePacket = Packets.NewTable +local NewRemoteTablePacket = Packets.NewRemoteTable +local DestroyRemoteTablePacket = Packets.DestroyRemoteTable + +local SendEventStream = Packets.SendEventStream + +local ClientTokens = {} :: {[Player]: {[Token]: boolean}} +local RemoteTables = {} :: {[Token]: { + Data: any, + Listeners: {[Player]: "Connected" | "Disconnected"}, +}} + +local TokenLinks = {} :: {[Table]: Token | false} +local ChangedTables = {} :: {[Token]: boolean} +local TableEventStream = {} :: {[Token]: {Offset: number, Buffer: buffer}} + +local function ResetRemoteTableBuffer(token: Token) + local event_stream = TableEventStream[token] + local event_offset = 0 + local event_buffer = event_stream.Buffer + + event_buffer, event_offset = BufferWriteU16(event_buffer, event_offset, token) + event_stream.Buffer = event_buffer + event_stream.Offset = event_offset +end + +local function DispatchRemoteTableEventStream(token: Token) + local event_stream = TableEventStream[token] + local package = BufferTruncate(event_stream.Buffer, event_stream.Offset) + for listener, state in RemoteTables[token].Listeners do + if state ~= "Connected" then continue end + SendEventStream:FireClient(listener, package) + end + ResetRemoteTableBuffer(token) + ChangedTables[token] = nil +end + +local function PushEvent(token: Token, event: string, ...: any) + ChangedTables[token] = true + local op_code = Util.OpCodeLookup[event] + local event_stream = TableEventStream[token] + local event_buffer = event_stream.Buffer + local event_offset = event_stream.Offset + + local packet = Packets[event] :: any + local event_package = packet:Serialize(...) + + event_buffer, event_offset = BufferWriteU8(event_buffer, event_offset, op_code) + event_buffer, event_offset = BufferAppend(event_buffer, event_offset, event_package) + + event_stream.Buffer = event_buffer + event_stream.Offset = event_offset +end + +local function SetReplicationInfo(token: number, tbl: any, info: any) + if typeof(info) == "table" then + for k, current_info in info do + local current_tbl = tbl[k] + TokenLinks[tbl] = token + SetReplicationInfo(token, current_tbl, current_info) + end + else + assert(typeof(info)=="boolean", "Non-boolean value in selective replication data.") + TokenLinks[tbl] = if info == false then false else token + end +end + +local function RegisterValue(token: number, parent: any, key: any, value: any) + if not TokenLinks[parent] then return end + if typeof(value) == "table" then + if TokenLinks[value] == false then return end + TokenLinks[value] = token + + PushEvent(token, "NewTable", UtilGetTableId(parent), key, UtilGetTableId(value)) + + for k, v in value do + RegisterValue(token, value, k, v) + end + else + PushEvent(token, "Set", UtilGetTableId(parent), key, value) + end +end + +local function UnregisterValue(token: number, value: any) + if value == nil or not TokenLinks[value] then return end + if typeof(value) == "table" then + TokenLinks[value] = nil + for k, v in value do + UnregisterValue(token, v) + end + end +end + +local function GetSnapshotStream(stream: buffer, offset: number, token: number, parent: any, key: any, value: any): (buffer, number) + if not TokenLinks[parent] then return stream, offset end + if typeof(value) == "table" then + if TokenLinks[value] == false then return stream, offset end + TokenLinks[value] = token + + local event_package = NewTablePacket:Serialize(UtilGetTableId(parent), key, UtilGetTableId(value)) + stream, offset = BufferWriteU8(stream, offset, NEW_TABLE_OPCODE) + stream, offset = BufferAppend(stream, offset, event_package) + for k, v in value do + stream, offset = GetSnapshotStream(stream, offset, token, value, k, v) + end + else + local event_package = SetTablePacket:Serialize(UtilGetTableId(parent), key, value) + stream, offset = BufferWriteU8(stream, offset, SET_OPCODE) + stream, offset = BufferAppend(stream, offset, event_package) + end + return stream, offset +end + +local function GetRootSnapshotStream(token: number): buffer + local root = RemoteTables[token].Data + local root_id = UtilGetTableId(root) + local stream = buffer.create(128) + local offset = 0 + + -- Token header + stream, offset = BufferWriteU16(stream, offset, token) + + -- Sync write start + local event_package = NewRemoteTablePacket:Serialize(root_id, false) + stream, offset = BufferWriteU8(stream, offset, NEW_REMOTE_TABLE_OPCODE) + stream, offset = BufferAppend(stream, offset, event_package) + + for k, v in root do + stream, offset = GetSnapshotStream(stream, offset, token, root, k, v) + end + + -- Sync write end + local event_package = NewRemoteTablePacket:Serialize(root_id, true) + stream, offset = BufferWriteU8(stream, offset, NEW_REMOTE_TABLE_OPCODE) + stream, offset = BufferAppend(stream, offset, event_package) + + return BufferTruncate(stream, offset) +end + +local function GetClientTokens(client: Player): {[Token]: boolean} + local client_tokens = ClientTokens[client] + if client_tokens then return client_tokens end + + local client_tokens = {} + ClientTokens[client] = client_tokens + return client_tokens +end + +local function ConnectClient(token: Token, client: Player) + local remote_table = RemoteTables[token] + if not remote_table then return end + + local state = remote_table.Listeners[client] + if not state or state ~= "Disconnected" then return end + + remote_table.Listeners[client] = "Connected" + Packets.SendEventStream:FireClient(client, GetRootSnapshotStream(token)) +end + +local function DisconnectClient(token: Token, client: Player) + local remote_table = RemoteTables[token] + if not remote_table then return end + + local state = remote_table.Listeners[client] + if not state or state ~= "Connected" then return end + + remote_table.Listeners[client] = "Disconnected" + + local stream = buffer.create(128) + local offset = 0 + + -- Token header + stream, offset = BufferWriteU16(stream, offset, token) + + local event_package = DestroyRemoteTablePacket:Serialize(token) + stream, offset = BufferWriteU8(stream, offset, DESTROY_REMOTE_TABLE_OPCODE) + stream, offset = BufferAppend(stream, offset, event_package) + Packets.SendEventStream:FireClient(client, BufferTruncate(stream, offset)) +end + +local function AddClient(token: Token, client: Player) + local remote_table = RemoteTables[token] + assert(remote_table, "[RemoteTable]: AddClient failed. RemoteTable does not exist.") + local state = remote_table.Listeners[client] + if state then return end + + remote_table.Listeners[client] = "Disconnected" + local client_tokens = GetClientTokens(client) + client_tokens[token] = true +end + +local function RemoveClient(token: Token, client: Player) + local remote_table = RemoteTables[token] + assert(remote_table, "[RemoteTable]: RemoveClient failed. RemoteTable does not exist.") + local state = remote_table.Listeners[client] + if not state then return end + + if state == "Connected" then + DisconnectClient(token, client) + end + + remote_table.Listeners[client] = nil + local client_tokens = GetClientTokens(client) + client_tokens[token] = nil +end + +function Server.Set(parent: any, key: Key, value: any) + assert(IsValidValueType(value), "Value can not be an Instance.") + local token = TokenLinks[parent] :: number + local previous_value = parent[key] + if value == previous_value then return end + + UnregisterValue(token, previous_value) + RegisterValue(token, parent, key, value) + parent[key] = value +end + +function Server.Increment(parent: any, key: Key, value: number) + if value == 0 then return end + local token = TokenLinks[parent] :: number + + parent[key] += value + RegisterValue(token, parent, key, parent[key]) +end + +function Server.Insert(tbl: {V}, value: V) + assert(IsValidValueType(value), "Value can not be an Instance.") + + table.insert(tbl, value) + + local token = TokenLinks[tbl] :: number + if token then + PushEvent(token, "Insert", UtilGetTableId(tbl), value) + end +end + +function Server.InsertAt(tbl: {V}, pos: number?, value: V) + local pos = pos or #tbl + 1 + assert(typeof(pos) == "number", "Index must be a number.") + assert(IsValidValueType(value), "Value can not be an Instance.") + + table.insert(tbl, pos, value) + + local token = TokenLinks[tbl] :: number + if token then + PushEvent(token, "InsertAt", UtilGetTableId(tbl), pos, value) + end +end + +function Server.Remove(tbl: {V}, pos: number?): V? + local pos = pos or #tbl + assert(typeof(pos) == "number", "Index must be a number.") + local removed = table.remove(tbl, pos) + if removed == nil then return nil end + + local token = TokenLinks[tbl] :: number + if token then + PushEvent(token, "Remove", UtilGetTableId(tbl), pos) + end + return removed +end + +function Server.SwapRemove(tbl: {V}, pos: number?): V? + local pos = pos or #tbl + assert(typeof(pos) == "number", "Index must be a number.") + local removed = Util.SwapRemove(tbl, pos) + if removed == nil then return nil end + + local token = TokenLinks[tbl] :: number + if token then + PushEvent(token, "SwapRemove", UtilGetTableId(tbl), pos) + end + return removed +end + +function Server.Clear(tbl: any) + table.clear(tbl) + local token = TokenLinks[tbl] + if token then + PushEvent(token :: number, "Clear", UtilGetTableId(tbl)) + end +end + +function Server.Create(token_name: string, template: T, selective_replication: any): T + assert(not TokenRegistry.IsTokenRegistered(token_name), "RemoteTable with this id already exists.") + local token = TokenRegistry.Register(token_name) + local root = DeepCopy(template) :: any + TableEventStream[token] = { + Buffer = buffer.create(128), + Offset = 0, + } + RemoteTables[token] = { + Data = root, + Listeners = {}, + } + TokenLinks[root] = token + if not selective_replication then + selective_replication = {} + end + for k, v in root :: any do + if selective_replication[k] == nil and typeof(root[k]) == "table" then + selective_replication[k] = true + end + end + SetReplicationInfo(token, root, selective_replication) + for k, v in root :: any do + RegisterValue(token, root, k, v) + end + ResetRemoteTableBuffer(token) + return root +end + +function Server.Get(token_name: string): any? + if not TokenRegistry.IsTokenRegistered(token_name) then return nil end + + local token = TokenRegistry.GetToken(token_name) + if not token then return nil end + + local remote_table = RemoteTables[token] + if not remote_table then return nil end + + return remote_table.Data +end + +function Server.Destroy(token_name: string) + if not TokenRegistry.IsTokenRegistered(token_name) then return end + + local token = TokenRegistry.GetToken(token_name) + local remote_table = RemoteTables[token] + local root = remote_table.Data + + PushEvent(token, "DestroyRemoteTable", token) + DispatchRemoteTableEventStream(token) + + for client, state in remote_table.Listeners do + RemoveClient(token, client) + end + + UnregisterValue(token, root) + + RemoteTables[token] = nil + + TokenLinks[root] = nil + ChangedTables[token] = nil + TableEventStream[token] = nil + + TokenRegistry.Unregister(token_name) +end + +function Server.AddClient(token_name: TokenName, clients: Player | {Player}) + assert(TokenRegistry.IsTokenRegistered(token_name), "Token not registered.") + local token = TokenRegistry.GetToken(token_name) + if typeof(clients) == "table" then + for _, client in clients do + AddClient(token, client) + end + else + AddClient(token, clients) + end +end + +function Server.RemoveClient(token_name: TokenName, clients: Player | {Player}) + assert(TokenRegistry.IsTokenRegistered(token_name), "Token not registered.") + local token = TokenRegistry.GetToken(token_name) + if typeof(clients) == "table" then + for _, client in clients do + RemoveClient(token, client) + end + else + RemoveClient(token, clients) + end +end + +local DispatchLoop = task.spawn(function() + while true do + coroutine.yield() + for token, changed in ChangedTables do + if not changed then continue end + DispatchRemoteTableEventStream(token) + end + end +end) +RunService.Heartbeat:Connect(function() task.defer(DispatchLoop) end) + +Packets.ConnectionRequest.OnServerEvent:Connect(function(client: Player, token: number) + ConnectClient(token, client) +end) + +Players.PlayerRemoving:Connect(function(client: Player, reason: Enum.PlayerExitReason) + local tokens = GetClientTokens(client) + if not tokens then return end + + for token, _ in tokens do + RemoveClient(token, client) + end + ClientTokens[client] = nil +end) + +return Server \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau new file mode 100644 index 0000000..7ca590c --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau @@ -0,0 +1,167 @@ +--[[ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates + the terms and conditions of version 3 of the GNU General Public + License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser + General Public License, and the "GNU GPL" refers to version 3 of the GNU + General Public License. + + "The Library" refers to a covered work governed by this License, + other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided + by the Library, but which is not otherwise based on the Library. + Defining a subclass of a class defined by the Library is deemed a mode + of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an + Application with the Library. The particular version of the Library + with which the Combined Work was made is also called the "Linked + Version". + + The "Minimal Corresponding Source" for a Combined Work means the + Corresponding Source for the Combined Work, excluding any source code + for portions of the Combined Work that, considered in isolation, are + based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the + object code and/or source code for the Application, including any data + and utility programs needed for reproducing the Combined Work from the + Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License + without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a + facility refers to a function or data to be supplied by an Application + that uses the facility (other than as an argument passed when the + facility is invoked), then you may convey a copy of the modified + version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from + a header file that is part of the Library. You may convey such object + code under terms of your choice, provided that, if the incorporated + material is not limited to numerical parameters, data structure + layouts and accessors, or small macros, inline functions and templates + (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, + taken together, effectively do not restrict modification of the + portions of the Library contained in the Combined Work and reverse + engineering for debugging such modifications, if you also do each of + the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the + Library side by side in a single library together with other library + facilities that are not Applications and are not covered by this + License, and convey such a combined library under terms of your + choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions + of the GNU Lesser General Public License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the + Library as you received it specifies that a certain numbered version + of the GNU Lesser General Public License "or any later version" + applies to it, you have the option of following the terms and + conditions either of that published version or of any later version + published by the Free Software Foundation. If the Library as you + received it does not specify a version number of the GNU Lesser + General Public License, you may choose any version of the GNU Lesser + General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide + whether future versions of the GNU Lesser General Public License shall + apply, that proxy's public statement of acceptance of any version is + permanent authorization for you to choose that version for the + Library. +]] \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau new file mode 100644 index 0000000..c582c2b --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau @@ -0,0 +1,101 @@ +--!strict + + +-- Requires +local Task = require(script.Parent.Task) + + +-- Types +export type Signal = { + Type: "Signal", + Previous: Connection, + Next: Connection, + Fire: (self: Signal, A...) -> (), + Connect: (self: Signal, func: (A...) -> ()) -> Connection, + Once: (self: Signal, func: (A...) -> ()) -> Connection, + Wait: (self: Signal) -> A..., +} + +export type Connection = { + Type: "Connection", + Previous: Connection, + Next: Connection, + Once: boolean, + Function: (player: Player, A...) -> (), + Thread: thread, + Disconnect: (self: Connection) -> (), +} + + +-- Varables +local Signal = {} :: Signal<...any> +local Connection = {} :: Connection<...any> + + +-- Constructor +local function Constructor() + local signal = (setmetatable({}, Signal) :: any) :: Signal + signal.Previous = signal :: any + signal.Next = signal :: any + return signal +end + + +-- Signal +Signal["__index"] = Signal +Signal.Type = "Signal" + +function Signal:Connect(func) + local connection = (setmetatable({}, Connection) :: any) :: Connection + connection.Previous = self.Previous + connection.Next = self :: any + connection.Once = false + connection.Function = func + self.Previous.Next = connection + self.Previous = connection + return connection +end + +function Signal:Once(func) + local connection = (setmetatable({}, Connection) :: any) :: Connection + connection.Previous = self.Previous + connection.Next = self :: any + connection.Once = true + connection.Function = func + self.Previous.Next = connection + self.Previous = connection + return connection +end + +function Signal:Wait() + local connection = (setmetatable({}, Connection) :: any) :: Connection + connection.Previous = self.Previous + connection.Next = self :: any + connection.Once = true + connection.Thread = coroutine.running() + self.Previous.Next = connection + self.Previous = connection + return coroutine.yield() +end + +function Signal:Fire(...) + local connection = self.Next + while connection.Type == "Connection" do + if connection.Function then Task:Defer(connection.Function, ...) else task.defer(connection.Thread, ...) end + if connection.Once then connection.Previous.Next = connection.Next connection.Next.Previous = connection.Previous end + connection = connection.Next + end +end + + +-- Connection +Connection["__index"] = Connection +Connection.Type = "Connection" + +function Connection:Disconnect() + self.Previous.Next = self.Next + self.Next.Previous = self.Previous +end + + +return Constructor \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau new file mode 100644 index 0000000..5731b1a --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau @@ -0,0 +1,46 @@ +--!strict + + +-- Types +export type Task = { + Type: "Task", + Spawn: (self: Task, func: (...any) -> (), ...any) -> thread, + Defer: (self: Task, func: (...any) -> (), ...any) -> thread, + Delay: (self: Task, duration: number, func: (...any) -> (), ...any) -> thread, +} + + +-- Varables +local Call, Thread +local Task = {} :: Task +local threads = {} :: {thread} + + +-- Task +Task.Type = "Task" + +function Task:Spawn(func, ...) + return task.spawn(table.remove(threads) or task.spawn(Thread), func, ...) +end + +function Task:Defer(func, ...) + return task.defer(table.remove(threads) or task.spawn(Thread), func, ...) +end + +function Task:Delay(duration, func, ...) + return task.delay(duration, table.remove(threads) or task.spawn(Thread), func, ...) +end + + +-- Functions +function Call(func: (...any) -> (), ...) + func(...) + table.insert(threads, coroutine.running()) +end + +function Thread() + while true do Call(coroutine.yield()) end +end + + +return Task \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau new file mode 100644 index 0000000..6e67cad --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau @@ -0,0 +1,6 @@ +return {[0] = -- Recommended character array lengths: 2, 4, 8, 16, 32, 64, 128, 256 + " ", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", + "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", + "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", +} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau new file mode 100644 index 0000000..dc53876 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau @@ -0,0 +1,11 @@ +return { -- Add any enum [Max: 255] + Enum.AccessoryType, + Enum.Axis, + Enum.BodyPart, + Enum.BodyPartR15, + Enum.EasingDirection, + Enum.EasingStyle, + Enum.KeyCode, + Enum.Material, + Enum.NormalId, +} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau new file mode 100644 index 0000000..9e1a06b --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau @@ -0,0 +1,8 @@ +return { + "DataStore Failed To Load", + "Another Static String", + math.pi, + 123456789, + Vector3.new(1, 2, 3), + "You can have upto 255 static values of any type" +} diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau new file mode 100644 index 0000000..9e1a06b --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau @@ -0,0 +1,8 @@ +return { + "DataStore Failed To Load", + "Another Static String", + math.pi, + 123456789, + Vector3.new(1, 2, 3), + "You can have upto 255 static values of any type" +} diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau new file mode 100644 index 0000000..9e1a06b --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau @@ -0,0 +1,8 @@ +return { + "DataStore Failed To Load", + "Another Static String", + math.pi, + 123456789, + Vector3.new(1, 2, 3), + "You can have upto 255 static values of any type" +} diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau new file mode 100644 index 0000000..4ba7c66 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau @@ -0,0 +1,705 @@ +--!strict +--!optimize 2 + +--[[ + S8 Minimum: -128 Maximum: 127 + S16 Minimum: -32768 Maximum: 32767 + S24 Minimum: -8388608 Maximum: 8388607 + S32 Minimum: -2147483648 Maximum: 2147483647 + + U8 Minimum: 0 Maximum: 255 + U16 Minimum: 0 Maximum: 65535 + U24 Minimum: 0 Maximum: 16777215 + U32 Minimum: 0 Maximum: 4294967295 + + F16 ±2048 [65520] + F24 ±262144 [4294959104] + F32 ±16777216 [170141183460469231731687303715884105728] + F64 ±9007199254740992 [huge] +]] + + +-- Types +export type Cursor = { + Buffer: buffer, + BufferLength: number, + BufferOffset: number, + Instances: {Instance}, + InstancesOffset: number, +} + + +-- Varables +local activeCursor : Cursor +local activeBuffer : buffer +local bufferLength : number +local bufferOffset : number +local instances : {Instance} +local instancesOffset : number +local types = {} +local reads = {} +local writes = {} +local anyReads = {} :: {[any]: () -> any} +local anyWrites = {} :: {[any]: (any) -> ()} + + +-- Functions +local function Allocate(bytes: number) + local targetLength = bufferOffset + bytes + if bufferLength < targetLength then + while bufferLength < targetLength do bufferLength *= 2 end + local newBuffer = buffer.create(bufferLength) + buffer.copy(newBuffer, 0, activeBuffer, 0, bufferOffset) + activeCursor.Buffer = newBuffer + activeBuffer = newBuffer + end +end + +local function ReadS8(): number local value = buffer.readi8(activeBuffer, bufferOffset) bufferOffset += 1 return value end +local function WriteS8(value: number) buffer.writei8(activeBuffer, bufferOffset, value) bufferOffset += 1 end +local function ReadS16(): number local value = buffer.readi16(activeBuffer, bufferOffset) bufferOffset += 2 return value end +local function WriteS16(value: number) buffer.writei16(activeBuffer, bufferOffset, value) bufferOffset += 2 end +local function ReadS24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) - 8388608 bufferOffset += 3 return value end +local function WriteS24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value + 8388608) bufferOffset += 3 end +local function ReadS32(): number local value = buffer.readi32(activeBuffer, bufferOffset) bufferOffset += 4 return value end +local function WriteS32(value: number) buffer.writei32(activeBuffer, bufferOffset, value) bufferOffset += 4 end +local function ReadU8(): number local value = buffer.readu8(activeBuffer, bufferOffset) bufferOffset += 1 return value end +local function WriteU8(value: number) buffer.writeu8(activeBuffer, bufferOffset, value) bufferOffset += 1 end +local function ReadU16(): number local value = buffer.readu16(activeBuffer, bufferOffset) bufferOffset += 2 return value end +local function WriteU16(value: number) buffer.writeu16(activeBuffer, bufferOffset, value) bufferOffset += 2 end +local function ReadU24(): number local value = buffer.readbits(activeBuffer, bufferOffset * 8, 24) bufferOffset += 3 return value end +local function WriteU24(value: number) buffer.writebits(activeBuffer, bufferOffset * 8, 24, value) bufferOffset += 3 end +local function ReadU32(): number local value = buffer.readu32(activeBuffer, bufferOffset) bufferOffset += 4 return value end +local function WriteU32(value: number) buffer.writeu32(activeBuffer, bufferOffset, value) bufferOffset += 4 end +local function ReadF32(): number local value = buffer.readf32(activeBuffer, bufferOffset) bufferOffset += 4 return value end +local function WriteF32(value: number) buffer.writef32(activeBuffer, bufferOffset, value) bufferOffset += 4 end +local function ReadF64(): number local value = buffer.readf64(activeBuffer, bufferOffset) bufferOffset += 8 return value end +local function WriteF64(value: number) buffer.writef64(activeBuffer, bufferOffset, value) bufferOffset += 8 end +local function ReadString(length: number) local value = buffer.readstring(activeBuffer, bufferOffset, length) bufferOffset += length return value end +local function WriteString(value: string) buffer.writestring(activeBuffer, bufferOffset, value) bufferOffset += #value end +local function ReadBuffer(length: number) local value = buffer.create(length) buffer.copy(value, 0, activeBuffer, bufferOffset, length) bufferOffset += length return value end +local function WriteBuffer(value: buffer) buffer.copy(activeBuffer, bufferOffset, value) bufferOffset += buffer.len(value) end +local function ReadInstance() instancesOffset += 1 return instances[instancesOffset] end +local function WriteInstance(value) instancesOffset += 1 instances[instancesOffset] = value end + +local function ReadF16(): number + local bitOffset = bufferOffset * 8 + bufferOffset += 2 + local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 10) + local exponent = buffer.readbits(activeBuffer, bitOffset + 10, 5) + local sign = buffer.readbits(activeBuffer, bitOffset + 15, 1) + if mantissa == 0b0000000000 then + if exponent == 0b00000 then return 0 end + if exponent == 0b11111 then return if sign == 0 then math.huge else -math.huge end + elseif exponent == 0b11111 then return 0/0 end + if sign == 0 then + return (mantissa / 1024 + 1) * 2 ^ (exponent - 15) + else + return -(mantissa / 1024 + 1) * 2 ^ (exponent - 15) + end +end +local function WriteF16(value: number) + local bitOffset = bufferOffset * 8 + bufferOffset += 2 + if value == 0 then + buffer.writebits(activeBuffer, bitOffset, 16, 0b0_00000_0000000000) + elseif value >= 65520 then + buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000000) + elseif value <= -65520 then + buffer.writebits(activeBuffer, bitOffset, 16, 0b1_11111_0000000000) + elseif value ~= value then + buffer.writebits(activeBuffer, bitOffset, 16, 0b0_11111_0000000001) + else + local sign = 0 + if value < 0 then sign = 1 value = -value end + local mantissa, exponent = math.frexp(value) + buffer.writebits(activeBuffer, bitOffset + 0, 10, mantissa * 2048 - 1023.5) + buffer.writebits(activeBuffer, bitOffset + 10, 5, exponent + 14) + buffer.writebits(activeBuffer, bitOffset + 15, 1, sign) + end +end + +local function ReadF24(): number + local bitOffset = bufferOffset * 8 + bufferOffset += 3 + local mantissa = buffer.readbits(activeBuffer, bitOffset + 0, 17) + local exponent = buffer.readbits(activeBuffer, bitOffset + 17, 6) + local sign = buffer.readbits(activeBuffer, bitOffset + 23, 1) + if mantissa == 0b00000000000000000 then + if exponent == 0b000000 then return 0 end + if exponent == 0b111111 then return if sign == 0 then math.huge else -math.huge end + elseif exponent == 0b111111 then return 0/0 end + if sign == 0 then + return (mantissa / 131072 + 1) * 2 ^ (exponent - 31) + else + return -(mantissa / 131072 + 1) * 2 ^ (exponent - 31) + end +end +local function WriteF24(value: number) + local bitOffset = bufferOffset * 8 + bufferOffset += 3 + if value == 0 then + buffer.writebits(activeBuffer, bitOffset, 24, 0b0_000000_00000000000000000) + elseif value >= 4294959104 then + buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000000) + elseif value <= -4294959104 then + buffer.writebits(activeBuffer, bitOffset, 24, 0b1_111111_00000000000000000) + elseif value ~= value then + buffer.writebits(activeBuffer, bitOffset, 24, 0b0_111111_00000000000000001) + else + local sign = 0 + if value < 0 then sign = 1 value = -value end + local mantissa, exponent = math.frexp(value) + buffer.writebits(activeBuffer, bitOffset + 0, 17, mantissa * 262144 - 131071.5) + buffer.writebits(activeBuffer, bitOffset + 17, 6, exponent + 30) + buffer.writebits(activeBuffer, bitOffset + 23, 1, sign) + end +end + + +-- Types +types.Any = "Any" :: any +reads.Any = function() return anyReads[ReadU8()]() end +writes.Any = function(value: any) anyWrites[typeof(value)](value) end + +types.Nil = ("Nil" :: any) :: nil +reads.Nil = function() return nil end +writes.Nil = function(value: nil) end + +types.NumberS8 = ("NumberS8" :: any) :: number +reads.NumberS8 = function() return ReadS8() end +writes.NumberS8 = function(value: number) Allocate(1) WriteS8(value) end + +types.NumberS16 = ("NumberS16" :: any) :: number +reads.NumberS16 = function() return ReadS16() end +writes.NumberS16 = function(value: number) Allocate(2) WriteS16(value) end + +types.NumberS24 = ("NumberS24" :: any) :: number +reads.NumberS24 = function() return ReadS24() end +writes.NumberS24 = function(value: number) Allocate(3) WriteS24(value) end + +types.NumberS32 = ("NumberS32" :: any) :: number +reads.NumberS32 = function() return ReadS32() end +writes.NumberS32 = function(value: number) Allocate(4) WriteS32(value) end + +types.NumberU8 = ("NumberU8" :: any) :: number +reads.NumberU8 = function() return ReadU8() end +writes.NumberU8 = function(value: number) Allocate(1) WriteU8(value) end + +types.NumberU16 = ("NumberU16" :: any) :: number +reads.NumberU16 = function() return ReadU16() end +writes.NumberU16 = function(value: number) Allocate(2) WriteU16(value) end + +types.NumberU24 = ("NumberU24" :: any) :: number +reads.NumberU24 = function() return ReadU24() end +writes.NumberU24 = function(value: number) Allocate(3) WriteU24(value) end + +types.NumberU32 = ("NumberU32" :: any) :: number +reads.NumberU32 = function() return ReadU32() end +writes.NumberU32 = function(value: number) Allocate(4) WriteU32(value) end + +types.NumberF16 = ("NumberF16" :: any) :: number +reads.NumberF16 = function() return ReadF16() end +writes.NumberF16 = function(value: number) Allocate(2) WriteF16(value) end + +types.NumberF24 = ("NumberF24" :: any) :: number +reads.NumberF24 = function() return ReadF24() end +writes.NumberF24 = function(value: number) Allocate(3) WriteF24(value) end + +types.NumberF32 = ("NumberF32" :: any) :: number +reads.NumberF32 = function() return ReadF32() end +writes.NumberF32 = function(value: number) Allocate(4) WriteF32(value) end + +types.NumberF64 = ("NumberF64" :: any) :: number +reads.NumberF64 = function() return ReadF64() end +writes.NumberF64 = function(value: number) Allocate(8) WriteF64(value) end + +types.String = ("String" :: any) :: string +reads.String = function() return ReadString(ReadU8()) end +writes.String = function(value: string) local length = #value Allocate(1 + length) WriteU8(length) WriteString(value) end + +types.StringLong = ("StringLong" :: any) :: string +reads.StringLong = function() return ReadString(ReadU16()) end +writes.StringLong = function(value: string) local length = #value Allocate(2 + length) WriteU16(length) WriteString(value) end + +types.Buffer = ("Buffer" :: any) :: buffer +reads.Buffer = function() return ReadBuffer(ReadU8()) end +writes.Buffer = function(value: buffer) local length = buffer.len(value) Allocate(1 + length) WriteU8(length) WriteBuffer(value) end + +types.BufferLong = ("BufferLong" :: any) :: buffer +reads.BufferLong = function() return ReadBuffer(ReadU16()) end +writes.BufferLong = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU16(length) WriteBuffer(value) end + +types.Instance = ("Instance" :: any) :: Instance +reads.Instance = function() return ReadInstance() end +writes.Instance = function(value: Instance) WriteInstance(value) end + +types.Boolean8 = ("Boolean8" :: any) :: boolean +reads.Boolean8 = function() return ReadU8() == 1 end +writes.Boolean8 = function(value: boolean) Allocate(1) WriteU8(if value then 1 else 0) end + +types.NumberRange = ("NumberRange" :: any) :: NumberRange +reads.NumberRange = function() return NumberRange.new(ReadF32(), ReadF32()) end +writes.NumberRange = function(value: NumberRange) Allocate(8) WriteF32(value.Min) WriteF32(value.Max) end + +types.BrickColor = ("BrickColor" :: any) :: BrickColor +reads.BrickColor = function() return BrickColor.new(ReadU16()) end +writes.BrickColor = function(value: BrickColor) Allocate(2) WriteU16(value.Number) end + +types.Color3 = ("Color3" :: any) :: Color3 +reads.Color3 = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end +writes.Color3 = function(value: Color3) Allocate(3) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end + +types.UDim = ("UDim" :: any) :: UDim +reads.UDim = function() return UDim.new(ReadS16() / 1000, ReadS16()) end +writes.UDim = function(value: UDim) Allocate(4) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end + +types.UDim2 = ("UDim2" :: any) :: UDim2 +reads.UDim2 = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end +writes.UDim2 = function(value: UDim2) Allocate(8) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end + +types.Rect = ("Rect" :: any) :: Rect +reads.Rect = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end +writes.Rect = function(value: Rect) Allocate(16) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end + +types.Vector2S16 = ("Vector2S16" :: any) :: Vector2 +reads.Vector2S16 = function() return Vector2.new(ReadS16(), ReadS16()) end +writes.Vector2S16 = function(value: Vector2) Allocate(4) WriteS16(value.X) WriteS16(value.Y) end + +types.Vector2F24 = ("Vector2F24" :: any) :: Vector2 +reads.Vector2F24 = function() return Vector2.new(ReadF24(), ReadF24()) end +writes.Vector2F24 = function(value: Vector2) Allocate(6) WriteF24(value.X) WriteF24(value.Y) end + +types.Vector2F32 = ("Vector2F32" :: any) :: Vector2 +reads.Vector2F32 = function() return Vector2.new(ReadF32(), ReadF32()) end +writes.Vector2F32 = function(value: Vector2) Allocate(8) WriteF32(value.X) WriteF32(value.Y) end + +types.Vector3S16 = ("Vector3S16" :: any) :: Vector3 +reads.Vector3S16 = function() return Vector3.new(ReadS16(), ReadS16(), ReadS16()) end +writes.Vector3S16 = function(value: Vector3) Allocate(6) WriteS16(value.X) WriteS16(value.Y) WriteS16(value.Z) end + +types.Vector3F24 = ("Vector3F24" :: any) :: Vector3 +reads.Vector3F24 = function() return Vector3.new(ReadF24(), ReadF24(), ReadF24()) end +writes.Vector3F24 = function(value: Vector3) Allocate(9) WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z) end + +types.Vector3F32 = ("Vector3F32" :: any) :: Vector3 +reads.Vector3F32 = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end +writes.Vector3F32 = function(value: Vector3) Allocate(12) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end + +types.NumberU4 = ("NumberU4" :: any) :: {number} +reads.NumberU4 = function() + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + return { + buffer.readbits(activeBuffer, bitOffset + 0, 4), + buffer.readbits(activeBuffer, bitOffset + 4, 4) + } +end +writes.NumberU4 = function(value: {number}) + Allocate(1) + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + buffer.writebits(activeBuffer, bitOffset + 0, 4, value[1]) + buffer.writebits(activeBuffer, bitOffset + 4, 4, value[2]) +end + +types.BooleanNumber = ("BooleanNumber" :: any) :: {Boolean: boolean, Number: number} +reads.BooleanNumber = function() + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + return { + Boolean = buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1, + Number = buffer.readbits(activeBuffer, bitOffset + 1, 7), + } +end +writes.BooleanNumber = function(value: {Boolean: boolean, Number: number}) + Allocate(1) + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + buffer.writebits(activeBuffer, bitOffset + 0, 1, if value.Boolean then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 1, 7, value.Number) +end + +types.Boolean1 = ("Boolean1" :: any) :: {boolean} +reads.Boolean1 = function() + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + return { + buffer.readbits(activeBuffer, bitOffset + 0, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 1, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 2, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 3, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 4, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 5, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 6, 1) == 1, + buffer.readbits(activeBuffer, bitOffset + 7, 1) == 1, + } +end +writes.Boolean1 = function(value: {boolean}) + Allocate(1) + local bitOffset = bufferOffset * 8 + bufferOffset += 1 + buffer.writebits(activeBuffer, bitOffset + 0, 1, if value[1] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 1, 1, if value[2] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 2, 1, if value[3] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 3, 1, if value[4] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 4, 1, if value[5] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 5, 1, if value[6] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 6, 1, if value[7] then 1 else 0) + buffer.writebits(activeBuffer, bitOffset + 7, 1, if value[8] then 1 else 0) +end + +types.CFrameF24U8 = ("CFrameF24U8" :: any) :: CFrame +reads.CFrameF24U8 = function() + return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331) + + Vector3.new(ReadF24(), ReadF24(), ReadF24()) +end +writes.CFrameF24U8 = function(value: CFrame) + local rx, ry, rz = value:ToEulerAnglesXYZ() + Allocate(12) + WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5) + WriteF24(value.X) WriteF24(value.Y) WriteF24(value.Z) +end + +types.CFrameF32U8 = ("CFrameF32U8" :: any) :: CFrame +reads.CFrameF32U8 = function() + return CFrame.fromEulerAnglesXYZ(ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331, ReadU8() / 40.58451048843331) + + Vector3.new(ReadF32(), ReadF32(), ReadF32()) +end +writes.CFrameF32U8 = function(value: CFrame) + local rx, ry, rz = value:ToEulerAnglesXYZ() + Allocate(15) + WriteU8(rx * 40.58451048843331 + 0.5) WriteU8(ry * 40.58451048843331 + 0.5) WriteU8(rz * 40.58451048843331 + 0.5) + WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) +end + +types.CFrameF32U16 = ("CFrameF32U16" :: any) :: CFrame +reads.CFrameF32U16 = function() + return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361) + + Vector3.new(ReadF32(), ReadF32(), ReadF32()) +end +writes.CFrameF32U16 = function(value: CFrame) + local rx, ry, rz = value:ToEulerAnglesXYZ() + Allocate(18) + WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5) + WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) +end + +types.Region3 = ("Region3" :: any) :: Region3 +reads.Region3 = function() + return Region3.new( + Vector3.new(ReadF32(), ReadF32(), ReadF32()), + Vector3.new(ReadF32(), ReadF32(), ReadF32()) + ) +end +writes.Region3 = function(value: Region3) + local halfSize = value.Size / 2 + local minimum = value.CFrame.Position - halfSize + local maximum = value.CFrame.Position + halfSize + Allocate(24) + WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z) + WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z) +end + +types.NumberSequence = ("NumberSequence" :: any) :: NumberSequence +reads.NumberSequence = function() + local length = ReadU8() + local keypoints = table.create(length) + for index = 1, length do + table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255)) + end + return NumberSequence.new(keypoints) +end +writes.NumberSequence = function(value: NumberSequence) + local length = #value.Keypoints + Allocate(1 + length * 3) + WriteU8(length) + for index, keypoint in value.Keypoints do + WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5) + end +end + +types.ColorSequence = ("ColorSequence" :: any) :: ColorSequence +reads.ColorSequence = function() + local length = ReadU8() + local keypoints = table.create(length) + for index = 1, length do + table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()))) + end + return ColorSequence.new(keypoints) +end +writes.ColorSequence = function(value: ColorSequence) + local length = #value.Keypoints + Allocate(1 + length * 4) + WriteU8(length) + for index, keypoint in value.Keypoints do + WriteU8(keypoint.Time * 255 + 0.5) + WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5) + end +end + +local characterIndices = {} +local characters = require(script.Characters) +for index, value in characters do characterIndices[value] = index end +local characterBits = math.ceil(math.log(#characters + 1, 2)) +local characterBytes = characterBits / 8 +types.Characters = ("Characters" :: any) :: string +reads.Characters = function() + local length = ReadU8() + local characterArray = table.create(length) + local bitOffset = bufferOffset * 8 + bufferOffset += math.ceil(length * characterBytes) + for index = 1, length do + table.insert(characterArray, characters[buffer.readbits(activeBuffer, bitOffset, characterBits)]) + bitOffset += characterBits + end + return table.concat(characterArray) +end +writes.Characters = function(value: string) + local length = #value + local bytes = math.ceil(length * characterBytes) + Allocate(1 + bytes) + WriteU8(length) + local bitOffset = bufferOffset * 8 + for index = 1, length do + buffer.writebits(activeBuffer, bitOffset, characterBits, characterIndices[value:sub(index, index)]) + bitOffset += characterBits + end + bufferOffset += bytes +end + +local enumIndices = {} +local enums = require(script.Enums) +for index, static in enums do enumIndices[static] = index end +types.EnumItem = ("EnumItem" :: any) :: EnumItem +reads.EnumItem = function() return enums[ReadU8()]:FromValue(ReadU16()) end +writes.EnumItem = function(value: EnumItem) Allocate(3) WriteU8(enumIndices[value.EnumType]) WriteU16(value.Value) end + +local staticIndices = {} +local statics = require(script.Static1) +for index, static in statics do staticIndices[static] = index end +types.Static1 = ("Static1" :: any) :: any +reads.Static1 = function() return statics[ReadU8()] end +writes.Static1 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end + +local staticIndices = {} +local statics = require(script.Static2) +for index, static in statics do staticIndices[static] = index end +types.Static2 = ("Static2" :: any) :: any +reads.Static2 = function() return statics[ReadU8()] end +writes.Static2 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end + +local staticIndices = {} +local statics = require(script.Static3) +for index, static in statics do staticIndices[static] = index end +types.Static3 = ("Static3" :: any) :: any +reads.Static3 = function() return statics[ReadU8()] end +writes.Static3 = function(value: any) Allocate(1) WriteU8(staticIndices[value] or 0) end + + +-- Any Types +anyReads[0] = function() return nil end +anyWrites["nil"] = function(value: nil) Allocate(1) WriteU8(0) end + +anyReads[1] = function() return -ReadU8() end +anyReads[2] = function() return -ReadU16() end +anyReads[3] = function() return -ReadU24() end +anyReads[4] = function() return -ReadU32() end +anyReads[5] = function() return ReadU8() end +anyReads[6] = function() return ReadU16() end +anyReads[7] = function() return ReadU24() end +anyReads[8] = function() return ReadU32() end +anyReads[9] = function() return ReadF32() end +anyReads[10] = function() return ReadF64() end +anyWrites.number = function(value: number) + if value % 1 == 0 then + if value < 0 then + if value > -256 then + Allocate(2) WriteU8(1) WriteU8(-value) + elseif value > -65536 then + Allocate(3) WriteU8(2) WriteU16(-value) + elseif value > -16777216 then + Allocate(4) WriteU8(3) WriteU24(-value) + elseif value > -4294967296 then + Allocate(5) WriteU8(4) WriteU32(-value) + else + Allocate(9) WriteU8(10) WriteF64(value) + end + else + if value < 256 then + Allocate(2) WriteU8(5) WriteU8(value) + elseif value < 65536 then + Allocate(3) WriteU8(6) WriteU16(value) + elseif value < 16777216 then + Allocate(4) WriteU8(7) WriteU24(value) + elseif value < 4294967296 then + Allocate(5) WriteU8(8) WriteU32(value) + else + Allocate(9) WriteU8(10) WriteF64(value) + end + end + elseif value > -1048576 and value < 1048576 then + Allocate(5) WriteU8(9) WriteF32(value) + else + Allocate(9) WriteU8(10) WriteF64(value) + end +end + +anyReads[11] = function() return ReadString(ReadU8()) end +anyWrites.string = function(value: string) local length = #value Allocate(2 + length) WriteU8(11) WriteU8(length) WriteString(value) end + +anyReads[12] = function() return ReadBuffer(ReadU8()) end +anyWrites.buffer = function(value: buffer) local length = buffer.len(value) Allocate(2 + length) WriteU8(12) WriteU8(length) WriteBuffer(value) end + +anyReads[13] = function() return ReadInstance() end +anyWrites.Instance = function(value: Instance) Allocate(1) WriteU8(13) WriteInstance(value) end + +anyReads[14] = function() return ReadU8() == 1 end +anyWrites.boolean = function(value: boolean) Allocate(2) WriteU8(14) WriteU8(if value then 1 else 0) end + +anyReads[15] = function() return NumberRange.new(ReadF32(), ReadF32()) end +anyWrites.NumberRange = function(value: NumberRange) Allocate(9) WriteU8(15) WriteF32(value.Min) WriteF32(value.Max) end + +anyReads[16] = function() return BrickColor.new(ReadU16()) end +anyWrites.BrickColor = function(value: BrickColor) Allocate(3) WriteU8(16) WriteU16(value.Number) end + +anyReads[17] = function() return Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()) end +anyWrites.Color3 = function(value: Color3) Allocate(4) WriteU8(17) WriteU8(value.R * 255 + 0.5) WriteU8(value.G * 255 + 0.5) WriteU8(value.B * 255 + 0.5) end + +anyReads[18] = function() return UDim.new(ReadS16() / 1000, ReadS16()) end +anyWrites.UDim = function(value: UDim) Allocate(5) WriteU8(18) WriteS16(value.Scale * 1000) WriteS16(value.Offset) end + +anyReads[19] = function() return UDim2.new(ReadS16() / 1000, ReadS16(), ReadS16() / 1000, ReadS16()) end +anyWrites.UDim2 = function(value: UDim2) Allocate(9) WriteU8(19) WriteS16(value.X.Scale * 1000) WriteS16(value.X.Offset) WriteS16(value.Y.Scale * 1000) WriteS16(value.Y.Offset) end + +anyReads[20] = function() return Rect.new(ReadF32(), ReadF32(), ReadF32(), ReadF32()) end +anyWrites.Rect = function(value: Rect) Allocate(17) WriteU8(20) WriteF32(value.Min.X) WriteF32(value.Min.Y) WriteF32(value.Max.X) WriteF32(value.Max.Y) end + +anyReads[21] = function() return Vector2.new(ReadF32(), ReadF32()) end +anyWrites.Vector2 = function(value: Vector2) Allocate(9) WriteU8(21) WriteF32(value.X) WriteF32(value.Y) end + +anyReads[22] = function() return Vector3.new(ReadF32(), ReadF32(), ReadF32()) end +anyWrites.Vector3 = function(value: Vector3) Allocate(13) WriteU8(22) WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) end + +anyReads[23] = function() + return CFrame.fromEulerAnglesXYZ(ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361, ReadU16() / 10430.219195527361) + + Vector3.new(ReadF32(), ReadF32(), ReadF32()) +end +anyWrites.CFrame = function(value: CFrame) + local rx, ry, rz = value:ToEulerAnglesXYZ() + Allocate(19) + WriteU8(23) + WriteU16(rx * 10430.219195527361 + 0.5) WriteU16(ry * 10430.219195527361 + 0.5) WriteU16(rz * 10430.219195527361 + 0.5) + WriteF32(value.X) WriteF32(value.Y) WriteF32(value.Z) +end + +anyReads[24] = function() + return Region3.new( + Vector3.new(ReadF32(), ReadF32(), ReadF32()), + Vector3.new(ReadF32(), ReadF32(), ReadF32()) + ) +end +anyWrites.Region3 = function(value: Region3) + local halfSize = value.Size / 2 + local minimum = value.CFrame.Position - halfSize + local maximum = value.CFrame.Position + halfSize + Allocate(25) + WriteU8(24) + WriteF32(minimum.X) WriteF32(minimum.Y) WriteF32(minimum.Z) + WriteF32(maximum.X) WriteF32(maximum.Y) WriteF32(maximum.Z) +end + +anyReads[25] = function() + local length = ReadU8() + local keypoints = table.create(length) + for index = 1, length do + table.insert(keypoints, NumberSequenceKeypoint.new(ReadU8() / 255, ReadU8() / 255, ReadU8() / 255)) + end + return NumberSequence.new(keypoints) +end +anyWrites.NumberSequence = function(value: NumberSequence) + local length = #value.Keypoints + Allocate(2 + length * 3) + WriteU8(25) + WriteU8(length) + for index, keypoint in value.Keypoints do + WriteU8(keypoint.Time * 255 + 0.5) WriteU8(keypoint.Value * 255 + 0.5) WriteU8(keypoint.Envelope * 255 + 0.5) + end +end + +anyReads[26] = function() + local length = ReadU8() + local keypoints = table.create(length) + for index = 1, length do + table.insert(keypoints, ColorSequenceKeypoint.new(ReadU8() / 255, Color3.fromRGB(ReadU8(), ReadU8(), ReadU8()))) + end + return ColorSequence.new(keypoints) +end +anyWrites.ColorSequence = function(value: ColorSequence) + local length = #value.Keypoints + Allocate(2 + length * 4) + WriteU8(26) + WriteU8(length) + for index, keypoint in value.Keypoints do + WriteU8(keypoint.Time * 255 + 0.5) + WriteU8(keypoint.Value.R * 255 + 0.5) WriteU8(keypoint.Value.G * 255 + 0.5) WriteU8(keypoint.Value.B * 255 + 0.5) + end +end + +anyReads[27] = function() + return enums[ReadU8()]:FromValue(ReadU16()) +end +anyWrites.EnumItem = function(value: EnumItem) + Allocate(4) + WriteU8(27) + WriteU8(enumIndices[value.EnumType]) + WriteU16(value.Value) +end + +anyReads[28] = function() + local value = {} + while true do + local typeId = ReadU8() + if typeId == 0 then return value else value[anyReads[typeId]()] = anyReads[ReadU8()]() end + end +end +anyWrites.table = function(value: {[any]: any}) + Allocate(1) + WriteU8(28) + for index, value in value do anyWrites[typeof(index)](index) anyWrites[typeof(value)](value) end + Allocate(1) + WriteU8(0) +end + + +return { + Import = function(cursor: Cursor) + activeCursor = cursor + activeBuffer = cursor.Buffer + bufferLength = cursor.BufferLength + bufferOffset = cursor.BufferOffset + instances = cursor.Instances + instancesOffset = cursor.InstancesOffset + end, + + Export = function() + activeCursor.BufferLength = bufferLength + activeCursor.BufferOffset = bufferOffset + activeCursor.InstancesOffset = instancesOffset + return activeCursor + end, + + Truncate = function() + local truncatedBuffer = buffer.create(bufferOffset) + buffer.copy(truncatedBuffer, 0, activeBuffer, 0, bufferOffset) + if instancesOffset == 0 then return truncatedBuffer else return truncatedBuffer, instances end + end, + + Ended = function() + return bufferOffset >= bufferLength + end, + + Types = types, + Reads = reads, + Writes = writes, +} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau new file mode 100644 index 0000000..ffc34a4 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau @@ -0,0 +1,395 @@ +--!strict + + +-- Requires +local Signal = require(script.Signal) +local Task = require(script.Task) +local Types = require(script.Types) + + +-- Types +export type Packet = { + Type: "Packet", + Id: number, + Name: string, + Reads: {() -> any}, + Writes: {(any) -> ()}, + ResponseTimeout: number, + ResponseTimeoutValue: any, + ResponseReads: {() -> any}, + ResponseWrites: {(any) -> ()}, + OnServerEvent: Signal.Signal<(Player, A...)>, + OnClientEvent: Signal.Signal, + OnServerInvoke: nil | (player: Player, A...) -> B..., + OnClientInvoke: nil | (A...) -> B..., + Response: (self: Packet, B...) -> Packet, + Fire: (self: Packet, A...) -> B..., + FireClient: (self: Packet, player: Player, A...) -> B..., + Serialize: (self: Packet, A...) -> (buffer, {Instance}?), + Deserialize: (self: Packet, serializeBuffer: buffer, instances: {Instance}?) -> A..., + DeserializeReturnOffset:(self: Packet, serializeBuffer: buffer, offset: number?) -> (number, {any}) +} + + +-- Varables +local ParametersToFunctions, TableToFunctions, ReadParameters, WriteParameters, Timeout +local RunService = game:GetService("RunService") +local PlayersService = game:GetService("Players") +local reads, writes, Import, Export, Truncate, Ended = Types.Reads, Types.Writes, Types.Import, Types.Export, Types.Truncate, Types.Ended +local ReadU8, WriteU8, ReadU16, WriteU16 = reads.NumberU8, writes.NumberU8, reads.NumberU16, writes.NumberU16 +local Packet = {} :: Packet<...any, ...any> +local packets = {} :: {[string | number]: Packet<...any, ...any>} +local playerCursors : {[Player]: Types.Cursor} +local playerThreads : {[Player]: {[number]: {Yielded: thread, Timeout: thread}, Index: number}} +local threads : {[number]: {Yielded: thread, Timeout: thread}, Index: number} +local remoteEvent : RemoteEvent +local packetCounter : number +local cursor = {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0} + + +-- Constructor +local function Constructor(_, name: string, ...: A...) + local packet = packets[name] :: Packet + if packet then return packet end + local packet = (setmetatable({}, Packet) :: any) :: Packet + packet.Name = name + if RunService:IsServer() then + packet.Id = packetCounter + packet.OnServerEvent = Signal() :: Signal.Signal<(Player, A...)> + remoteEvent:SetAttribute(name, packetCounter) + packets[packetCounter] = packet + packetCounter += 1 + else + packet.Id = remoteEvent:GetAttribute(name) + packet.OnClientEvent = Signal() :: Signal.Signal + if packet.Id then packets[packet.Id] = packet end + end + packet.Reads, packet.Writes = ParametersToFunctions(table.pack(...)) + packets[packet.Name] = packet + return packet +end + + +-- Packet +Packet["__index"] = Packet +Packet.Type = "Packet" + +function Packet:Response(...) + self.ResponseTimeout = self.ResponseTimeout or 10 + self.ResponseReads, self.ResponseWrites = ParametersToFunctions(table.pack(...)) + return self +end + +function Packet:Fire(...) + if self.ResponseReads then + if RunService:IsServer() then error("You must use FireClient(player)", 2) end + local responseThread + for i = 1, 128 do + responseThread = threads[threads.Index] + if responseThread then threads.Index = (threads.Index + 1) % 128 else break end + end + if responseThread then error("Cannot have more than 128 yielded threads", 2) end + Import(cursor) + WriteU8(self.Id) + WriteU8(threads.Index) + threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, threads, threads.Index, self.ResponseTimeoutValue)} + threads.Index = (threads.Index + 1) % 128 + WriteParameters(self.Writes, {...}) + cursor = Export() + return coroutine.yield() + else + Import(cursor) + WriteU8(self.Id) + WriteParameters(self.Writes, {...}) + cursor = Export() + end +end + +function Packet:FireClient(player, ...) + if player.Parent == nil then return end + if self.ResponseReads then + local threads = playerThreads[player] + if threads == nil then threads = {Index = 0} playerThreads[player] = threads end + local responseThread + for i = 1, 128 do + responseThread = threads[threads.Index] + if responseThread then threads.Index = (threads.Index + 1) % 128 else break end + end + if responseThread then error("Cannot have more than 128 yielded threads", 2) return end + Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}) + WriteU8(self.Id) + WriteU8(threads.Index) + threads[threads.Index] = {Yielded = coroutine.running(), Timeout = Task:Delay(self.ResponseTimeout, Timeout, threads, threads.Index, self.ResponseTimeoutValue)} + threads.Index = (threads.Index + 1) % 128 + WriteParameters(self.Writes, {...}) + playerCursors[player] = Export() + return coroutine.yield() + else + Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}) + WriteU8(self.Id) + WriteParameters(self.Writes, {...}) + playerCursors[player] = Export() + end +end + +function Packet:Serialize(...) + Import({Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}) + WriteParameters(self.Writes, {...}) + return Truncate() +end + +function Packet:Deserialize(serializeBuffer, instances) + Import({Buffer = serializeBuffer, BufferLength = buffer.len(serializeBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0}) + return ReadParameters(self.Reads) +end + +function Packet:DeserializeReturnOffset(serializeBuffer, offset) + Import({Buffer = serializeBuffer, BufferLength = buffer.len(serializeBuffer), BufferOffset = offset or 0, Instances = {}, InstancesOffset = 0}) + local values = table.create(#self.Reads) + for index, func in self.Reads do values[index] = func() end + return Export().BufferOffset, values +end + + +-- Functions +function ParametersToFunctions(parameters: {any}) + local readFunctions, writeFunctions = table.create(#parameters), table.create(#parameters) + for index, parameter in ipairs(parameters) do + if type(parameter) == "table" then + readFunctions[index], writeFunctions[index] = TableToFunctions(parameter) + else + readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter] + end + end + return readFunctions, writeFunctions +end + +function TableToFunctions(parameters: {any}) + if #parameters == 1 then + local parameter = parameters[1] + local ReadFunction, WriteFunction + if type(parameter) == "table" then + ReadFunction, WriteFunction = TableToFunctions(parameter) + else + ReadFunction, WriteFunction = reads[parameter], writes[parameter] + end + local Read = function() + local length = ReadU16() + local values = table.create(length) + for index = 1, length do values[index] = ReadFunction() end + return values + end + local Write = function(values: {any}) + WriteU16(#values) + for index, value in values do WriteFunction(value) end + end + return Read, Write + else + local keys = {} for key, value in parameters do table.insert(keys, key) end table.sort(keys) + local readFunctions, writeFunctions = table.create(#keys), table.create(#keys) + for index, key in keys do + local parameter = parameters[key] + if type(parameter) == "table" then + readFunctions[index], writeFunctions[index] = TableToFunctions(parameter) + else + readFunctions[index], writeFunctions[index] = reads[parameter], writes[parameter] + end + end + local Read = function() + local values = {} + for index, ReadFunction in readFunctions do values[keys[index]] = ReadFunction() end + return values + end + local Write = function(values: {[any]: any}) + for index, WriteFunction in writeFunctions do WriteFunction(values[keys[index]]) end + end + return Read, Write + end +end + +function ReadParameters(reads: {() -> any}) + local values = table.create(#reads) + for index, func in reads do values[index] = func() end + return table.unpack(values) +end + +function WriteParameters(writes: {(any) -> ()}, values: {any}) + for index, func in writes do func(values[index]) end +end + +function Timeout(threads: {[number]: {Yielded: thread, Timeout: thread}, Index: number}, threadIndex: number, value: any) + local responseThreads = threads[threadIndex] + task.defer(responseThreads.Yielded, value) + threads[threadIndex] = nil +end + + +-- Initialize +if RunService:IsServer() then + playerCursors = {} + playerThreads = {} + packetCounter = 0 + remoteEvent = Instance.new("RemoteEvent", script) + + local playerBytes = {} + + local thread = task.spawn(function() + while true do + coroutine.yield() + if cursor.BufferOffset > 0 then + local truncatedBuffer = buffer.create(cursor.BufferOffset) + buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset) + if cursor.InstancesOffset == 0 then + remoteEvent:FireAllClients(truncatedBuffer) + else + remoteEvent:FireAllClients(truncatedBuffer, cursor.Instances) + cursor.InstancesOffset = 0 + table.clear(cursor.Instances) + end + cursor.BufferOffset = 0 + end + for player, cursor in playerCursors do + local truncatedBuffer = buffer.create(cursor.BufferOffset) + buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset) + if cursor.InstancesOffset == 0 then + remoteEvent:FireClient(player, truncatedBuffer) + else + remoteEvent:FireClient(player, truncatedBuffer, cursor.Instances) + end + end + table.clear(playerCursors) + table.clear(playerBytes) + end + end) + + local respond = function(packet: Packet, player: Player, threadIndex: number, ...) + if packet.OnServerInvoke == nil then if RunService:IsStudio() then warn("OnServerInvoke not found for packet:", packet.Name, "discarding event:", ...) end return end + local values = {packet.OnServerInvoke(player, ...)} + if player.Parent == nil then return end + Import(playerCursors[player] or {Buffer = buffer.create(128), BufferLength = 128, BufferOffset = 0, Instances = {}, InstancesOffset = 0}) + WriteU8(packet.Id) + WriteU8(threadIndex + 128) + WriteParameters(packet.ResponseWrites, values) + playerCursors[player] = Export() + end + + local onServerEvent = function(player: Player, receivedBuffer: buffer, instances: {Instance}?) + local bytes = (playerBytes[player] or 0) + math.max(buffer.len(receivedBuffer), 800) + if bytes > 8_000 then if RunService:IsStudio() then warn(player.Name, "is exceeding the data/rate limit; some events may be dropped") end return end + playerBytes[player] = bytes + Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0}) + while Ended() == false do + local packet = packets[ReadU8()] + if packet.ResponseReads then + local threadIndex = ReadU8() + if threadIndex < 128 then + Task:Defer(respond, packet, player, threadIndex, ReadParameters(packet.Reads)) + else + threadIndex -= 128 + local responseThreads = playerThreads[player][threadIndex] + if responseThreads then + task.cancel(responseThreads.Timeout) + task.defer(responseThreads.Yielded, ReadParameters(packet.ResponseReads)) + playerThreads[player][threadIndex] = nil + elseif RunService:IsStudio() then + warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads)) + else + ReadParameters(packet.ResponseReads) + end + end + else + packet.OnServerEvent:Fire(player, ReadParameters(packet.Reads)) + end + end + end + + remoteEvent.OnServerEvent:Connect(function(player: Player, ...) + local success, errorMessage: string? = pcall(onServerEvent, player, ...) + if errorMessage and RunService:IsStudio() then warn(player.Name, errorMessage) end + end) + + PlayersService.PlayerRemoving:Connect(function(player) + playerCursors[player] = nil + playerThreads[player] = nil + playerBytes[player] = nil + end) + + RunService.Heartbeat:Connect(function(deltaTime) task.defer(thread) end) +else + threads = {Index = 0} + remoteEvent = script:WaitForChild("RemoteEvent") + local totalTime = 0 + + local thread = task.spawn(function() + while true do + coroutine.yield() + if cursor.BufferOffset > 0 then + local truncatedBuffer = buffer.create(cursor.BufferOffset) + buffer.copy(truncatedBuffer, 0, cursor.Buffer, 0, cursor.BufferOffset) + if cursor.InstancesOffset == 0 then + remoteEvent:FireServer(truncatedBuffer) + else + remoteEvent:FireServer(truncatedBuffer, cursor.Instances) + cursor.InstancesOffset = 0 + table.clear(cursor.Instances) + end + cursor.BufferOffset = 0 + end + end + end) + + local respond = function(packet: Packet, threadIndex: number, ...) + if packet.OnClientInvoke == nil then warn("OnClientInvoke not found for packet:", packet.Name, "discarding event:", ...) return end + local values = {packet.OnClientInvoke(...)} + Import(cursor) + WriteU8(packet.Id) + WriteU8(threadIndex + 128) + WriteParameters(packet.ResponseWrites, values) + cursor = Export() + end + + remoteEvent.OnClientEvent:Connect(function(receivedBuffer: buffer, instances: {Instance}?) + Import({Buffer = receivedBuffer, BufferLength = buffer.len(receivedBuffer), BufferOffset = 0, Instances = instances or {}, InstancesOffset = 0}) + while Ended() == false do + local packet = packets[ReadU8()] + if packet.ResponseReads then + local threadIndex = ReadU8() + if threadIndex < 128 then + Task:Defer(respond, packet, threadIndex, ReadParameters(packet.Reads)) + else + threadIndex -= 128 + local responseThreads = threads[threadIndex] + if responseThreads then + task.cancel(responseThreads.Timeout) + task.defer(responseThreads.Yielded, ReadParameters(packet.ResponseReads)) + threads[threadIndex] = nil + else + warn("Response thread not found for packet:", packet.Name, "discarding response:", ReadParameters(packet.ResponseReads)) + end + end + else + packet.OnClientEvent:Fire(ReadParameters(packet.Reads)) + end + end + end) + + remoteEvent.AttributeChanged:Connect(function(name) + local packet = packets[name] + if packet then + if packet.Id then packets[packet.Id] = nil end + packet.Id = remoteEvent:GetAttribute(name) + if packet.Id then packets[packet.Id] = packet end + end + end) + + RunService.Heartbeat:Connect(function(deltaTime) + totalTime += deltaTime + if totalTime > 0.016666666666666666 then + totalTime %= 0.016666666666666666 + task.defer(thread) + end + end) +end + + +return setmetatable(Types.Types, {__call = Constructor}) \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau new file mode 100644 index 0000000..61773bc --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau @@ -0,0 +1,34 @@ +--!strict +local NS = "__REMOTE_TABLE__" +local Packet = require(script.Parent.Packet) +local Token = Packet.NumberU16 +local TokenName = Packet.String +local TableId = Packet.NumberF64 +local Key = Packet.Any +local Value = Packet.Any +local EventStream = Packet.BufferLong +local WriteStart = Packet.Boolean8 + +local SendEventStreamRemote: RemoteEvent +local IsServer = game:GetService("RunService"):IsServer() +if IsServer then + SendEventStreamRemote = Instance.new("RemoteEvent") + SendEventStreamRemote.Name = "SendEventStreamRemote" + SendEventStreamRemote.Parent = script +else + SendEventStreamRemote = script:WaitForChild("SendEventStreamRemote") +end +return { + ConnectionRequest = Packet(NS.."Request", Token), + + SendEventStream = SendEventStreamRemote, + NewRemoteTable = Packet(NS.."NewRemoteTable", TableId, WriteStart), + DestroyRemoteTable = Packet(NS.."DestroyRemoteTable", TableId), + NewTable = Packet(NS.."NewTable", TableId, Key, TableId), + Set = Packet(NS.."Set", TableId, Key, Value), + Insert = Packet(NS.."Insert", TableId, Value), + InsertAt = Packet(NS.."InsertAt", TableId, Key, Value), + Remove = Packet(NS.."Remove", TableId, Key), + SwapRemove = Packet(NS.."SwapRemove", TableId, Key), + Clear = Packet(NS.."Clear", TableId), +} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau new file mode 100644 index 0000000..5c5bb57 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau @@ -0,0 +1,88 @@ +--!strict +--!optimize 2 + +local Signal = require(script.Parent.Zignal) + +local PromiseLight = {} +PromiseLight.__index = PromiseLight + +export type Status = "Success" | "Timeout" | "Cancel" +type Callback = (Status, A...) -> () +type self = { + Resolved: boolean, + Destroyed: boolean, + PreResolve: Callback?, + PostResolve: Callback?, + _Timeout: number?, + _TimeoutThread: thread?, + _OutputSignal: Signal.Signal<(Status, A...)>, +} +export type PromiseLight = typeof(setmetatable({} :: self, PromiseLight)) + +function PromiseLight.new(timeout: number?): PromiseLight + local self = setmetatable({}, PromiseLight) :: PromiseLight + self.Resolved = false + self.Destroyed = false + self._OutputSignal = Signal.new() + if timeout then + self._Timeout = timeout + local timeout_thread = task.delay(self._Timeout, function() + self.Resolve(self :: any, "Timeout") + end) + self._TimeoutThread = timeout_thread + end + return self +end + +-- Resolves with given arguments and status +function PromiseLight.Resolve(self: PromiseLight, status: Status, ...: A...) + if self.Destroyed then warn("Can not resolve a destroyed PromiseLight.") return end + if self.Resolved then warn("Can not resolve a resolved PromiseLight.") return end + self.Resolved = true + if self.PreResolve then + self.PreResolve(status, ...) + end + if self._TimeoutThread then + if coroutine.status(self._TimeoutThread) == "suspended" then + task.cancel(self._TimeoutThread) + end + self._TimeoutThread = nil + end + self._OutputSignal:Fire(status, ...) + if self.PostResolve then + self.PostResolve(status, ...) + end + self.Destroy(self :: any) +end + +-- Awaits the resolvement +function PromiseLight.Await(self: PromiseLight): (Status, A...) + assert(not self.Destroyed, "Can not await a destroyed PromiseLight.") + return self._OutputSignal:Wait() +end + +-- Hooks a callback to run on resolvement +function PromiseLight.OnResolve(self: PromiseLight, callback: Callback) + assert(not self.Destroyed, "Can not hook a callback to a destroyed PromiseLight.") + self._OutputSignal:Connect(callback) +end + +-- Resolves with "Cancel" status +function PromiseLight.Cancel(self: PromiseLight) + assert(not self.Destroyed, "Can not cancel a destroyed PromiseLight.") + if self.Resolved then return end + self.Resolve(self :: any, "Cancel") +end + +-- Resolves with "Cancel" if not resolved and destroys the class +function PromiseLight.Destroy(self: PromiseLight) + if self.Destroyed then return end + if not self.Resolved then + self.Cancel(self :: any) + end + self.Destroyed = true + self._OutputSignal:DisconnectAll() + self._OutputSignal = nil :: any +end + +return PromiseLight \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau new file mode 100644 index 0000000..87cac70 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau @@ -0,0 +1,128 @@ +--!strict + +local TokenRegistry = {} + +local IsServer = game:GetService("RunService"):IsServer() +local IsClient = game:GetService("RunService"):IsClient() + +local TokenRemote: RemoteEvent +if IsServer then + TokenRemote = Instance.new("RemoteEvent") + TokenRemote.Name = "TokenRemote" + TokenRemote.Parent = script +else + TokenRemote = script:WaitForChild("TokenRemote") +end + +local Shared = script.Parent +local Util = require(Shared.Util) +local Signal = require(Shared.Zignal) +local Packets = require(Shared.Packets) +local PromiseLight = require(Shared.PromiseLight) + +local SanitizeForAttributeName = Util.SanitizeForAttributeName + +type Token = Util.Token +type TokenName = string + +local MAX_IDS = 2^16 - 1 +local TokenNames = {} :: {[Token]: TokenName} +local TokenLookup = {} :: {[TokenName]: Token} +local TokenPromises = {} :: {[TokenName]: PromiseLight.PromiseLight} + +local function FindFirstGap(tbl: any) + local gap_index = 0 + for i, _ in ipairs(tbl) do + gap_index = i + end + gap_index += 1 + assert(gap_index <= MAX_IDS, "Max RemoteTable count has been exceeded: 65535") + return gap_index +end + +local function Register(name: TokenName, token: Token?): Token + local name = SanitizeForAttributeName(name) + local token = token or FindFirstGap(TokenNames) + TokenNames[token] = name + TokenLookup[name] = token + + local promise = TokenPromises[name] + if promise then + promise:Resolve("Success", token) + end + + if IsServer then + script:SetAttribute(name, token) + TokenRemote:FireAllClients("A", name, token) + end + return token +end + +local function Unregister(name: TokenName) + local name = SanitizeForAttributeName(name) + local token = TokenLookup[name] + if token then + TokenLookup[name] = nil + end + + local promise = TokenPromises[name] + if promise then + promise:Cancel() + end + + if IsServer then + script:SetAttribute(name, nil) + TokenRemote:FireAllClients("R", name) + end +end + +function TokenRegistry.IsTokenRegistered(name: TokenName): boolean + local name = SanitizeForAttributeName(name) + return script:GetAttribute(name) ~= nil +end + +function TokenRegistry.GetToken(name: TokenName): Token + local name = SanitizeForAttributeName(name) + if IsServer then + return TokenLookup[name] + else + return script:GetAttribute(name) + end +end + +function TokenRegistry.GetTokenName(token: Token): TokenName + return TokenNames[token] +end + +function TokenRegistry.WaitForToken(name: TokenName, timeout: number?): Token + local name = SanitizeForAttributeName(name) + if script:GetAttribute(name) ~= nil then + return script:GetAttribute(name) + end + + local promise = TokenPromises[name] + if promise then return select(2, promise:Await()) end + + local promise = PromiseLight.new(timeout) :: any + TokenPromises[name] = promise + promise.PreResolve = function() + TokenPromises[name] = nil + end + + return select(2, promise:Await()) +end + +TokenRegistry.Register = Register +TokenRegistry.Unregister = Unregister + +if IsClient then + for token_name, token in script:GetAttributes() do + Register(token_name, token) + end + TokenRemote.OnClientEvent:Connect(function(command: "A" | "R", token_name: TokenName, token: Token) + if command == "A" then Register(token_name, token) end + if command == "R" then Unregister(token_name) end + end) +end + +return TokenRegistry \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau new file mode 100644 index 0000000..be4f259 --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau @@ -0,0 +1,141 @@ +--!strict +--!optimize 2 + +export type Id = number +export type Key = string | number +export type Path = string +export type Table = any +export type Token = number +export type TokenName = string + +local Util = {} + +local SEPERATOR = string.char(31) + +local function DeepCopy(target: T): T + if type(target) == "table" then + local copy = {} + for key, value in target do + copy[DeepCopy(key)] = DeepCopy(value) + end + return copy :: any + else + return target + end +end + +local function SanitizeForAttributeName(str) + return (str:gsub("[^%w]", "_")) +end + +local function ToPathString(path_list: {Key}): string + return table.concat(path_list, SEPERATOR) +end + +local function AppendPath(path: Path, key: Key): string + if path == "" then + return `{key}` + else + return `{path}{SEPERATOR}{key}` + end +end + +local function IsValidKeyType(value: any): boolean + local type_ = typeof(value) + return type_ == "string" or type_ == "number" +end + +local function IsValidValueType(value: any): boolean + return typeof(value) ~= "Instance" +end + +@native +local function GetTableId(tbl: any): number + return tonumber(tostring(tbl):sub(8)) :: number +end + +@native +local function SwapRemove(tbl: {V}, pos: number?): V? + local len = #tbl + if len == 0 or (pos and pos > len) then + return nil + end + + local pos = pos or len + local value = tbl[pos] + tbl[pos] = tbl[len] + tbl[len] = nil + return value +end + +@native +local function ExpandBuffer(target: buffer): buffer + local target_len = buffer.len(target) + if target_len == 0 then + target_len = 1 + end + local expanded = buffer.create(target_len * 2) -- growth rate + buffer.copy(expanded, 0, target) + return expanded +end + +@native +local function BufferWriteU8(target: buffer, offset: number, value: number): (buffer, number) + local final_offset = offset + 1 + while final_offset > buffer.len(target) do + target = ExpandBuffer(target) + end + buffer.writeu8(target, offset, value) + return target, final_offset +end + +@native +local function BufferWriteU16(target: buffer, offset: number, value: number): (buffer, number) + local final_offset = offset + 2 + while final_offset > buffer.len(target) do + target = ExpandBuffer(target) + end + buffer.writeu16(target, offset, value) + return target, final_offset +end + +@native +local function BufferAppend(target: buffer, offset: number, source: buffer): (buffer, number) + local final_offset = buffer.len(source) + offset + while final_offset > buffer.len(target) do + target = ExpandBuffer(target) + end + buffer.copy(target, offset, source) + return target, final_offset +end + +@native +local function BufferTruncate(target: buffer, offset: number): buffer + local truncated = buffer.create(offset) + buffer.copy(truncated, 0, target, 0, offset) + return truncated +end + +Util.OpCodes = {"NewRemoteTable", "DestroyRemoteTable", "NewTable", "Set", "Insert", "InsertAt", "Remove", "SwapRemove", "Clear"} +Util.OpCodeLookup = {} :: {[string]: number} +for index, event in Util.OpCodes do + Util.OpCodeLookup[event] = index +end +table.freeze(Util.OpCodes) +table.freeze(Util.OpCodeLookup) + +Util.DeepCopy = DeepCopy +Util.SanitizeForAttributeName = SanitizeForAttributeName +Util.ToPathString = ToPathString +Util.AppendPath = AppendPath +Util.IsValidKeyType = IsValidKeyType +Util.IsValidValueType = IsValidValueType +Util.GetTableId = GetTableId +Util.SwapRemove = SwapRemove +Util.ExpandBuffer = ExpandBuffer +Util.BufferWriteU8 = BufferWriteU8 +Util.BufferWriteU16 = BufferWriteU16 +Util.BufferAppend = BufferAppend +Util.BufferTruncate = BufferTruncate + +return Util \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau new file mode 100644 index 0000000..072eb9e --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau @@ -0,0 +1,7 @@ +--[[ + +# 1.0 +- Initial Release. + + +]] \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau new file mode 100644 index 0000000..fd8d59c --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau @@ -0,0 +1,105 @@ +--!strict + +export type Connection = { + Connected: boolean, + Disconnect: (self: Connection) -> (), + _function: (A...) -> (), + _next: Connection?, + _previous: Connection, +} + +export type Signal = { + new: () -> Signal, + Fire: (self: Signal, A...) -> (), + Wait: (self: Signal) -> A..., + Once: (self: Signal, func: (A...) -> ()) -> Connection, + Connect: (self: Signal, func: (A...) -> ()) -> Connection, + DisconnectAll: (self: Signal) -> (), + _next: Connection?, +} + +local threads: {thread} = {} + +local function Call(func: (A...) -> (), thread: thread, ...: A...): () + func(...) + table.insert(threads, thread) +end + +local function Yield(): () + while true do Call(coroutine.yield()) end +end + +local Signal, Connection = {}, {} +Signal.__index, Connection.__index = Signal, Connection + +local function Disconnect(self: Connection): () + if self.Connected then self.Connected = false else return end + local next, previous = self._next, self._previous + if next then next._previous = previous end + previous._next = next +end + +local function New(): Signal + return (setmetatable({}, Signal) :: any) :: Signal +end + +local function Fire(self: Signal, ...: A...): () + local link = self._next + while link do + local length, thread = #threads, nil + if length == 0 then + thread = coroutine.create(Yield) + coroutine.resume(thread) + else + thread = threads[length] + threads[length] = nil + end + task.spawn(thread, link._function, thread, ...) + link = link._next + end +end + +local function Connect(self: Signal, func: (A...) -> ()): Connection + local next = self._next + local link = {Connected = true, _previous = self, _function = func, _next = next} :: any + if next ~= nil then next._previous = link end + self._next = link + return setmetatable(link, Connection) :: Connection +end + +local function Once(self: Signal, func: (A...) -> ()): Connection + local connection + connection = Connect(self, function(...) + Disconnect(connection) + func(...) + end) + return connection +end + +local function Wait(self: Signal): A... + local thread, connection = coroutine.running(), nil + connection = Connect(self, function(...) + Disconnect(connection) + if coroutine.status(thread) == "suspended" then task.spawn(thread, ...) end + end) + return coroutine.yield() +end + +local function DisconnectAll(self: Signal): () + local link = self._next + while link do + link.Connected = false + link = link._next + end + self._next = nil +end + +Signal.new = New +Signal.Fire = Fire +Signal.Wait = Wait +Signal.Once = Once +Signal.Connect = Connect +Signal.DisconnectAll = DisconnectAll +Connection.Disconnect = Disconnect + +return Signal \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/init.luau b/src/DebuggerUI/Shared/External/RemoteTableLight/init.luau new file mode 100644 index 0000000..49a9d7a --- /dev/null +++ b/src/DebuggerUI/Shared/External/RemoteTableLight/init.luau @@ -0,0 +1,15 @@ +--!strict + +local Server: typeof(require(script.Server)) +local Client: typeof(require(script.Client)) + +if game:GetService("RunService"):IsServer() then + Server = require(script.Server) +else + Client = require(script.Client) +end + +return { + Server = Server, + Client = Client, +} \ No newline at end of file From 470cd2179fe2382eb9ae209d95241712093522d7 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:16:02 +0700 Subject: [PATCH 09/22] Claude: Refactor IrisLocal --- default.project.json | 3 +- sourcemap.json | 2 +- src/DebuggerUI/Client/IrisLocal.client.luau | 854 ++++++++++---------- 3 files changed, 409 insertions(+), 450 deletions(-) diff --git a/default.project.json b/default.project.json index 5116388..8c80496 100644 --- a/default.project.json +++ b/default.project.json @@ -5,7 +5,8 @@ "ReplicatedStorage": { "FastCast2": { "$path": "src/FastCast2" - } + }, + "$path": "src/DebuggerUI/Shared" } } } \ No newline at end of file diff --git a/sourcemap.json b/sourcemap.json index 32e8fba..9c0cfcd 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"External","className":"Folder","children":[{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Window.luau"]}]}]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]}]}]} \ No newline at end of file diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau index f13bc4e..4a2d471 100644 --- a/src/DebuggerUI/Client/IrisLocal.client.luau +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -1,71 +1,109 @@ +--!strict --[[ - - Author: Mawin_CK - - Date: 2025 + Author: Mawin_CK + Date: 2025 + Note: Studio DebuggerUI only. Published UI is separate. ]] -- TODO: Add blockcast support -- Services local Rep = game:GetService("ReplicatedStorage") -local Players = game:GetService("Players") local RepFirst = game:GetService("ReplicatedFirst") -local UIS = game:GetService("UserInputService") +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") -- Modules local FastCast2 = Rep:WaitForChild("FastCast2") - --- Requires -local iris = require(Rep:WaitForChild("iris")) local FastCastM = require(FastCast2) local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) local Jolt = require(Rep:WaitForChild("Jolt")) local Signal = require(FastCast2:WaitForChild("Signal")) +local iris = require(Rep:WaitForChild("iris")) +local RemoteTable = require(Rep:WaitForChild("RemoteTableLight")).Client +-- ============================================================ -- Types -type RequestLogDataType = "FastCastBehavior" | "Setting" +-- ============================================================ --- Variables -local player = Players.LocalPlayer -local character = player.Character or player.CharacterAdded:Wait() - ---local TargetPartOrigin: BasePart = nil ---local TargetOrigin: Vector3 = Vector3.new() +type CastType = "Raycast" | "Blockcast" | "Spherecast" ---local TargetDirection = Vector3.new() - -local ProjectileContainer = workspace:WaitForChild('Projectiles') -local ProjectileTemplate = Rep:WaitForChild("Projectile") - -local ClientProjectileCount = 0 -local ServerProjectileCount = 0 +-- ============================================================ +-- Constants +-- ============================================================ +local DEBOUNCE_LC_TIME = 1.5 +local DEFAULT_CACHE_SIZE = 1000 +local DEBUGGER_TOKEN = "DebuggerState" -local HighFidelityBehaviorName = { +local HIGH_FIDELITY_NAMES = { [1] = "Default", [2] = "Automatic", - [3] = "Always" + [3] = "Always", } -local TestModules = {} -for _, value in Rep:WaitForChild("Tests"):GetChildren() do - TestModules[value.Name] = require(value) +local CAST_TYPES: { CastType } = { "Raycast", "Blockcast", "Spherecast" } + +-- ============================================================ +-- Player / Character +-- ============================================================ + +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() + +-- ============================================================ +-- Assets +-- ============================================================ + +local ProjectileContainer = workspace:WaitForChild("Projectiles") +local ProjectileTemplate = Rep:WaitForChild("Projectile") + +-- ============================================================ +-- External Modules (Tests & FastCastEventsModules) +-- ============================================================ + +local TestModules: { [string]: any } = {} +for _, mod: ModuleScript in Rep:WaitForChild("Tests"):GetChildren() do + TestModules[mod.Name] = require(mod) :: any end -local FastCastEventsModules = {} -for _, value in Rep:WaitForChild("FastCastEventsModules"):GetChildren() do - FastCastEventsModules[value.Name] = value +local FastCastEventsModules: { [string]: ModuleScript } = {} +for _, mod in Rep:WaitForChild("FastCastEventsModules"):GetChildren() do + FastCastEventsModules[mod.Name] = mod :: ModuleScript end -local debounce_lc = false -local debounce_lc_time = 1.5 +-- ============================================================ +-- Jolt Events (client → server) +-- ============================================================ + +local CastBehaviorServerUpdate = Jolt.Client("CastBehaviorServerUpdate") :: Jolt.Client +local LoggingServer = Jolt.Client("LoggingServer") :: Jolt.Client +local ServerSettingUpdate = Jolt.Client("ServerSettingUpdate") :: Jolt.Client<{ velocity: number, projectileLimit: number }> +local ServerProjectile = Jolt.Client("ServerProjectile") :: Jolt.Client +local ServerCastModuleUpdate = Jolt.Client("ServerCastModuleUpdate") :: Jolt.Client + +-- ============================================================ +-- RemoteTable — live server state (replaces ServerProjectileCountEvent) +-- ============================================================ + +local serverState = RemoteTable.WaitForTable(DEBUGGER_TOKEN, 10) +if not serverState then + warn("[IrisLocal] Timed out waiting for DebuggerState RemoteTable.") +end +-- ============================================================ -- CastParams +-- ============================================================ + local CastParams = RaycastParams.new() -CastParams.FilterDescendantsInstances = {character} +CastParams.FilterDescendantsInstances = { character } CastParams.FilterType = Enum.RaycastFilterType.Exclude CastParams.IgnoreWater = true +-- ============================================================ -- Behavior +-- ============================================================ + local CastBehaviorClient: FastCastTypes.FastCastBehavior = FastCastM.newBehavior() CastBehaviorClient.RaycastParams = CastParams CastBehaviorClient.VisualizeCasts = false @@ -73,16 +111,16 @@ CastBehaviorClient.Acceleration = Vector3.new() CastBehaviorClient.AutoIgnoreContainer = true CastBehaviorClient.MaxDistance = 1000 CastBehaviorClient.HighFidelitySegmentSize = 1 - CastBehaviorClient.CosmeticBulletContainer = ProjectileContainer CastBehaviorClient.CosmeticBulletTemplate = ProjectileTemplate +CastBehaviorClient.SimulateAfterPhysic = true CastBehaviorClient.FastCastEventsConfig = { UseLengthChanged = false, UseHit = false, UseCastTerminating = true, UseCastFire = false, - UsePierced = false + UsePierced = false, } CastBehaviorClient.FastCastEventsModuleConfig = { @@ -91,480 +129,400 @@ CastBehaviorClient.FastCastEventsModuleConfig = { UseCastTerminating = true, UseCastFire = false, UsePierced = false, - UseCanPierce = false + UseCanPierce = false, } -CastBehaviorClient.SimulateAfterPhysic = true - --- Events -local CastBehaviorServerUpdate = Jolt.Client("CastBehaviorServerUpdate") :: Jolt.Client -local LoggingServer = Jolt.Client("LoggingServer") :: Jolt.Client -local ServerSettingUpdate = Jolt.Client("ServerSettingUpdate") :: Jolt.Client<{velocity: number, ProjectileLimit: number}> -local ServerProjectile = Jolt.Client("ServerProjectile") :: Jolt.Client -local ServerProjectileCountEvent = Jolt.Client("ServerProjectileCount") :: Jolt.Client -local ServerCastModuleUpdate = Jolt.Client("ServerCastModuleUpdate") :: Jolt.Client - +-- ============================================================ -- Caster +-- ============================================================ + local Caster = FastCastM.new() -Caster:Init( - 4, - RepFirst, - "CastVMs", - RepFirst, - "VMContainer", - "CastVM", - true -) - --- CONSTANTS -local DEFAULT_CACHE_SIZE = 1000 +Caster:Init(4, RepFirst, "CastVMs", RepFirst, "VMContainer", "CastVM", true) + +local debounce_lc = false +local ClientProjectileCount = 0 + +local IntCountEvent = Signal.new() +IntCountEvent:Connect(function(amount: number) + ClientProjectileCount += amount +end) + +local function onCastTerminating(cast: FastCastTypes.ActiveCastCompement) + local obj = cast.RayInfo.CosmeticBulletObject + if obj then obj:Destroy() end + ClientProjectileCount -= 1 +end --- States +local function onCastTerminatingObjectCache(cast: FastCastTypes.ActiveCastCompement) + local obj = cast.RayInfo.CosmeticBulletObject + if obj then Caster.ObjectCache:ReturnObject(obj) end + ClientProjectileCount -= 1 +end + +Caster.CastTerminating = onCastTerminating +Caster.CastFire = function() print("[IrisLocal] CastFire") end +Caster.Hit = function() print("[IrisLocal] Hit") end +Caster.LengthChanged = function() + if debounce_lc then return end + debounce_lc = true + print("[IrisLocal] LengthChanged") + task.delay(DEBOUNCE_LC_TIME, function() debounce_lc = false end) +end +Caster.Pierced = function() print("[IrisLocal] Pierced") end -local WindowSize = iris.State(Vector2.new(500, 400)) +-- ============================================================ +-- Iris States +-- ============================================================ + +-- Window +local WindowSize = iris.State(Vector2.new(500, 400)) local WindowVisible = iris.State(true) -local SelectedBehavior = iris.State(CastBehaviorClient.HighFidelityBehavior) -local AcclerationState = iris.State(CastBehaviorClient.Acceleration) -local AutoIgnoreContainerState = iris.State(CastBehaviorClient.AutoIgnoreContainer) -local MaxDistanceState = iris.State(CastBehaviorClient.MaxDistance) -local VisualizeCastsState = iris.State(CastBehaviorClient.VisualizeCasts) +-- Behavior +local SelectedBehavior = iris.State(CastBehaviorClient.HighFidelityBehavior) +local AccelerationState = iris.State(CastBehaviorClient.Acceleration) +local AutoIgnoreContainerState = iris.State(CastBehaviorClient.AutoIgnoreContainer) +local MaxDistanceState = iris.State(CastBehaviorClient.MaxDistance) +local VisualizeCastsState = iris.State(CastBehaviorClient.VisualizeCasts) local HighFidelitySegmentSizeState = iris.State(CastBehaviorClient.HighFidelitySegmentSize) -local UseCosmeticBulletTemplate = iris.State(true) -local SimulateAfterPhysicState = iris.State(CastBehaviorClient.SimulateAfterPhysic) +local UseCosmeticBulletTemplate = iris.State(true) +local SimulateAfterPhysicState = iris.State(CastBehaviorClient.SimulateAfterPhysic) +local AutomaticPerformanceState = iris.State(true) -local OriginValue = iris.State(Vector3.new(0, 5, 0)) -local DirectionValue = iris.State(Vector3.new(0,0,-1000)) -local VelocityValue = iris.State(50) +-- Settings +local VelocityValue = iris.State(50) local ClientProjectileLimitValue = iris.State(1000) -local P_ClientTestValue = iris.State(false) -local P_ServerTestValue = iris.State(false) -local FastCastEventsConfigStates = {} -local FastCastEventsModuleConfigStates = {} - -local CastTypeState = iris.State("Raycast") +local CastTypeState = iris.State("Raycast") +local BlockSizeState = iris.State(Vector3.new(1, 1, 1)) +local SphereRadiusState = iris.State(1) -local CastTypes = { - "Raycast", - "Blockcast", - "Spherecast" -} +-- Caster toggles +local ObjectEnabledValue = iris.State(Caster.ObjectCacheEnabled) +local ObjectCacheSizeValue = iris.State(DEFAULT_CACHE_SIZE) +local BulkMoveEnabledValue = iris.State(Caster.BulkMoveEnabled) -local P_TestValue = iris.State(false) +-- Performance / config sub-tables (built from behavior defaults) +local FastCastEventsConfigStates: { [string]: any } = {} +local FastCastEventsModuleConfigStates: { [string]: any } = {} +local AdaptivePerformanceStates: { [string]: any } = {} +local VisualizeCastSettingSt: { [string]: any } = {} for key, value in CastBehaviorClient.FastCastEventsConfig do FastCastEventsConfigStates[key] = iris.State(value) end - for key, value in CastBehaviorClient.FastCastEventsModuleConfig do FastCastEventsModuleConfigStates[key] = iris.State(value) end - -local AutomaticPerformanceState = iris.State(true) -local AdaptivePerformanceStates = {} for key, value in CastBehaviorClient.AdaptivePerformance do AdaptivePerformanceStates[key] = iris.State(value) end - -local VisualizeCastSettingSt = {} for key, value in CastBehaviorClient.VisualizeCastSettings do VisualizeCastSettingSt[key] = iris.State(value) end -local ObjectEnabledValue = iris.State(Caster.ObjectCacheEnabled) -local ObjectCacheSizeValue = iris.State(DEFAULT_CACHE_SIZE) -local BulkMoveEnabledValue = iris.State(Caster.BulkMoveEnabled) - -local SelectedTests = {} -for key, _ in TestModules do - SelectedTests[key] = iris.State(false) +-- Tests / Modules +local SelectedTests: { [string]: any } = {} +for key in TestModules do + SelectedTests[key] = iris.State(false) end -local SelcetedModule = iris.State(nil) - --- Local functions - ---[[local function Format(Time: number): string - if Time < 1E-6 then - return `{Time * 1E+9} ns` - elseif Time < 0.001 then - return `{Time * 1E+6} μs` - elseif Time < 1 then - return `{Time * 1000} ms` - else - return `{Time} s` - end -end]] - -local function UpdateClientBehavior() - CastBehaviorClient.HighFidelityBehavior = SelectedBehavior.value - CastBehaviorClient.Acceleration = AcclerationState.value - CastBehaviorClient.AutoIgnoreContainer = AutoIgnoreContainerState.value - CastBehaviorClient.MaxDistance = MaxDistanceState.value - CastBehaviorClient.VisualizeCasts = VisualizeCastsState.value - CastBehaviorClient.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value - CastBehaviorClient.CosmeticBulletTemplate = UseCosmeticBulletTemplate.value == true and ProjectileTemplate or nil - CastBehaviorClient.SimulateAfterPhysic = SimulateAfterPhysicState.value - CastBehaviorClient.AutomaticPerformance = AutomaticPerformanceState.value - for key, v in FastCastEventsConfigStates do - CastBehaviorClient.FastCastEventsConfig[key] = v.value - end - for key, v in FastCastEventsModuleConfigStates do - CastBehaviorClient.FastCastEventsModuleConfig[key] = v.value - end - for key, v in AdaptivePerformanceStates do - CastBehaviorClient.AdaptivePerformance[key] = v.value - end - for key, v in VisualizeCastSettingSt do - CastBehaviorClient.VisualizeCastSettings[key] = v.value - end -end +-- Performance test +local OriginValue = iris.State(Vector3.new(0, 5, 0)) +local DirectionValue = iris.State(Vector3.new(0, 0, -1000)) +local P_ClientTestValue = iris.State(false) +local P_ServerTestValue = iris.State(false) +local P_TestValue = iris.State(false) -local function UpdateServerBehavior() - local newBehavior = FastCastM.newBehavior() - newBehavior.HighFidelityBehavior = SelectedBehavior.value - newBehavior.Acceleration = AcclerationState.value - newBehavior.AutoIgnoreContainer = AutoIgnoreContainerState.value - newBehavior.MaxDistance = MaxDistanceState.value - newBehavior.VisualizeCasts = VisualizeCastsState.value - newBehavior.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value - newBehavior.SimulateAfterPhysic = SimulateAfterPhysicState.value - newBehavior.AutomaticPerformance = AutomaticPerformanceState.value - for key, v in FastCastEventsConfigStates do - newBehavior.FastCastEventsConfig[key] = v.value - end - for key, v in FastCastEventsModuleConfigStates do - newBehavior.FastCastEventsModuleConfig[key] = v.value - end - for key, v in AdaptivePerformanceStates do - newBehavior.AdaptivePerformance[key] = v.value - end - for key, v in VisualizeCastSettingSt do - newBehavior.VisualizeCastSettings[key] = v.value - end - CastBehaviorServerUpdate:Fire(newBehavior) -end +-- FastCastEventsModule selection +local SelectedModuleKey: string? = nil -local function IntCount(amount: number) - ClientProjectileCount += amount -end +-- ============================================================ +-- Helpers +-- ============================================================ -local function OnCastTerminating(cast: FastCastTypes.ActiveCastCompement) - local obj = cast.RayInfo.CosmeticBulletObject - if obj then - obj:Destroy() - end - IntCount(-1) +local function buildClientBehavior() + CastBehaviorClient.HighFidelityBehavior = SelectedBehavior.value + CastBehaviorClient.Acceleration = AccelerationState.value + CastBehaviorClient.AutoIgnoreContainer = AutoIgnoreContainerState.value + CastBehaviorClient.MaxDistance = MaxDistanceState.value + CastBehaviorClient.VisualizeCasts = VisualizeCastsState.value + CastBehaviorClient.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value + CastBehaviorClient.CosmeticBulletTemplate = UseCosmeticBulletTemplate.value and ProjectileTemplate or nil + CastBehaviorClient.SimulateAfterPhysic = SimulateAfterPhysicState.value + CastBehaviorClient.AutomaticPerformance = AutomaticPerformanceState.value + for key, v in FastCastEventsConfigStates do CastBehaviorClient.FastCastEventsConfig[key] = v.value end + for key, v in FastCastEventsModuleConfigStates do CastBehaviorClient.FastCastEventsModuleConfig[key] = v.value end + for key, v in AdaptivePerformanceStates do CastBehaviorClient.AdaptivePerformance[key] = v.value end + for key, v in VisualizeCastSettingSt do CastBehaviorClient.VisualizeCastSettings[key] = v.value end end -local function OnCastTerminating_ObjectCache(cast: FastCastTypes.ActiveCastCompement) - local obj = cast.RayInfo.CosmeticBulletObject - if obj then - Caster.ObjectCache:ReturnObject(obj) - end - IntCount(-1) +local function buildServerBehavior() + local b = FastCastM.newBehavior() + b.HighFidelityBehavior = SelectedBehavior.value + b.Acceleration = AccelerationState.value + b.AutoIgnoreContainer = AutoIgnoreContainerState.value + b.MaxDistance = MaxDistanceState.value + b.VisualizeCasts = false -- never visualize on server + b.HighFidelitySegmentSize = HighFidelitySegmentSizeState.value + b.SimulateAfterPhysic = SimulateAfterPhysicState.value + b.AutomaticPerformance = AutomaticPerformanceState.value + for key, v in FastCastEventsConfigStates do b.FastCastEventsConfig[key] = v.value end + for key, v in FastCastEventsModuleConfigStates do b.FastCastEventsModuleConfig[key] = v.value end + for key, v in AdaptivePerformanceStates do b.AdaptivePerformance[key] = v.value end + for key, v in VisualizeCastSettingSt do b.VisualizeCastSettings[key] = v.value end + CastBehaviorServerUpdate:Fire(b) end -local function CasterFire( - targetType: "Raycast" | "Blockcast" | "Spherecast", - origin: Vector3, - arg: any?, - direction: Vector3, - velocity: Vector3 | number, - behavior: FastCastTypes.FastCastBehavior? -) - --print(targetType, origin, arg, direction, velocity) - if targetType == "Raycast" then +local function casterFire(castType: CastType, origin: Vector3, arg: any, direction: Vector3, velocity: number, behavior: FastCastTypes.FastCastBehavior?) + if castType == "Raycast" then Caster:RaycastFire(origin, direction, velocity, behavior) - elseif targetType == "Blockcast" then + elseif castType == "Blockcast" then Caster:BlockcastFire(origin, arg, direction, velocity, behavior) - elseif targetType == "Spherecast" then + elseif castType == "Spherecast" then Caster:SpherecastFire(origin, arg, direction, velocity, behavior) end end -local BlockSizeState = iris.State(Vector3.new(1,1,1)) -local SphereRadiusState = iris.State(1) +-- ============================================================ +-- Init — push initial state to server on load +-- ============================================================ --- Init iris.Init() + CastBehaviorServerUpdate:Fire(CastBehaviorClient) ServerSettingUpdate:Fire({ velocity = VelocityValue.value, - projectileLimit = ClientProjectileLimitValue.value + projectileLimit = ClientProjectileLimitValue.value, }) -local IntCountEvent = Signal.new() -IntCountEvent:Connect(function(amount: number) - ClientProjectileCount += amount -end) +-- ============================================================ +-- Iris UI +-- ============================================================ -Caster.CastTerminating = OnCastTerminating -Caster.CastFire = function() - print("CastFire Test!") -end -Caster.Hit = function() - print("Hit Test!") -end -Caster.LengthChanged = function() - if not debounce_lc then - debounce_lc = true - print("OnLengthChanged Test!") - task.delay(debounce_lc_time, function() - debounce_lc = false - end) +iris:Connect(function() + iris.Window({ "FastCast2 Debugger [Studio]" }, { size = WindowSize, isOpened = WindowVisible }) + + -- -------------------------------------------------------- + -- Settings + -- -------------------------------------------------------- + iris.Tree({ "Settings" }) + iris.InputNum({ "Velocity" }, { number = VelocityValue }) + iris.InputNum({ "Projectile Limit" }, { number = ClientProjectileLimitValue }) + + iris.Tree({ "Cast Type" }) + for _, castType in CAST_TYPES do + iris.RadioButton({ castType, castType }, { index = CastTypeState }, castType) end -end -Caster.Pierced = function() - print("pierced Test!") -end + iris.End() --- iris -iris:Connect(function() - iris.Window({"FastCast2 TestGUI"},{size=WindowSize, isOpened=WindowVisible}) - iris.Tree("Setting") - iris.InputNum({"Velocity"}, {number=VelocityValue}) - iris.InputNum({"Projectile limit"}, {number=ClientProjectileLimitValue}) - iris.Tree("CastType") - for k,v in CastTypes do - iris.RadioButton( - { v, v }, - {index = CastTypeState}, - k - ) - end - iris.End() - - local arg = nil - - if CastTypeState.value == "Blockcast" then - local input = iris.InputVector3({"BlockcastSize"}, {number = BlockSizeState}) - if input.numberChanged() then - arg = BlockSizeState.value - end - end + local castArg: any = nil + if CastTypeState.value == "Blockcast" then + iris.InputVector3({ "Blockcast Size" }, { number = BlockSizeState }) + castArg = BlockSizeState.value + elseif CastTypeState.value == "Spherecast" then + iris.InputNum({ "Spherecast Radius" }, { number = SphereRadiusState }) + castArg = SphereRadiusState.value + end - if CastTypeState.value == "Spherecast" then - local input = iris.InputNum({"SpherecastRadius"}, {number = SphereRadiusState}) - if input.numberChanged() then - arg = SphereRadiusState.value - end - end - if iris.Button("Update Server").clicked() then - ServerSettingUpdate:Fire({ - velocity = VelocityValue.value, - projectileLimit = ClientProjectileLimitValue.value, - castType = CastTypeState.value, - arg = arg - }) - end - iris.End() - - -- FastCastBehavior - iris.Tree({"FastCastBehavior"}) - iris.Tree("HighFidelityBehavior") - for i = 1, 3 do - iris.RadioButton( - { HighFidelityBehaviorName[i], i }, - {index = SelectedBehavior}, - i - ) - end - iris.End() - - iris.InputVector3({"Acceleration"}, {number = AcclerationState}) - iris.Checkbox({"AutoIgnoreContainer"}, {isChecked = AutoIgnoreContainerState}) - iris.InputNum({"MaxDistance"}, {number = MaxDistanceState}) - iris.Checkbox({"VisualizeCasts"}, {isChecked = VisualizeCastsState}) - iris.InputNum({"HighFidelitySegmentSize", 0.1, 0.1, 2}, {number = HighFidelitySegmentSizeState}) - iris.Checkbox({"Use CosmeticBulletTemplate"}, {isChecked = UseCosmeticBulletTemplate}) - iris.Checkbox({"SimulateAfterPhysic"}, {isChecked=SimulateAfterPhysicState}) - - iris.Tree("FastCastEventsConfig") - for key, state in FastCastEventsConfigStates do - iris.Checkbox({key}, {isChecked = state}) - end - iris.End() + if iris.Button({ "Update Server Settings" }).clicked() then + ServerSettingUpdate:Fire({ + velocity = VelocityValue.value, + projectileLimit = ClientProjectileLimitValue.value, + }) + end + iris.End() - iris.Tree("FastCastEventsModuleConfig") - for key, state in FastCastEventsModuleConfigStates do - iris.Checkbox({key}, {isChecked = state}) - end - iris.End() - - iris.Checkbox({"Automatic performance"}, {isChecked=AutomaticPerformanceState}) - - iris.Tree("Adaptive performance") - iris.InputNum({"HighFidelitySegmentSizeIncrease", 0.1, 0.1, 2}, {number=AdaptivePerformanceStates.HighFidelitySegmentSizeIncrease}) - iris.Checkbox({"LowerHighFidelityBehavior"}, {isChecked=AdaptivePerformanceStates.LowerHighFidelityBehavior}) - iris.End() - - iris.Tree("VisualizeCastSettings") - iris.Text({"Segment"}) - - iris.InputColor3({"Debug_SegmentColor"}, {number=VisualizeCastSettingSt.Debug_SegmentColor}) - iris.InputNum({"Debug_SegmentTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_SegmentTransparency}) - iris.InputNum({"Debug_SegmentSize", 0.05, 0.05, 2}, {number=VisualizeCastSettingSt.Debug_SegmentSize}) - - iris.Text({"Hit"}) - - iris.InputColor3({"Debug_HitColor"}, {number=VisualizeCastSettingSt.Debug_HitColor}) - iris.InputNum({"Debug_HitTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_HitTransparency}) - iris.InputNum({"Debug_HitSize", 0.5, 0.5, 5}, {number=VisualizeCastSettingSt.Debug_HitSize}) - - iris.Text({"Raypierce"}) - - iris.InputColor3({"Debug_RayPierceColor"}, {number=VisualizeCastSettingSt.Debug_RayPierceColor}) - iris.InputNum({"Debug_RayPierceTransparency", 0.1, 0, 1}, {number=VisualizeCastSettingSt.Debug_RayPierceTransparency}) - iris.InputNum({"Debug_RayPierceSize", 0.5, 0.5, 5}, {number=VisualizeCastSettingSt.Debug_RayPierceSize}) - - iris.Text({"Lifetime"}) - iris.InputNum({"Debug_RayLifetime"}, {number=VisualizeCastSettingSt.Debug_RayLifetime}) - iris.InputNum({"Debug_HitLifetime"}, {number=VisualizeCastSettingSt.Debug_HitLifetime}) - iris.End() - - if iris.Button("Update Client").clicked() then - print("Updated FastCastBehaviorClient") - UpdateClientBehavior() - end - - if iris.Button("Update Server").clicked() then - print("Updated FastCastBehaviorServer") - UpdateServerBehavior() - end - - iris.End() - - iris.Tree({"Caster"}) - do - iris.Checkbox({"ObjectCache Enabled"}, {isChecked = ObjectEnabledValue}) - if ObjectEnabledValue.value == true then - if not Caster.ObjectCacheEnabled then - Caster:SetObjectCacheEnabled(true, ProjectileTemplate, DEFAULT_CACHE_SIZE, ProjectileContainer) - Caster.CastTerminating = OnCastTerminating_ObjectCache - end - else - if Caster.ObjectCacheEnabled then - Caster:SetObjectCacheEnabled(false) - Caster.CastTerminating = OnCastTerminating - end - end - - iris.Checkbox({"BulkMoveTo"}, {isChecked = BulkMoveEnabledValue}) - if BulkMoveEnabledValue.value == true then - if not Caster.BulkMoveEnabled then - Caster:SetBulkMoveEnabled(true) - end - else - if Caster.BulkMoveEnabled then - Caster:SetBulkMoveEnabled(false) - end - end - end - iris.End() - - iris.Tree({"Logging"}) - if iris.Button({"FastCastBehavior Client"}).clicked() then - print(CastBehaviorClient) - end - - if iris.Button({"FastCastBehavior Server"}).clicked() then - LoggingServer:Fire("FastCastBehavior") - end - - if iris.Button({"Server Setting"}).clicked() then - LoggingServer:Fire("Setting") - end - - iris.End() - - iris.Tree("Performance Test") - iris.InputVector3({"Origin"}, {number=OriginValue}) - iris.InputVector3({"Direction"}, {number=DirectionValue}) - do - iris.Checkbox({"Client Test"}, {isChecked=P_ClientTestValue}) - iris.Checkbox({"Server Test"}, {isChecked=P_ServerTestValue}) - - iris.Checkbox({"Fire Projectiles"}, {isChecked=P_TestValue}) - - if P_TestValue.value == true then - if ClientProjectileCount < ClientProjectileLimitValue.value and P_ClientTestValue.value == true then - CasterFire(CastTypeState.value, OriginValue.value, arg, DirectionValue.value, VelocityValue.value, CastBehaviorClient) - IntCount(1) - end - - if P_ServerTestValue.value == true then - ServerProjectile:FireUnreliable(OriginValue.value, DirectionValue.value) - end - end - end - - iris.End() - - iris.Tree("FastCastEventsModule") - --[[ - S1 = nil - - A1 - A2 - ]] - for key, moduleScript in FastCastEventsModules do - local isThisChecked = (SelcetedModule == key) - - local checkbox = iris.Checkbox({key}, {isChecked = isThisChecked}) - if checkbox.checked() then - SelcetedModule = key - Caster:SetFastCastEventsModule(moduleScript) - end - - if checkbox.unchecked() then - SelcetedModule = nil - Caster:SetFastCastEventsModule(nil) - end - end - - if iris.Button("Update Server").clicked() then - ServerCastModuleUpdate:Fire(Caster.FastCastEventsModule) - end - iris.End() - - iris.Tree("TestModules") - for key, state in SelectedTests do - local checkbox = iris.Checkbox({key}, {isChecked = state}) - - if checkbox.checked() then - TestModules[key].Start(IntCountEvent, Caster, VelocityValue.value, CastBehaviorClient) - end - - if checkbox.unchecked() then - TestModules[key].Stop() - end - end - - if iris.Button("Update TestModules").clicked() then - for key, state in SelectedTests do - if state.value == true then - TestModules[key].Update(Caster, VelocityValue.value, CastBehaviorClient) - end - end + -- -------------------------------------------------------- + -- FastCastBehavior + -- -------------------------------------------------------- + iris.Tree({ "FastCastBehavior" }) + iris.Tree({ "HighFidelityBehavior" }) + for i = 1, 3 do + iris.RadioButton({ HIGH_FIDELITY_NAMES[i], i }, { index = SelectedBehavior }, i) + end + iris.End() + + iris.InputVector3({ "Acceleration" }, { number = AccelerationState }) + iris.Checkbox({ "AutoIgnoreContainer" }, { isChecked = AutoIgnoreContainerState }) + iris.InputNum({ "MaxDistance" }, { number = MaxDistanceState }) + iris.Checkbox({ "VisualizeCasts" }, { isChecked = VisualizeCastsState }) + iris.InputNum({ "HighFidelitySegmentSize", 0.1, 0.1, 2 }, { number = HighFidelitySegmentSizeState }) + iris.Checkbox({ "Use CosmeticBulletTemplate" }, { isChecked = UseCosmeticBulletTemplate }) + iris.Checkbox({ "SimulateAfterPhysic" }, { isChecked = SimulateAfterPhysicState }) + + iris.Tree({ "FastCastEventsConfig" }) + for key, state in FastCastEventsConfigStates do + iris.Checkbox({ key }, { isChecked = state }) + end + iris.End() + + iris.Tree({ "FastCastEventsModuleConfig" }) + for key, state in FastCastEventsModuleConfigStates do + iris.Checkbox({ key }, { isChecked = state }) + end + iris.End() + + iris.Checkbox({ "Automatic Performance" }, { isChecked = AutomaticPerformanceState }) + + iris.Tree({ "Adaptive Performance" }) + iris.InputNum({ "HighFidelitySegmentSizeIncrease", 0.1, 0.1, 2 }, { number = AdaptivePerformanceStates.HighFidelitySegmentSizeIncrease }) + iris.Checkbox({ "LowerHighFidelityBehavior" }, { isChecked = AdaptivePerformanceStates.LowerHighFidelityBehavior }) + iris.End() + + iris.Tree({ "VisualizeCastSettings" }) + iris.Text({ "-- Segment --" }) + iris.InputColor3({ "Debug_SegmentColor" }, { number = VisualizeCastSettingSt.Debug_SegmentColor }) + iris.InputNum({ "Debug_SegmentTransparency", 0.1, 0, 1 }, { number = VisualizeCastSettingSt.Debug_SegmentTransparency }) + iris.InputNum({ "Debug_SegmentSize", 0.05, 0.05, 2 }, { number = VisualizeCastSettingSt.Debug_SegmentSize }) + + iris.Text({ "-- Hit --" }) + iris.InputColor3({ "Debug_HitColor" }, { number = VisualizeCastSettingSt.Debug_HitColor }) + iris.InputNum({ "Debug_HitTransparency", 0.1, 0, 1 }, { number = VisualizeCastSettingSt.Debug_HitTransparency }) + iris.InputNum({ "Debug_HitSize", 0.5, 0.5, 5 }, { number = VisualizeCastSettingSt.Debug_HitSize }) + + iris.Text({ "-- Ray Pierce --" }) + iris.InputColor3({ "Debug_RayPierceColor" }, { number = VisualizeCastSettingSt.Debug_RayPierceColor }) + iris.InputNum({ "Debug_RayPierceTransparency", 0.1, 0, 1 }, { number = VisualizeCastSettingSt.Debug_RayPierceTransparency }) + iris.InputNum({ "Debug_RayPierceSize", 0.5, 0.5, 5 }, { number = VisualizeCastSettingSt.Debug_RayPierceSize }) + + iris.Text({ "-- Lifetime --" }) + iris.InputNum({ "Debug_RayLifetime" }, { number = VisualizeCastSettingSt.Debug_RayLifetime }) + iris.InputNum({ "Debug_HitLifetime" }, { number = VisualizeCastSettingSt.Debug_HitLifetime }) + iris.End() + + if iris.Button({ "Update Client Behavior" }).clicked() then + buildClientBehavior() + print("[IrisLocal] Client behavior updated") + end + if iris.Button({ "Update Server Behavior" }).clicked() then + buildServerBehavior() + print("[IrisLocal] Server behavior updated") + end + iris.End() + + -- -------------------------------------------------------- + -- Caster (ObjectCache / BulkMove) + -- -------------------------------------------------------- + iris.Tree({ "Caster" }) + iris.Checkbox({ "ObjectCache Enabled" }, { isChecked = ObjectEnabledValue }) + if ObjectEnabledValue.value then + if not Caster.ObjectCacheEnabled then + Caster:SetObjectCacheEnabled(true, ProjectileTemplate, DEFAULT_CACHE_SIZE, ProjectileContainer) + Caster.CastTerminating = onCastTerminatingObjectCache + end + else + if Caster.ObjectCacheEnabled then + Caster:SetObjectCacheEnabled(false) + Caster.CastTerminating = onCastTerminating + end + end + + iris.Checkbox({ "BulkMoveTo" }, { isChecked = BulkMoveEnabledValue }) + if BulkMoveEnabledValue.value then + if not Caster.BulkMoveEnabled then Caster:SetBulkMoveEnabled(true) end + else + if Caster.BulkMoveEnabled then Caster:SetBulkMoveEnabled(false) end + end + iris.End() + + -- -------------------------------------------------------- + -- Logging + -- -------------------------------------------------------- + iris.Tree({ "Logging" }) + if iris.Button({ "Print Client Behavior" }).clicked() then + print(CastBehaviorClient) + end + if iris.Button({ "Print Server Behavior" }).clicked() then + LoggingServer:Fire("FastCastBehavior") + end + if iris.Button({ "Print Server Settings" }).clicked() then + LoggingServer:Fire("Setting") + end + iris.End() + + -- -------------------------------------------------------- + -- Performance Test + -- -------------------------------------------------------- + iris.Tree({ "Performance Test" }) + iris.InputVector3({ "Origin" }, { number = OriginValue }) + iris.InputVector3({ "Direction" }, { number = DirectionValue }) + iris.Checkbox({ "Client Test" }, { isChecked = P_ClientTestValue }) + iris.Checkbox({ "Server Test" }, { isChecked = P_ServerTestValue }) + iris.Checkbox({ "Fire Projectiles" }, { isChecked = P_TestValue }) + + if P_TestValue.value then + if P_ClientTestValue.value and ClientProjectileCount < ClientProjectileLimitValue.value then + casterFire(CastTypeState.value, OriginValue.value, nil, DirectionValue.value, VelocityValue.value, CastBehaviorClient) + ClientProjectileCount += 1 + end + if P_ServerTestValue.value then + ServerProjectile:FireUnreliable(OriginValue.value, DirectionValue.value) + end + end + iris.End() + + -- -------------------------------------------------------- + -- FastCastEventsModule + -- -------------------------------------------------------- + iris.Tree({ "FastCastEventsModule" }) + for key, moduleScript in FastCastEventsModules do + local isSelected = iris.State(SelectedModuleKey == key) + local checkbox = iris.Checkbox({ key }, { isChecked = isSelected }) + + if checkbox.checked() then + SelectedModuleKey = key + Caster:SetFastCastEventsModule(moduleScript) + elseif checkbox.unchecked() then + SelectedModuleKey = nil + Caster:SetFastCastEventsModule(nil) + end + end + + if iris.Button({ "Sync Module to Server" }).clicked() then + ServerCastModuleUpdate:Fire(Caster.FastCastEventsModule) + end + iris.End() + + -- -------------------------------------------------------- + -- Test Modules + -- -------------------------------------------------------- + iris.Tree({ "Test Modules" }) + for key, state in SelectedTests do + local checkbox = iris.Checkbox({ key }, { isChecked = state }) + + if checkbox.checked() then + TestModules[key].Start(IntCountEvent, Caster, VelocityValue.value, CastBehaviorClient) + elseif checkbox.unchecked() then + TestModules[key].Stop() + end + end + + if iris.Button({ "Update Running Tests" }).clicked() then + for key, state in SelectedTests do + if state.value then + TestModules[key].Update(Caster, VelocityValue.value, CastBehaviorClient) end - iris.End() - - iris.Text({"Client projectile count : " .. ClientProjectileCount}) - iris.Text({"Server projectile count : " .. ServerProjectileCount}) - + end + end iris.End() + + -- -------------------------------------------------------- + -- Projectile Counters (live via RemoteTable) + -- -------------------------------------------------------- + iris.Separator() + iris.Text({ "Client projectiles: " .. ClientProjectileCount }) + iris.Text({ "Server projectiles: " .. (serverState and serverState.projectileCount or "?") }) + + iris.End() -- Window end) -game:GetService("UserInputService").InputEnded:Connect(function(input: InputObject, gp: boolean) +-- ============================================================ +-- Keybind: E to toggle window +-- ============================================================ + +UserInputService.InputEnded:Connect(function(input: InputObject, gp: boolean) if gp then return end if input.KeyCode == Enum.KeyCode.E then WindowVisible:set(not WindowVisible:get()) end end) - --- Event Connections - -ServerProjectileCountEvent:Connect(function(newCount: number) - ServerProjectileCount = newCount -end) \ No newline at end of file From bd2318dc85b5664a2adff794207e852aa384d13b Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:16:49 +0700 Subject: [PATCH 10/22] Claude: Refactor IrisServer --- src/DebuggerUI/Server/IrisServer.server.luau | 206 ++++++++++++------- 1 file changed, 131 insertions(+), 75 deletions(-) diff --git a/src/DebuggerUI/Server/IrisServer.server.luau b/src/DebuggerUI/Server/IrisServer.server.luau index c960b1b..dd0c87d 100644 --- a/src/DebuggerUI/Server/IrisServer.server.luau +++ b/src/DebuggerUI/Server/IrisServer.server.luau @@ -1,40 +1,68 @@ +--!strict +--[[ + Author: Mawin_CK + Date: 2025 + Note: Studio DebuggerUI server script. +]] + -- Services local Rep = game:GetService("ReplicatedStorage") local SSS = game:GetService("ServerScriptService") +local Players = game:GetService("Players") -- Modules local FastCast2 = Rep:WaitForChild("FastCast2") - --- Requires local FastCastM = require(FastCast2) local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) local Jolt = require(Rep:WaitForChild("Jolt")) +local RemoteTable = require(Rep:WaitForChild("RemoteTableLight")).Server +-- ============================================================ -- Types -type RequestLogDataType = "FastCastBehavior" | "Setting" +-- ============================================================ --- Variables -local currentVelocity = 50 -local ServerProjectileLimit = 1000 -local scriptController: LocalScript = nil +type LogRequest = "FastCastBehavior" | "Setting" -local debounce_lc = false -local debounce_lc_time = 1.5 +type DebuggerState = { + velocity: number, + projectileLimit: number, + projectileCount: number, +} -local ServerProjectileCount = 0 +-- ============================================================ +-- Constants +-- ============================================================ --- Events -local CastBehaviorServerUpdate = Jolt.Server("CastBehaviorServerUpdate") :: Jolt.Server -local LoggingServer = Jolt.Server("LoggingServer") :: Jolt.Server -local ServerSettingUpdate = Jolt.Server("ServerSettingUpdate") :: Jolt.Server<{velocity: number, projectileLimit: number}> -local ServerProjectile = Jolt.Server("ServerProjectile") :: Jolt.Server -local ServerProjectileCountEvent = Jolt.Server("ServerProjectileCount") :: Jolt.Server -local ServerCastModuleUpdate = Jolt.Server("ServerCastModuleUpdate") :: Jolt.Server +local DEBOUNCE_LC_TIME = 1.5 +local DEBUGGER_TOKEN = "DebuggerState" + +-- ============================================================ +-- State +-- ============================================================ +local state: DebuggerState = { + velocity = 50, + projectileLimit = 1000, + projectileCount = 0, +} + +-- ============================================================ +-- RemoteTable — server → client state sync +-- ============================================================ + +local debuggerData = RemoteTable.Create(DEBUGGER_TOKEN, state) + +local function setState(key: string, value: any) + state[key] = value + RemoteTable.Set(debuggerData, key, value) +end +-- ============================================================ -- Behavior +-- ============================================================ + local CastBehaviorServer: FastCastTypes.FastCastBehavior = FastCastM.newBehavior() -CastBehaviorServer.VisualizeCasts = false -- Explictly set to false to avoid confusion. +CastBehaviorServer.VisualizeCasts = false CastBehaviorServer.Acceleration = Vector3.new() CastBehaviorServer.AutoIgnoreContainer = true CastBehaviorServer.MaxDistance = 1000 @@ -45,7 +73,7 @@ CastBehaviorServer.FastCastEventsConfig = { UseRayHit = true, UseCastTerminating = true, UseCastFire = false, - UseRayPierced = false + UseRayPierced = false, } CastBehaviorServer.FastCastEventsModuleConfig = { @@ -54,60 +82,64 @@ CastBehaviorServer.FastCastEventsModuleConfig = { UseCastTerminating = true, UseCastFire = false, UseRayPierced = false, - UseCanRayPierce = false + UseCanRayPierce = false, } --- Local functions - -local function IntCount(amount: number) - ServerProjectileCount += amount - ServerProjectileCountEvent:FireAllUnreliable(ServerProjectileCount) -end - +-- ============================================================ -- Caster +-- ============================================================ + local Caster = FastCastM.new() -Caster:Init( - 4, - SSS, - "CastVMs", - SSS, - "VMContainer", - "CastVM" -) +Caster:Init(4, SSS, "CastVMs", SSS, "VMContainer", "CastVM") + +local debounce_lc = false Caster.CastTerminating = function() - IntCount(-1) + setState("projectileCount", state.projectileCount - 1) end + Caster.CastFire = function() - print("CastFire Test!") + print("[IrisServer] CastFire") end -Caster.Hit = function(cast, result) - print("RayHit Test!") + +Caster.Hit = function(_cast: FastCastTypes.ActiveCastCompement, _result: RaycastResult) + print("[IrisServer] RayHit") end + Caster.LengthChanged = function() - if not debounce_lc then - debounce_lc = true - print("OnLengthChanged Test!") - task.delay(debounce_lc_time, function() - debounce_lc = false - end) - end + if debounce_lc then return end + debounce_lc = true + print("[IrisServer] LengthChanged") + task.delay(DEBOUNCE_LC_TIME, function() + debounce_lc = false + end) end + Caster.Pierced = function() - print("Ray pierced Test!") + print("[IrisServer] Pierced") end --- Connections +-- ============================================================ +-- Jolt Events (client → server commands) +-- ============================================================ -LoggingServer:Connect(function(player: Player, RequestLogDataType: RequestLogDataType) - if RequestLogDataType == "FastCastBehavior" then - print(CastBehaviorServer) - end - - if RequestLogDataType == "Setting" then - print({ - velocity = currentVelocity, - projectileLimit = ServerProjectileLimit +local CastBehaviorServerUpdate = Jolt.Server("CastBehaviorServerUpdate") :: Jolt.Server +local LoggingServer = Jolt.Server("LoggingServer") :: Jolt.Server +local ServerSettingUpdate = Jolt.Server("ServerSettingUpdate") :: Jolt.Server<{ velocity: number, projectileLimit: number }> +local ServerProjectile = Jolt.Server("ServerProjectile") :: Jolt.Server +local ServerCastModuleUpdate = Jolt.Server("ServerCastModuleUpdate") :: Jolt.Server + +-- ============================================================ +-- Connections +-- ============================================================ + +LoggingServer:Connect(function(_player: Player, request: LogRequest) + if request == "FastCastBehavior" then + print("[IrisServer] CastBehaviorServer:", CastBehaviorServer) + elseif request == "Setting" then + print("[IrisServer] Settings:", { + velocity = state.velocity, + projectileLimit = state.projectileLimit, }) end end) @@ -116,30 +148,54 @@ CastBehaviorServerUpdate:Connect(function(player: Player, newBehavior: FastCastT for key, value in newBehavior do CastBehaviorServer[key] = value end - if CastBehaviorServer.CosmeticBulletTemplate then - CastBehaviorServer.CosmeticBulletTemplate = nil - end + + -- Server never uses a cosmetic template + CastBehaviorServer.CosmeticBulletTemplate = nil + + -- Always ensure valid RaycastParams exist if not CastBehaviorServer.RaycastParams then - local CastParams = RaycastParams.new() - CastParams.IgnoreWater = true - CastParams.FilterType = Enum.RaycastFilterType.Exclude - CastParams.FilterDescendantsInstances = {player.Character, workspace:WaitForChild("Projectiles")} + local castParams = RaycastParams.new() + castParams.IgnoreWater = true + castParams.FilterType = Enum.RaycastFilterType.Exclude + castParams.FilterDescendantsInstances = { + player.Character :: any, + workspace:WaitForChild("Projectiles"), + } + CastBehaviorServer.RaycastParams = castParams -- was missing in original! end end) -ServerSettingUpdate:Connect(function(player: Player, data) - currentVelocity = data.velocity - ServerProjectileLimit = data.projectileLimit +ServerSettingUpdate:Connect(function(_player: Player, data: { velocity: number, projectileLimit: number }) + setState("velocity", data.velocity) + setState("projectileLimit", data.projectileLimit) end) -ServerProjectile:Connect(function(player: Player, Origin: Vector3, Direction: Vector3, velocity: number?) - if ServerProjectileCount >= ServerProjectileLimit then - return - end - Caster:RaycastFire(Origin, Direction, velocity or currentVelocity, CastBehaviorServer) - IntCount(1) +ServerProjectile:Connect(function(_player: Player, origin: Vector3, direction: Vector3, velocity: number?) + if state.projectileCount >= state.projectileLimit then return end + Caster:RaycastFire(origin, direction, velocity or state.velocity, CastBehaviorServer) + setState("projectileCount", state.projectileCount + 1) end) -ServerCastModuleUpdate:Connect(function(player: Player, moduleScript: ModuleScript) +ServerCastModuleUpdate:Connect(function(_player: Player, moduleScript: ModuleScript) Caster:SetFastCastEventsModule(moduleScript) -end) \ No newline at end of file +end) + +-- ============================================================ +-- Player lifecycle — grant / revoke RemoteTable access +-- ============================================================ + +local function onPlayerAdded(player: Player) + RemoteTable.AddClient(DEBUGGER_TOKEN, player) +end + +local function onPlayerRemoving(player: Player) + RemoteTable.RemoveClient(DEBUGGER_TOKEN, player) +end + +Players.PlayerAdded:Connect(onPlayerAdded) +Players.PlayerRemoving:Connect(onPlayerRemoving) + +-- Catch players who joined before this script ran +for _, player in Players:GetPlayers() do + onPlayerAdded(player) +end From 06834da2b2ea3aed5dde77d6854d0fe0ac94e053 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:30:38 +0700 Subject: [PATCH 11/22] Update sourcemap.json --- default.project.json | 12 ++++++++++++ sourcemap.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/default.project.json b/default.project.json index 8c80496..af6b382 100644 --- a/default.project.json +++ b/default.project.json @@ -2,11 +2,23 @@ "name": "FastCast2", "tree": { "$className": "DataModel", + "$ignoreUnknownInstances": true, + "ReplicatedStorage": { "FastCast2": { "$path": "src/FastCast2" }, "$path": "src/DebuggerUI/Shared" + }, + + "ServerScriptService":{ + "$path": "src/DebuggerUI/Server" + }, + + "StarterPlayer":{ + "StarterPlayerScripts":{ + "$path": "src/Debugger/Client" + } } } } \ No newline at end of file diff --git a/sourcemap.json b/sourcemap.json index 9c0cfcd..eb9e1ae 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"External","className":"Folder","children":[{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Window.luau"]}]}]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"External","className":"Folder","children":[{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Window.luau"]}]}]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts"}]}]} \ No newline at end of file From 50c0e89b7e7356e8144ae707d11f3cd94f908740 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:31:27 +0700 Subject: [PATCH 12/22] Update sourcemap.json --- sourcemap.json | 2 +- src/DebuggerUI/Shared/{External => }/Jolt/Bridge.luau | 0 src/DebuggerUI/Shared/{External => }/Jolt/Client.luau | 0 src/DebuggerUI/Shared/{External => }/Jolt/Server.luau | 0 src/DebuggerUI/Shared/{External => }/Jolt/Utils/Buffers.luau | 0 src/DebuggerUI/Shared/{External => }/Jolt/Utils/Remotes.luau | 0 .../Shared/{External => }/Jolt/Utils/Remotes.meta.json | 0 src/DebuggerUI/Shared/{External => }/Jolt/init.luau | 0 .../Shared/{External => }/RemoteTableLight/Client.luau | 0 .../Shared/{External => }/RemoteTableLight/Server.luau | 0 .../Shared/{External => }/RemoteTableLight/Shared/LICENSE.luau | 0 .../{External => }/RemoteTableLight/Shared/Packet/Signal.luau | 0 .../{External => }/RemoteTableLight/Shared/Packet/Task.luau | 0 .../RemoteTableLight/Shared/Packet/Types/Characters.luau | 0 .../RemoteTableLight/Shared/Packet/Types/Enums.luau | 0 .../RemoteTableLight/Shared/Packet/Types/Static1.luau | 0 .../RemoteTableLight/Shared/Packet/Types/Static2.luau | 0 .../RemoteTableLight/Shared/Packet/Types/Static3.luau | 0 .../RemoteTableLight/Shared/Packet/Types/init.luau | 0 .../{External => }/RemoteTableLight/Shared/Packet/init.luau | 0 .../Shared/{External => }/RemoteTableLight/Shared/Packets.luau | 0 .../{External => }/RemoteTableLight/Shared/PromiseLight.luau | 0 .../{External => }/RemoteTableLight/Shared/TokenRegistry.luau | 0 .../Shared/{External => }/RemoteTableLight/Shared/Util.luau | 0 .../Shared/{External => }/RemoteTableLight/Shared/VERSIONS.luau | 0 .../Shared/{External => }/RemoteTableLight/Shared/Zignal.luau | 0 src/DebuggerUI/Shared/{External => }/RemoteTableLight/init.luau | 0 src/DebuggerUI/Shared/{External => }/iris/API.luau | 0 src/DebuggerUI/Shared/{External => }/iris/Internal.luau | 0 src/DebuggerUI/Shared/{External => }/iris/PubTypes.luau | 0 src/DebuggerUI/Shared/{External => }/iris/Types.luau | 0 src/DebuggerUI/Shared/{External => }/iris/WidgetTypes.luau | 0 src/DebuggerUI/Shared/{External => }/iris/config.luau | 0 src/DebuggerUI/Shared/{External => }/iris/demoWindow.luau | 0 src/DebuggerUI/Shared/{External => }/iris/init.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Button.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Checkbox.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Combo.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Format.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Image.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Input.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Menu.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Plot.luau | 0 .../Shared/{External => }/iris/widgets/RadioButton.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Root.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Tab.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Table.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Text.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Tree.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/Window.luau | 0 src/DebuggerUI/Shared/{External => }/iris/widgets/init.luau | 0 51 files changed, 1 insertion(+), 1 deletion(-) rename src/DebuggerUI/Shared/{External => }/Jolt/Bridge.luau (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/Client.luau (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/Server.luau (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/Utils/Buffers.luau (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/Utils/Remotes.luau (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/Utils/Remotes.meta.json (100%) rename src/DebuggerUI/Shared/{External => }/Jolt/init.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Client.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Server.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/LICENSE.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Signal.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Task.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/Characters.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/Enums.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/Static1.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/Static2.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/Static3.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/Types/init.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packet/init.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Packets.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/PromiseLight.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/TokenRegistry.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Util.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/VERSIONS.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/Shared/Zignal.luau (100%) rename src/DebuggerUI/Shared/{External => }/RemoteTableLight/init.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/API.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/Internal.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/PubTypes.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/Types.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/WidgetTypes.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/config.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/demoWindow.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/init.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Button.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Checkbox.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Combo.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Format.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Image.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Input.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Menu.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Plot.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/RadioButton.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Root.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Tab.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Table.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Text.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Tree.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/Window.luau (100%) rename src/DebuggerUI/Shared/{External => }/iris/widgets/init.luau (100%) diff --git a/sourcemap.json b/sourcemap.json index eb9e1ae..d5b1cb1 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"External","className":"Folder","children":[{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/External/iris/widgets/Window.luau"]}]}]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts"}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts"}]}]} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/External/Jolt/Bridge.luau b/src/DebuggerUI/Shared/Jolt/Bridge.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Bridge.luau rename to src/DebuggerUI/Shared/Jolt/Bridge.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Client.luau b/src/DebuggerUI/Shared/Jolt/Client.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Client.luau rename to src/DebuggerUI/Shared/Jolt/Client.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Server.luau b/src/DebuggerUI/Shared/Jolt/Server.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Server.luau rename to src/DebuggerUI/Shared/Jolt/Server.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau b/src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Utils/Buffers.luau rename to src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau b/src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.luau rename to src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau diff --git a/src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json b/src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/Utils/Remotes.meta.json rename to src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json diff --git a/src/DebuggerUI/Shared/External/Jolt/init.luau b/src/DebuggerUI/Shared/Jolt/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/Jolt/init.luau rename to src/DebuggerUI/Shared/Jolt/init.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau b/src/DebuggerUI/Shared/RemoteTableLight/Client.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Client.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Client.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau b/src/DebuggerUI/Shared/RemoteTableLight/Server.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Server.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Server.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/LICENSE.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Signal.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Task.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Characters.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Enums.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static1.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static2.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/Static3.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/Types/init.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packet/init.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Packets.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/PromiseLight.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/TokenRegistry.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Util.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/VERSIONS.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/Shared/Zignal.luau rename to src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau diff --git a/src/DebuggerUI/Shared/External/RemoteTableLight/init.luau b/src/DebuggerUI/Shared/RemoteTableLight/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/RemoteTableLight/init.luau rename to src/DebuggerUI/Shared/RemoteTableLight/init.luau diff --git a/src/DebuggerUI/Shared/External/iris/API.luau b/src/DebuggerUI/Shared/iris/API.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/API.luau rename to src/DebuggerUI/Shared/iris/API.luau diff --git a/src/DebuggerUI/Shared/External/iris/Internal.luau b/src/DebuggerUI/Shared/iris/Internal.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/Internal.luau rename to src/DebuggerUI/Shared/iris/Internal.luau diff --git a/src/DebuggerUI/Shared/External/iris/PubTypes.luau b/src/DebuggerUI/Shared/iris/PubTypes.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/PubTypes.luau rename to src/DebuggerUI/Shared/iris/PubTypes.luau diff --git a/src/DebuggerUI/Shared/External/iris/Types.luau b/src/DebuggerUI/Shared/iris/Types.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/Types.luau rename to src/DebuggerUI/Shared/iris/Types.luau diff --git a/src/DebuggerUI/Shared/External/iris/WidgetTypes.luau b/src/DebuggerUI/Shared/iris/WidgetTypes.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/WidgetTypes.luau rename to src/DebuggerUI/Shared/iris/WidgetTypes.luau diff --git a/src/DebuggerUI/Shared/External/iris/config.luau b/src/DebuggerUI/Shared/iris/config.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/config.luau rename to src/DebuggerUI/Shared/iris/config.luau diff --git a/src/DebuggerUI/Shared/External/iris/demoWindow.luau b/src/DebuggerUI/Shared/iris/demoWindow.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/demoWindow.luau rename to src/DebuggerUI/Shared/iris/demoWindow.luau diff --git a/src/DebuggerUI/Shared/External/iris/init.luau b/src/DebuggerUI/Shared/iris/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/init.luau rename to src/DebuggerUI/Shared/iris/init.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Button.luau b/src/DebuggerUI/Shared/iris/widgets/Button.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Button.luau rename to src/DebuggerUI/Shared/iris/widgets/Button.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau b/src/DebuggerUI/Shared/iris/widgets/Checkbox.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Checkbox.luau rename to src/DebuggerUI/Shared/iris/widgets/Checkbox.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Combo.luau b/src/DebuggerUI/Shared/iris/widgets/Combo.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Combo.luau rename to src/DebuggerUI/Shared/iris/widgets/Combo.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Format.luau b/src/DebuggerUI/Shared/iris/widgets/Format.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Format.luau rename to src/DebuggerUI/Shared/iris/widgets/Format.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Image.luau b/src/DebuggerUI/Shared/iris/widgets/Image.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Image.luau rename to src/DebuggerUI/Shared/iris/widgets/Image.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Input.luau b/src/DebuggerUI/Shared/iris/widgets/Input.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Input.luau rename to src/DebuggerUI/Shared/iris/widgets/Input.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Menu.luau b/src/DebuggerUI/Shared/iris/widgets/Menu.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Menu.luau rename to src/DebuggerUI/Shared/iris/widgets/Menu.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Plot.luau b/src/DebuggerUI/Shared/iris/widgets/Plot.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Plot.luau rename to src/DebuggerUI/Shared/iris/widgets/Plot.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau b/src/DebuggerUI/Shared/iris/widgets/RadioButton.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/RadioButton.luau rename to src/DebuggerUI/Shared/iris/widgets/RadioButton.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Root.luau b/src/DebuggerUI/Shared/iris/widgets/Root.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Root.luau rename to src/DebuggerUI/Shared/iris/widgets/Root.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Tab.luau b/src/DebuggerUI/Shared/iris/widgets/Tab.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Tab.luau rename to src/DebuggerUI/Shared/iris/widgets/Tab.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Table.luau b/src/DebuggerUI/Shared/iris/widgets/Table.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Table.luau rename to src/DebuggerUI/Shared/iris/widgets/Table.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Text.luau b/src/DebuggerUI/Shared/iris/widgets/Text.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Text.luau rename to src/DebuggerUI/Shared/iris/widgets/Text.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Tree.luau b/src/DebuggerUI/Shared/iris/widgets/Tree.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Tree.luau rename to src/DebuggerUI/Shared/iris/widgets/Tree.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/Window.luau b/src/DebuggerUI/Shared/iris/widgets/Window.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/Window.luau rename to src/DebuggerUI/Shared/iris/widgets/Window.luau diff --git a/src/DebuggerUI/Shared/External/iris/widgets/init.luau b/src/DebuggerUI/Shared/iris/widgets/init.luau similarity index 100% rename from src/DebuggerUI/Shared/External/iris/widgets/init.luau rename to src/DebuggerUI/Shared/iris/widgets/init.luau From bb05b138fe37f979ccee31e4062f90d9015533b4 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:35:29 +0700 Subject: [PATCH 13/22] Claude Refactor: ShotTest --- .../Shared/Tests/Client ShotTest.luau | 82 ++++++++++++------- .../Shared/Tests/Server ShotTest.luau | 68 ++++++++++----- 2 files changed, 99 insertions(+), 51 deletions(-) diff --git a/src/DebuggerUI/Shared/Tests/Client ShotTest.luau b/src/DebuggerUI/Shared/Tests/Client ShotTest.luau index 3a5ff3e..f2f4e64 100644 --- a/src/DebuggerUI/Shared/Tests/Client ShotTest.luau +++ b/src/DebuggerUI/Shared/Tests/Client ShotTest.luau @@ -1,60 +1,84 @@ +--!strict -- Services -local UIS = game:GetService("UserInputService") +local UserInputService = game:GetService("UserInputService") local Rep = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") -- Modules local FastCast2 = Rep:WaitForChild("FastCast2") - --- Requires local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) --- Variables -local currentBehavior: FastCastTypes.FastCastBehavior = nil -local currentVelocity: number = 50 -local currentCaster: FastCastTypes.Caster = nil +-- ============================================================ +-- Types +-- ============================================================ -local player = Players.LocalPlayer -local character = player.Character or player.CharacterAdded:Wait() +-- IntCountEvent is a Signal, not an RBXScriptConnection +type IntCountSignal = { Fire: (self: any, amount: number) -> () } -local Head: BasePart = character:WaitForChild("Head") +-- ============================================================ +-- Constants +-- ============================================================ +local DEBOUNCE_TIME = 0.01 + +-- ============================================================ +-- State +-- ============================================================ + +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() +local Head = character:WaitForChild("Head") :: BasePart local mouse = player:GetMouse() -local debounce = false -local debounce_time = 0.01 +local currentBehavior: FastCastTypes.FastCastBehavior = nil :: any +local currentVelocity: number = 50 +local currentCaster: FastCastTypes.Caster = nil :: any +local intCountEvent: IntCountSignal = nil :: any -local connection: RBXScriptConnection = nil +local debounce = false +local connection: RBXScriptConnection? = nil +-- ============================================================ -- Module +-- ============================================================ local module = {} -function module.Start(IntCountEvent: RBXScriptConnection, newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) +function module.Start( + newIntCountEvent: IntCountSignal, + newCaster: FastCastTypes.Caster, + newVelocity: number, + newBehavior: FastCastTypes.FastCastBehavior +) currentBehavior = newBehavior currentVelocity = newVelocity currentCaster = newCaster - - connection = UIS.InputBegan:Connect(function(Input: InputObject, gp: boolean) - if gp then return end - if debounce then return end + intCountEvent = newIntCountEvent + + connection = UserInputService.InputBegan:Connect(function(input: InputObject, gp: boolean) + if gp or debounce then return end - if Input.UserInputType == Enum.UserInputType.MouseButton1 then + if input.UserInputType == Enum.UserInputType.MouseButton1 then debounce = true - - local Origin = Head.Position - local Direction = (mouse.Hit.Position - Origin).Unit - - newCaster:RaycastFire(Origin, Direction, currentVelocity, currentBehavior) - IntCountEvent:Fire(1) - - task.wait(debounce_time) - debounce = false + + local origin = Head.Position + local direction = (mouse.Hit.Position - origin).Unit + + currentCaster:RaycastFire(origin, direction, currentVelocity, currentBehavior) + intCountEvent:Fire(1) + + task.delay(DEBOUNCE_TIME, function() + debounce = false + end) end end) end -function module.Update(newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) +function module.Update( + newCaster: FastCastTypes.Caster, + newVelocity: number, + newBehavior: FastCastTypes.FastCastBehavior +) currentCaster = newCaster currentVelocity = newVelocity currentBehavior = newBehavior diff --git a/src/DebuggerUI/Shared/Tests/Server ShotTest.luau b/src/DebuggerUI/Shared/Tests/Server ShotTest.luau index 67a5d75..967b5eb 100644 --- a/src/DebuggerUI/Shared/Tests/Server ShotTest.luau +++ b/src/DebuggerUI/Shared/Tests/Server ShotTest.luau @@ -1,56 +1,80 @@ +--!strict -- Services -local UIS = game:GetService("UserInputService") +local UserInputService = game:GetService("UserInputService") local Rep = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") -- Modules local FastCast2 = Rep:WaitForChild("FastCast2") - --- Requires local FastCastTypes = require(FastCast2:WaitForChild("TypeDefinitions")) local Jolt = require(Rep:WaitForChild("Jolt")) --- Variables -local player = Players.LocalPlayer -local character = player.Character or player.CharacterAdded:Wait() +-- ============================================================ +-- Constants +-- ============================================================ + +local DEBOUNCE_TIME = 0.01 -local Head: BasePart = character:WaitForChild("Head") +-- ============================================================ +-- State +-- ============================================================ +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() +local Head = character:WaitForChild("Head") :: BasePart local mouse = player:GetMouse() +-- Kept as a module-level variable so Update() can refresh it +local currentVelocity: number = 50 + local debounce = false -local debounce_time = 0.01 +local connection: RBXScriptConnection? = nil -local connection: RBXScriptConnection = nil +-- ============================================================ +-- Jolt Events +-- ============================================================ --- Events local ServerProjectile = Jolt.Client("ServerProjectile") :: Jolt.Client +-- ============================================================ -- Module +-- ============================================================ local module = {} -function module.Start() - connection = UIS.InputBegan:Connect(function(Input: InputObject, gp: boolean) - if gp then return end - if debounce then return end +function module.Start( + _intCountEvent: any, -- unused for server shots; server tracks its own count + _caster: FastCastTypes.Caster, + newVelocity: number, + _behavior: FastCastTypes.FastCastBehavior +) + currentVelocity = newVelocity + + connection = UserInputService.InputBegan:Connect(function(input: InputObject, gp: boolean) + if gp or debounce then return end - if Input.UserInputType == Enum.UserInputType.MouseButton1 then + if input.UserInputType == Enum.UserInputType.MouseButton1 then debounce = true - local Origin = Head.Position - local Direction = (mouse.Hit.Position - Origin).Unit + local origin = Head.Position + local direction = (mouse.Hit.Position - origin).Unit - ServerProjectile:Fire(Origin, Direction) + -- Pass velocity so server uses the same speed as the client expects + ServerProjectile:Fire(origin, direction, currentVelocity) - task.wait(debounce_time) - debounce = false + task.delay(DEBOUNCE_TIME, function() + debounce = false + end) end end) end -function module.Update(newCaster: FastCastTypes.Caster, newVelocity: number, newBehavior: FastCastTypes.Behavior) - +function module.Update( + _caster: FastCastTypes.Caster, + newVelocity: number, + _behavior: FastCastTypes.FastCastBehavior +) + currentVelocity = newVelocity end function module.Stop() From b625146baa5bce03639c70253dafa66f3e6d1281 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 16:48:57 +0700 Subject: [PATCH 14/22] CasterFire function error when castType is not raycast --- src/DebuggerUI/Client/IrisLocal.client.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau index 4a2d471..c66f204 100644 --- a/src/DebuggerUI/Client/IrisLocal.client.luau +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -452,7 +452,7 @@ iris:Connect(function() if P_TestValue.value then if P_ClientTestValue.value and ClientProjectileCount < ClientProjectileLimitValue.value then - casterFire(CastTypeState.value, OriginValue.value, nil, DirectionValue.value, VelocityValue.value, CastBehaviorClient) + casterFire(CastTypeState.value, OriginValue.value, castArg, DirectionValue.value, VelocityValue.value, CastBehaviorClient) ClientProjectileCount += 1 end if P_ServerTestValue.value then From 7eca19f3c046656c01bab5edc4a6892e6fce1881 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 17:01:45 +0700 Subject: [PATCH 15/22] Update sourcemap.json --- default.project.json | 2 +- sourcemap.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.project.json b/default.project.json index af6b382..17c123d 100644 --- a/default.project.json +++ b/default.project.json @@ -16,7 +16,7 @@ }, "StarterPlayer":{ - "StarterPlayerScripts":{ + "StarterCharacterScripts":{ "$path": "src/Debugger/Client" } } diff --git a/sourcemap.json b/sourcemap.json index d5b1cb1..d5d0cf3 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts"}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterCharacterScripts","className":"StarterCharacterScripts"}]}]} \ No newline at end of file From f35dacefd0a6254dd158272814a9adce6ecdd6d4 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:41:52 +0700 Subject: [PATCH 16/22] Remove --!strict --- .claude/settings.local.json | 7 +++++++ sourcemap.json | 2 +- src/DebuggerUI/Client/IrisLocal.client.luau | 1 - src/DebuggerUI/Server/IrisServer.server.luau | 1 - 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..da6f1a6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "ask": [ + "Change existing codes." + ] + } +} diff --git a/sourcemap.json b/sourcemap.json index d5d0cf3..71cd8a0 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]},{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterCharacterScripts","className":"StarterCharacterScripts"}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterCharacterScripts","className":"StarterCharacterScripts"}]}]} \ No newline at end of file diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau index c66f204..6366c02 100644 --- a/src/DebuggerUI/Client/IrisLocal.client.luau +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -1,4 +1,3 @@ ---!strict --[[ Author: Mawin_CK Date: 2025 diff --git a/src/DebuggerUI/Server/IrisServer.server.luau b/src/DebuggerUI/Server/IrisServer.server.luau index dd0c87d..67552f7 100644 --- a/src/DebuggerUI/Server/IrisServer.server.luau +++ b/src/DebuggerUI/Server/IrisServer.server.luau @@ -1,4 +1,3 @@ ---!strict --[[ Author: Mawin_CK Date: 2025 From 97121a37a4033de71787fb6a83745b21cb973a65 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:43:51 +0700 Subject: [PATCH 17/22] Fix linting --- src/DebuggerUI/Client/IrisLocal.client.luau | 32 ++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau index 6366c02..81d631a 100644 --- a/src/DebuggerUI/Client/IrisLocal.client.luau +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -146,28 +146,40 @@ IntCountEvent:Connect(function(amount: number) ClientProjectileCount += amount end) -local function onCastTerminating(cast: FastCastTypes.ActiveCastCompement) - local obj = cast.RayInfo.CosmeticBulletObject - if obj then obj:Destroy() end +local function onCastTerminating(cast: FastCastTypes.ActiveCastData) + local obj: Instance? = cast.RayInfo.CosmeticBulletObject :: Instance? + if obj then + obj:Destroy() + end ClientProjectileCount -= 1 end -local function onCastTerminatingObjectCache(cast: FastCastTypes.ActiveCastCompement) - local obj = cast.RayInfo.CosmeticBulletObject - if obj then Caster.ObjectCache:ReturnObject(obj) end +local function onCastTerminatingObjectCache(cast: FastCastTypes.ActiveCastData) + local obj: Instance? = cast.RayInfo.CosmeticBulletObject :: Instance? + if obj then + Caster.ObjectCache:ReturnObject(obj) + end ClientProjectileCount -= 1 end Caster.CastTerminating = onCastTerminating -Caster.CastFire = function() print("[IrisLocal] CastFire") end -Caster.Hit = function() print("[IrisLocal] Hit") end +Caster.CastFire = function() + print("[IrisLocal] CastFire") +end +Caster.Hit = function() + print("[IrisLocal] Hit") +end Caster.LengthChanged = function() if debounce_lc then return end debounce_lc = true print("[IrisLocal] LengthChanged") - task.delay(DEBOUNCE_LC_TIME, function() debounce_lc = false end) + task.delay(DEBOUNCE_LC_TIME, function() + debounce_lc = false + end) +end +Caster.Pierced = function() + print("[IrisLocal] Pierced") end -Caster.Pierced = function() print("[IrisLocal] Pierced") end -- ============================================================ -- Iris States From e06b26e12e6652436f597e5e297a427e74f2e658 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:45:01 +0700 Subject: [PATCH 18/22] Fix claude hallacutation --- src/DebuggerUI/Server/IrisServer.server.luau | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/DebuggerUI/Server/IrisServer.server.luau b/src/DebuggerUI/Server/IrisServer.server.luau index 67552f7..9c58025 100644 --- a/src/DebuggerUI/Server/IrisServer.server.luau +++ b/src/DebuggerUI/Server/IrisServer.server.luau @@ -69,19 +69,19 @@ CastBehaviorServer.HighFidelitySegmentSize = 1 CastBehaviorServer.FastCastEventsConfig = { UseLengthChanged = false, - UseRayHit = true, + UseHit = true, UseCastTerminating = true, UseCastFire = false, - UseRayPierced = false, + UsePierced = false, } CastBehaviorServer.FastCastEventsModuleConfig = { UseLengthChanged = false, - UseRayHit = true, + UseHit = true, UseCastTerminating = true, UseCastFire = false, - UseRayPierced = false, - UseCanRayPierce = false, + UsePierced = false, + UseCanPierce = false, } -- ============================================================ @@ -101,7 +101,7 @@ Caster.CastFire = function() print("[IrisServer] CastFire") end -Caster.Hit = function(_cast: FastCastTypes.ActiveCastCompement, _result: RaycastResult) +Caster.Hit = function(_cast: FastCastTypes.ActiveCastData, _result: RaycastResult) print("[IrisServer] RayHit") end From 179b77440ff18b3f544f9fbe9d45b8002ac0bf01 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:53:05 +0700 Subject: [PATCH 19/22] Delete external license --- selene.toml | 5 +- sourcemap.json | 2 +- .../RemoteTableLight/Shared/LICENSE.luau | 167 ------------------ 3 files changed, 5 insertions(+), 169 deletions(-) delete mode 100644 src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau diff --git a/selene.toml b/selene.toml index 1f1e170..e397732 100644 --- a/selene.toml +++ b/selene.toml @@ -1 +1,4 @@ -std = "roblox" \ No newline at end of file +std = "roblox" +exclude = [ + "src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau" +] \ No newline at end of file diff --git a/sourcemap.json b/sourcemap.json index 71cd8a0..c42626a 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"LICENSE","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau"]},{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterCharacterScripts","className":"StarterCharacterScripts"}]}]} \ No newline at end of file +{"name":"FastCast2","className":"DataModel","filePaths":["default.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"FastCastEventsModule","className":"Folder","children":[{"name":"Default","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/FastCastEventsModule/Default.luau"]}]},{"name":"Jolt","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/init.luau"],"children":[{"name":"Bridge","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Bridge.luau"]},{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Server.luau"]},{"name":"Utils","className":"Folder","children":[{"name":"Buffers","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Buffers.luau"]},{"name":"Remotes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Jolt/Utils/Remotes.luau","src/DebuggerUI/Shared/Jolt/Utils/Remotes.meta.json"]}]}]},{"name":"RemoteTableLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/init.luau"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Client.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Server.luau"]},{"name":"Shared","className":"Folder","children":[{"name":"Packet","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/init.luau"],"children":[{"name":"Signal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Signal.luau"]},{"name":"Task","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Task.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/init.luau"],"children":[{"name":"Characters","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Characters.luau"]},{"name":"Enums","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Enums.luau"]},{"name":"Static1","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static1.luau"]},{"name":"Static2","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static2.luau"]},{"name":"Static3","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packet/Types/Static3.luau"]}]}]},{"name":"Packets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Packets.luau"]},{"name":"PromiseLight","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/PromiseLight.luau"]},{"name":"TokenRegistry","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/TokenRegistry.luau"]},{"name":"Util","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Util.luau"]},{"name":"VERSIONS","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/VERSIONS.luau"]},{"name":"Zignal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/RemoteTableLight/Shared/Zignal.luau"]}]}]},{"name":"Tests","className":"Folder","children":[{"name":"Client ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Client ShotTest.luau"]},{"name":"Server ShotTest","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/Tests/Server ShotTest.luau"]}]},{"name":"iris","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/init.luau"],"children":[{"name":"API","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/API.luau"]},{"name":"Internal","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Internal.luau"]},{"name":"PubTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/PubTypes.luau"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/Types.luau"]},{"name":"WidgetTypes","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/WidgetTypes.luau"]},{"name":"config","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/config.luau"]},{"name":"demoWindow","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/demoWindow.luau"]},{"name":"widgets","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/init.luau"],"children":[{"name":"Button","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Button.luau"]},{"name":"Checkbox","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Checkbox.luau"]},{"name":"Combo","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Combo.luau"]},{"name":"Format","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Format.luau"]},{"name":"Image","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Image.luau"]},{"name":"Input","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Input.luau"]},{"name":"Menu","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Menu.luau"]},{"name":"Plot","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Plot.luau"]},{"name":"RadioButton","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/RadioButton.luau"]},{"name":"Root","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Root.luau"]},{"name":"Tab","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tab.luau"]},{"name":"Table","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Table.luau"]},{"name":"Text","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Text.luau"]},{"name":"Tree","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Tree.luau"]},{"name":"Window","className":"ModuleScript","filePaths":["src/DebuggerUI/Shared/iris/widgets/Window.luau"]}]}]},{"name":"FastCast2","className":"ModuleScript","filePaths":["src/FastCast2/init.luau"],"children":[{"name":"ActiveCast","className":"ModuleScript","filePaths":["src/FastCast2/ActiveCast.luau"]},{"name":"BaseCast","className":"ModuleScript","filePaths":["src/FastCast2/BaseCast.luau"]},{"name":"Configs","className":"ModuleScript","filePaths":["src/FastCast2/Configs.luau"]},{"name":"DefaultConfigs","className":"ModuleScript","filePaths":["src/FastCast2/DefaultConfigs.luau"]},{"name":"FastCastEnums","className":"ModuleScript","filePaths":["src/FastCast2/FastCastEnums.luau"]},{"name":"FastCastVMs","className":"ModuleScript","filePaths":["src/FastCast2/FastCastVMs/init.luau"],"children":[{"name":"ClientVM","className":"LocalScript","filePaths":["src/FastCast2/FastCastVMs/ClientVM.client.luau","src/FastCast2/FastCastVMs/ClientVM.meta.json"]},{"name":"ServerVM","className":"Script","filePaths":["src/FastCast2/FastCastVMs/ServerVM.server.luau","src/FastCast2/FastCastVMs/ServerVM.meta.json"]}]},{"name":"ObjectCache","className":"ModuleScript","filePaths":["src/FastCast2/ObjectCache.luau"]},{"name":"Signal","className":"ModuleScript","filePaths":["src/FastCast2/Signal.luau"]},{"name":"TypeDefinitions","className":"ModuleScript","filePaths":["src/FastCast2/TypeDefinitions.luau"]}]}]},{"name":"ServerScriptService","className":"ServerScriptService","children":[{"name":"IrisServer","className":"Script","filePaths":["src/DebuggerUI/Server/IrisServer.server.luau"]}]},{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterCharacterScripts","className":"StarterCharacterScripts"}]}]} \ No newline at end of file diff --git a/src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau b/src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau deleted file mode 100644 index 7ca590c..0000000 --- a/src/DebuggerUI/Shared/RemoteTableLight/Shared/LICENSE.luau +++ /dev/null @@ -1,167 +0,0 @@ ---[[ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates - the terms and conditions of version 3 of the GNU General Public - License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser - General Public License, and the "GNU GPL" refers to version 3 of the GNU - General Public License. - - "The Library" refers to a covered work governed by this License, - other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided - by the Library, but which is not otherwise based on the Library. - Defining a subclass of a class defined by the Library is deemed a mode - of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an - Application with the Library. The particular version of the Library - with which the Combined Work was made is also called the "Linked - Version". - - The "Minimal Corresponding Source" for a Combined Work means the - Corresponding Source for the Combined Work, excluding any source code - for portions of the Combined Work that, considered in isolation, are - based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the - object code and/or source code for the Application, including any data - and utility programs needed for reproducing the Combined Work from the - Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License - without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a - facility refers to a function or data to be supplied by an Application - that uses the facility (other than as an argument passed when the - facility is invoked), then you may convey a copy of the modified - version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from - a header file that is part of the Library. You may convey such object - code under terms of your choice, provided that, if the incorporated - material is not limited to numerical parameters, data structure - layouts and accessors, or small macros, inline functions and templates - (ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, - taken together, effectively do not restrict modification of the - portions of the Library contained in the Combined Work and reverse - engineering for debugging such modifications, if you also do each of - the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the - Library side by side in a single library together with other library - facilities that are not Applications and are not covered by this - License, and convey such a combined library under terms of your - choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions - of the GNU Lesser General Public License from time to time. Such new - versions will be similar in spirit to the present version, but may - differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the - Library as you received it specifies that a certain numbered version - of the GNU Lesser General Public License "or any later version" - applies to it, you have the option of following the terms and - conditions either of that published version or of any later version - published by the Free Software Foundation. If the Library as you - received it does not specify a version number of the GNU Lesser - General Public License, you may choose any version of the GNU Lesser - General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide - whether future versions of the GNU Lesser General Public License shall - apply, that proxy's public statement of acceptance of any version is - permanent authorization for you to choose that version for the - Library. -]] \ No newline at end of file From 5dc86e895697f9a571513f929b38a6e5a444e586 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:56:12 +0700 Subject: [PATCH 20/22] Fixed man --- src/DebuggerUI/Shared/FastCastEventsModule/Default.luau | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau index 41fd537..56bcee4 100644 --- a/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau +++ b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau @@ -15,7 +15,7 @@ local module: TypeDef.FastCastEvents = {} local debounce = false local debounce_time = 0.2 -module.LengthChanged = function(cast : TypeDef.ActiveCast) +module.LengthChanged = function() if not debounce then debounce = true print("OnLengthChanged Test") @@ -33,11 +33,11 @@ module.CastTerminating = function() print("CastTerminating!") end -module.RayHit = function() +module.Hit = function() print("Hit!") end -module.CanPierce = function(cast, resultOfCast : RaycastResult, segmentVelocity, CosmeticBulletObject) +module.CanPierce = function(_, resultOfCast : RaycastResult, segmentVelocity, CosmeticBulletObject) local CanPierce = false if resultOfCast.Instance:GetAttribute("CanPierce") == true then CanPierce = true From f33fb69356305fb2d1f9288d63eb7ee04f60e4f4 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 19:59:33 +0700 Subject: [PATCH 21/22] Unused parameters --- src/DebuggerUI/Shared/FastCastEventsModule/Default.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau index 56bcee4..951a513 100644 --- a/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau +++ b/src/DebuggerUI/Shared/FastCastEventsModule/Default.luau @@ -37,7 +37,7 @@ module.Hit = function() print("Hit!") end -module.CanPierce = function(_, resultOfCast : RaycastResult, segmentVelocity, CosmeticBulletObject) +module.CanPierce = function(_, resultOfCast : RaycastResult) local CanPierce = false if resultOfCast.Instance:GetAttribute("CanPierce") == true then CanPierce = true From 0d7b56d60b2c2a61d205581093b68040d7119512 Mon Sep 17 00:00:00 2001 From: Mawin CK Date: Sat, 14 Mar 2026 20:36:59 +0700 Subject: [PATCH 22/22] Fix statements --- src/DebuggerUI/Client/IrisLocal.client.luau | 32 +++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/DebuggerUI/Client/IrisLocal.client.luau b/src/DebuggerUI/Client/IrisLocal.client.luau index 81d631a..afb5e33 100644 --- a/src/DebuggerUI/Client/IrisLocal.client.luau +++ b/src/DebuggerUI/Client/IrisLocal.client.luau @@ -150,7 +150,7 @@ local function onCastTerminating(cast: FastCastTypes.ActiveCastData) local obj: Instance? = cast.RayInfo.CosmeticBulletObject :: Instance? if obj then obj:Destroy() - end + end ClientProjectileCount -= 1 end @@ -261,10 +261,22 @@ local function buildClientBehavior() CastBehaviorClient.CosmeticBulletTemplate = UseCosmeticBulletTemplate.value and ProjectileTemplate or nil CastBehaviorClient.SimulateAfterPhysic = SimulateAfterPhysicState.value CastBehaviorClient.AutomaticPerformance = AutomaticPerformanceState.value - for key, v in FastCastEventsConfigStates do CastBehaviorClient.FastCastEventsConfig[key] = v.value end - for key, v in FastCastEventsModuleConfigStates do CastBehaviorClient.FastCastEventsModuleConfig[key] = v.value end - for key, v in AdaptivePerformanceStates do CastBehaviorClient.AdaptivePerformance[key] = v.value end - for key, v in VisualizeCastSettingSt do CastBehaviorClient.VisualizeCastSettings[key] = v.value end + + for key, v in FastCastEventsConfigStates do + CastBehaviorClient.FastCastEventsConfig[key] = v.value + end + + for key, v in FastCastEventsModuleConfigStates do + CastBehaviorClient.FastCastEventsModuleConfig[key] = v.value + end + + for key, v in AdaptivePerformanceStates do + CastBehaviorClient.AdaptivePerformance[key] = v.value + end + + for key, v in VisualizeCastSettingSt do + CastBehaviorClient.VisualizeCastSettings[key] = v.value + end end local function buildServerBehavior() @@ -430,9 +442,13 @@ iris:Connect(function() iris.Checkbox({ "BulkMoveTo" }, { isChecked = BulkMoveEnabledValue }) if BulkMoveEnabledValue.value then - if not Caster.BulkMoveEnabled then Caster:SetBulkMoveEnabled(true) end + if not Caster.BulkMoveEnabled then + Caster:SetBulkMoveEnabled(true) + end else - if Caster.BulkMoveEnabled then Caster:SetBulkMoveEnabled(false) end + if Caster.BulkMoveEnabled then + Caster:SetBulkMoveEnabled(false) + end end iris.End() @@ -536,4 +552,4 @@ UserInputService.InputEnded:Connect(function(input: InputObject, gp: boolean) if input.KeyCode == Enum.KeyCode.E then WindowVisible:set(not WindowVisible:get()) end -end) +end) \ No newline at end of file