Signals Class
This commit is contained in:
		
							parent
							
								
									787ed32c9a
								
							
						
					
					
						commit
						f5268009cd
					
				
					 8 changed files with 618 additions and 32 deletions
				
			
		| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
local RunService = game:GetService("RunService")
 | 
			
		||||
local UserInputService = game:GetService("UserInputService")
 | 
			
		||||
local replicated = game:GetService("ReplicatedStorage")
 | 
			
		||||
 | 
			
		||||
local Draggable = require(replicated.Modules.Managers.UIManager.UIObjects.Draggable)
 | 
			
		||||
local types = require(replicated.Modules.Managers.UIManager.Types)
 | 
			
		||||
local Component = require(replicated.Modules.Managers.UIManager.Component)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,11 +18,33 @@ component:OnBuild(function(self: types.Component): ()
 | 
			
		|||
    
 | 
			
		||||
    self.Frame.Parent = self.Parent.Frame
 | 
			
		||||
 | 
			
		||||
    task.delay(2, self.Update, self, "Size")
 | 
			
		||||
    -- local draggablePage = Draggable.new("Test")
 | 
			
		||||
    -- draggablePage:OnBuild(function(draggable: types.Draggable)
 | 
			
		||||
    --     local button = Instance.new("TextButton")
 | 
			
		||||
    --     button.Size = UDim2.fromScale(1, 1)
 | 
			
		||||
    --     button.Position = UDim2.fromScale(0, 0)
 | 
			
		||||
    --     button.BackgroundColor3 = Color3.fromHSV(0.805556, 0.611765, 1.000000)
 | 
			
		||||
    --     button.Parent = self.Frame
 | 
			
		||||
    --     draggable.DragButton = button
 | 
			
		||||
    -- end)
 | 
			
		||||
    -- draggablePage:OnDragStart(function(draggable: types.Draggable)
 | 
			
		||||
    --     self:Update("DragLoop", { DragState = true })
 | 
			
		||||
    -- end)
 | 
			
		||||
    -- draggablePage:OnDragStop(function(draggable: types.Draggable)
 | 
			
		||||
    --     print("stopped!!!")
 | 
			
		||||
    -- end)
 | 
			
		||||
 | 
			
		||||
    -- draggablePage:Build(self)
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
component:OnUpdate("Size", function(self: types.Component, _: {}?)
 | 
			
		||||
    self.Frame.Size = UDim2.fromScale(0.1, 0.1)
 | 
			
		||||
component:OnUpdate("DragLoop", function(self: types.Component, parameters: { DragState: boolean }?)
 | 
			
		||||
    if parameters and parameters.DragState then
 | 
			
		||||
        self.Container:Connect(RunService.Heartbeat, function()
 | 
			
		||||
            print(UserInputService:GetMouseLocation().X)
 | 
			
		||||
        end)
 | 
			
		||||
    else
 | 
			
		||||
        self.Container:Clean()
 | 
			
		||||
    end
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
return component
 | 
			
		||||
| 
						 | 
				
			
			@ -6,19 +6,21 @@ local Page = require(replicated.Modules.Managers.UIManager.Page)
 | 
			
		|||
local page = Page.new(script.Name)
 | 
			
		||||
 | 
			
		||||
page:OnBuild(function(self: types.Page): ()
 | 
			
		||||
    self.Frame = Instance.new("Frame")
 | 
			
		||||
    local frame = Instance.new("Frame")
 | 
			
		||||
    frame.Visible = false
 | 
			
		||||
    frame.Size = UDim2.fromScale(0.5, 0.5)
 | 
			
		||||
    frame.Position = UDim2.fromScale(0.5, 0.5)
 | 
			
		||||
    frame.Parent = self.Parent.ScreenGui
 | 
			
		||||
 | 
			
		||||
    self.Frame.Visible = false
 | 
			
		||||
    self.Frame.Size = UDim2.fromScale(0.2, 0.2)
 | 
			
		||||
    self.Frame.Position = UDim2.fromScale(0.5, 0.5)
 | 
			
		||||
    
 | 
			
		||||
    self.Frame.Parent = self.Parent.ScreenGui
 | 
			
		||||
    self.Frame = frame
 | 
			
		||||
    self.Container:Add(frame) --Because we creating it each time, and not just locating it from the cloned ScreenGui, we need to make sure it gets cleaned on page close.
 | 
			
		||||
 | 
			
		||||
    for _, component: Instance in script.Components:GetChildren() do
 | 
			
		||||
        if not component:IsA("ModuleScript") then continue end
 | 
			
		||||
        if not component:IsA("ModuleScript") then 
 | 
			
		||||
            continue 
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        self:AddComponent(require(component))
 | 
			
		||||
        self:OpenComponent(component.Name)
 | 
			
		||||
        self:AddComponent(require(component)).AsOpened()
 | 
			
		||||
    end
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,10 +15,13 @@ window:OnBuild(function(self: types.Window): ()
 | 
			
		|||
 | 
			
		||||
    for _, page: Instance in script.Pages:GetChildren() do
 | 
			
		||||
        if not page:IsA("ModuleScript") then continue end
 | 
			
		||||
        self:AddPage(require(page))
 | 
			
		||||
        
 | 
			
		||||
        if page.Name == DEFAULT_PAGE then 
 | 
			
		||||
            self:AddPage(require(page)).AsOpened() 
 | 
			
		||||
        else 
 | 
			
		||||
            self:AddPage(require(page)) 
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    self:OpenPage(DEFAULT_PAGE)
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
return window
 | 
			
		||||
| 
						 | 
				
			
			@ -5,12 +5,3 @@ uiManager:Build(script.Parent.Test:GetChildren() :: {})
 | 
			
		|||
local default = uiManager:GetWindow("window")
 | 
			
		||||
 | 
			
		||||
uiManager:OpenWindow(default)
 | 
			
		||||
 | 
			
		||||
task.wait(10)
 | 
			
		||||
 | 
			
		||||
uiManager:CloseWindow(default)
 | 
			
		||||
 | 
			
		||||
task.wait(10)
 | 
			
		||||
 | 
			
		||||
uiManager:OpenWindow(default)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,8 @@ function page:BuildButtons()
 | 
			
		|||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function page:AddButton(button: types.Button)
 | 
			
		||||
    self.Buttons.Stored[button.Name] = button
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -81,14 +83,29 @@ end
 | 
			
		|||
 | 
			
		||||
--[[ Components ]]
 | 
			
		||||
 | 
			
		||||
function page:BuildComponents()
 | 
			
		||||
local function addComponentTo(self: types.Page, component: types.Component)
 | 
			
		||||
    component:Build(self)
 | 
			
		||||
    component:Open()
 | 
			
		||||
    self.Components.Open[component.Name] = component
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[ Component Methods ]]
 | 
			
		||||
 | 
			
		||||
function page:BuildComponents(): ()
 | 
			
		||||
    for _, component: types.Button in self.Components.Stored do
 | 
			
		||||
        component:Build(self)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function page:AddComponent(component: types.Component)
 | 
			
		||||
    self.Components.Stored[component.Name] = component
 | 
			
		||||
function page:AddComponent(component: types.Component): {AsOpened: () -> ()}
 | 
			
		||||
    local name: string = component.Name
 | 
			
		||||
    self.Components.Stored[name] = component
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        AsOpened = function(): ()
 | 
			
		||||
            addComponentTo(self, component)
 | 
			
		||||
        end
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function page:GetComponent(component: string): types.Component
 | 
			
		||||
| 
						 | 
				
			
			@ -98,9 +115,7 @@ end
 | 
			
		|||
function page:OpenComponent(component: string)
 | 
			
		||||
    local componentModule= self.Components.Stored[component]
 | 
			
		||||
    if componentModule then
 | 
			
		||||
        componentModule:Build(self)
 | 
			
		||||
        componentModule:Open()
 | 
			
		||||
        self.Components.Open[component] = componentModule
 | 
			
		||||
        addComponentTo(self, componentModule)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,41 @@
 | 
			
		|||
local replicated = game:GetService("ReplicatedStorage")
 | 
			
		||||
local signal = require(replicated.Modules.Utility.Signal)
 | 
			
		||||
local trove = require(replicated.Modules.Utility.Trove)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--[[ UIObjects testing]]
 | 
			
		||||
 | 
			
		||||
export type DraggableClass = {
 | 
			
		||||
    Parent: Page | Component,
 | 
			
		||||
    Name: string,
 | 
			
		||||
    DragButton: GuiButton,
 | 
			
		||||
    DragEvent: signal.Signal<string, Enum.UserInputType, Enum.UserInputState, {Vector2}?, number?, number?>,
 | 
			
		||||
 | 
			
		||||
    _: {
 | 
			
		||||
        IsDragging: boolean,
 | 
			
		||||
        DragStartTime: number,
 | 
			
		||||
 | 
			
		||||
        OnBuild: (self: Draggable) -> ()?,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    OnBuild: (self: Draggable, callback: (self: Draggable) -> ()) -> (),
 | 
			
		||||
 | 
			
		||||
    Build: (self: Draggable, parent: Page | Component) -> (),
 | 
			
		||||
    IsDragging: (self: Draggable) -> (boolean),
 | 
			
		||||
 | 
			
		||||
    _DragStart: (self: Draggable) -> (),
 | 
			
		||||
    _DragStop: (self: Draggable) -> (),
 | 
			
		||||
 | 
			
		||||
    _MobileDrag: (self: Draggable, touchPositions: {Vector2}, scale: number, velocity: number, state: Enum.UserInputState) -> (),
 | 
			
		||||
 | 
			
		||||
    Clean: (self: Draggable) -> (),
 | 
			
		||||
    Destroy: (self: Draggable) -> (),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Draggable = typeof(setmetatable({} :: DraggableClass, {}))
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
export type ButtonClass = {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +149,9 @@ export type PageClass = {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    BuildComponents: (self: Page) -> (),
 | 
			
		||||
    AddComponent: (self: Page, component: Component) -> (),
 | 
			
		||||
    AddComponent: (self: Page, component: Component) -> (
 | 
			
		||||
        {AsOpened: () -> ()}
 | 
			
		||||
    ),
 | 
			
		||||
    GetComponent: (self: Page, component: string) -> (),
 | 
			
		||||
    OpenComponent: (self: Page, component: string) -> (),
 | 
			
		||||
    CloseComponent: (self: Page, component: string) -> (),
 | 
			
		||||
| 
						 | 
				
			
			@ -156,8 +192,15 @@ export type WindowClass = {
 | 
			
		|||
        Stored: { [string]: Page },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    Events: {
 | 
			
		||||
        PageOpened: signal.Signal<Page, boolean>,
 | 
			
		||||
        PageClosed: signal.Signal<Page, boolean>,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    BuildPages: (self: Window) -> (),
 | 
			
		||||
    AddPage: (self: Window, page: Page) -> (),
 | 
			
		||||
    AddPage: (self: Window, page: Page) -> (
 | 
			
		||||
        {AsOpened: () -> ()}
 | 
			
		||||
    ),
 | 
			
		||||
    GetPage: (self: Window, page: string) -> (),
 | 
			
		||||
    OpenPage: (self: Window, page: string, command: "Weighted" | "Forced"?) -> (),
 | 
			
		||||
    OpenLastPage: (self: Window) -> (),
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +243,11 @@ export type Window = typeof(setmetatable({} :: WindowClass, {}))
 | 
			
		|||
--
 | 
			
		||||
 | 
			
		||||
export type Manager = {
 | 
			
		||||
    Events: {
 | 
			
		||||
        WindowOpened: signal.Signal<Window, boolean>,
 | 
			
		||||
        WindowClosed: signal.Signal<Window, boolean>,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    --[=[
 | 
			
		||||
        Useful packages that may be used globally by all windows, pages, and so forth.
 | 
			
		||||
    ]=]
 | 
			
		||||
| 
						 | 
				
			
			@ -257,6 +305,9 @@ export type Manager = {
 | 
			
		|||
    Clean: (self: Manager) -> (),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
return {}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
 | 
			
		||||
local uis = game:GetService("UserInputService")
 | 
			
		||||
local types = require(ReplicatedStorage.Modules.Managers.UIManager.Types)
 | 
			
		||||
local Signal = require(ReplicatedStorage.Modules.Utility.Signal)
 | 
			
		||||
local module = {}
 | 
			
		||||
 | 
			
		||||
local draggable: types.DraggableClass = {} :: types.DraggableClass
 | 
			
		||||
draggable["__index"] = draggable
 | 
			
		||||
 | 
			
		||||
function module.new(name: string): types.Draggable
 | 
			
		||||
    local self = setmetatable({
 | 
			
		||||
        Name = name,
 | 
			
		||||
        DragEvent = Signal.new(),
 | 
			
		||||
 | 
			
		||||
        _ = {
 | 
			
		||||
            IsDragging = false,
 | 
			
		||||
            DragStartTime = 0,
 | 
			
		||||
        },
 | 
			
		||||
    }, draggable)
 | 
			
		||||
 | 
			
		||||
    return self
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[ Draggable ]]
 | 
			
		||||
 | 
			
		||||
--[[
 | 
			
		||||
    Use TouchSwipe for mobile, and MouseButton1Down with MouseButton1Up for PC
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
function draggable:OnBuild(callback: (self: types.Draggable) -> ())
 | 
			
		||||
    self._.OnBuild = callback
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:Build(parent: types.Page | types.Component)
 | 
			
		||||
    self.Parent = parent
 | 
			
		||||
 | 
			
		||||
    if self._.OnBuild then
 | 
			
		||||
        self._.OnBuild(self)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if self.DragButton then
 | 
			
		||||
        if uis.MouseEnabled then
 | 
			
		||||
            self.Parent.Container:ConnectMethod(self.DragButton.MouseButton1Down, "_DragStart", self, {})
 | 
			
		||||
            self.Parent.Container:ConnectMethod(self.DragButton.MouseButton1Up, "_DragStop", self, {})
 | 
			
		||||
        else
 | 
			
		||||
            self.Parent.Container:Connect(self.DragButton.TouchPan, function(touchPositions: {Vector2}, scale: number, velocity: number, state: Enum.UserInputState)
 | 
			
		||||
                self:_MobileDrag(touchPositions, scale, velocity, state)
 | 
			
		||||
            end)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:IsDragging(): boolean
 | 
			
		||||
    return self._.IsDragging
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:_DragStart()
 | 
			
		||||
    if self._.IsDragging then
 | 
			
		||||
        return
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    self._.IsDragging = true
 | 
			
		||||
    self._.DragStartTime = os.clock()
 | 
			
		||||
 | 
			
		||||
    self.DragEvent:Fire(Enum.UserInputType.Touch, true)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:_DragStop()
 | 
			
		||||
    self._.IsDragging = false
 | 
			
		||||
 | 
			
		||||
    self.DragEvent:Fire(Enum.UserInputType.Touch, false)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:_MobileDrag(touchPositions: {Vector2}, scale: number, velocity: number, state: Enum.UserInputState)
 | 
			
		||||
    if self._.IsDragging and state == Enum.UserInputState.Begin then
 | 
			
		||||
        return
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    self._.IsDragging = state == Enum.UserInputState.Begin
 | 
			
		||||
    self._.DragStartTime = self._.IsDragging and os.clock() or 0
 | 
			
		||||
 | 
			
		||||
    self.DragEvent:Fire(Enum.UserInputType.Touch, state, touchPositions, scale, velocity)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:Clean()
 | 
			
		||||
    self.DragEvent:DisconnectAll()
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function draggable:Destroy()
 | 
			
		||||
    self:Clean()
 | 
			
		||||
    setmetatable(getmetatable(self), nil)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
return module
 | 
			
		||||
							
								
								
									
										405
									
								
								src/Shared/Modules/Utility/Signal.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								src/Shared/Modules/Utility/Signal.lua
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,405 @@
 | 
			
		|||
-- -----------------------------------------------------------------------------
 | 
			
		||||
--               Batched Yield-Safe Signal Implementation                     --
 | 
			
		||||
-- This is a Signal class which has effectively identical behavior to a       --
 | 
			
		||||
-- normal RBXScriptSignal, with the only difference being a couple extra      --
 | 
			
		||||
-- stack frames at the bottom of the stack trace when an error is thrown.     --
 | 
			
		||||
-- This implementation caches runner coroutines, so the ability to yield in   --
 | 
			
		||||
-- the signal handlers comes at minimal extra cost over a naive signal        --
 | 
			
		||||
-- implementation that either always or never spawns a thread.                --
 | 
			
		||||
--                                                                            --
 | 
			
		||||
-- API:                                                                       --
 | 
			
		||||
--   local Signal = require(THIS MODULE)                                      --
 | 
			
		||||
--   local sig = Signal.new()                                                 --
 | 
			
		||||
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
 | 
			
		||||
--   sig:Fire(arg1, arg2, ...)                                                --
 | 
			
		||||
--   connection:Disconnect()                                                  --
 | 
			
		||||
--   sig:DisconnectAll()                                                      --
 | 
			
		||||
--   local arg1, arg2, ... = sig:Wait()                                       --
 | 
			
		||||
--                                                                            --
 | 
			
		||||
-- License:                                                                   --
 | 
			
		||||
--   Licensed under the MIT license.                                          --
 | 
			
		||||
--                                                                            --
 | 
			
		||||
-- Authors:                                                                   --
 | 
			
		||||
--   stravant - July 31st, 2021 - Created the file.                           --
 | 
			
		||||
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
 | 
			
		||||
-- -----------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
-- Signal types
 | 
			
		||||
export type Connection = {
 | 
			
		||||
	Disconnect: (self: Connection) -> (),
 | 
			
		||||
	Destroy: (self: Connection) -> (),
 | 
			
		||||
	Connected: boolean,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Signal<T...> = {
 | 
			
		||||
	Fire: (self: Signal<T...>, T...) -> (),
 | 
			
		||||
	FireDeferred: (self: Signal<T...>, T...) -> (),
 | 
			
		||||
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
 | 
			
		||||
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
 | 
			
		||||
	DisconnectAll: (self: Signal<T...>) -> (),
 | 
			
		||||
	GetConnections: (self: Signal<T...>) -> { Connection },
 | 
			
		||||
	Destroy: (self: Signal<T...>) -> (),
 | 
			
		||||
	Wait: (self: Signal<T...>) -> T...,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
-- The currently idle thread to run the next handler on
 | 
			
		||||
local freeRunnerThread = nil
 | 
			
		||||
 | 
			
		||||
-- Function which acquires the currently idle handler runner thread, runs the
 | 
			
		||||
-- function fn on it, and then releases the thread, returning it to being the
 | 
			
		||||
-- currently idle one.
 | 
			
		||||
-- If there was a currently idle runner thread already, that's okay, that old
 | 
			
		||||
-- one will just get thrown and eventually GCed.
 | 
			
		||||
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
 | 
			
		||||
	local acquiredRunnerThread = freeRunnerThread
 | 
			
		||||
	freeRunnerThread = nil
 | 
			
		||||
	fn(...)
 | 
			
		||||
	-- The handler finished running, this runner thread is free again.
 | 
			
		||||
	freeRunnerThread = acquiredRunnerThread
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Coroutine runner that we create coroutines of. The coroutine can be
 | 
			
		||||
-- repeatedly resumed with functions to run followed by the argument to run
 | 
			
		||||
-- them with.
 | 
			
		||||
local function runEventHandlerInFreeThread(...)
 | 
			
		||||
	acquireRunnerThreadAndCallEventHandler(...)
 | 
			
		||||
	while true do
 | 
			
		||||
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@within Signal
 | 
			
		||||
	@interface SignalConnection
 | 
			
		||||
	.Connected boolean
 | 
			
		||||
	.Disconnect (SignalConnection) -> ()
 | 
			
		||||
 | 
			
		||||
	Represents a connection to a signal.
 | 
			
		||||
	```lua
 | 
			
		||||
	local connection = signal:Connect(function() end)
 | 
			
		||||
	print(connection.Connected) --> true
 | 
			
		||||
	connection:Disconnect()
 | 
			
		||||
	print(connection.Connected) --> false
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
 | 
			
		||||
-- Connection class
 | 
			
		||||
local Connection = {}
 | 
			
		||||
Connection.__index = Connection
 | 
			
		||||
 | 
			
		||||
function Connection.new(signal, fn)
 | 
			
		||||
	return setmetatable({
 | 
			
		||||
		Connected = true,
 | 
			
		||||
		_signal = signal,
 | 
			
		||||
		_fn = fn,
 | 
			
		||||
		_next = false,
 | 
			
		||||
	}, Connection)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Connection:Disconnect()
 | 
			
		||||
	if not self.Connected then
 | 
			
		||||
		return
 | 
			
		||||
	end
 | 
			
		||||
	self.Connected = false
 | 
			
		||||
 | 
			
		||||
	-- Unhook the node, but DON'T clear it. That way any fire calls that are
 | 
			
		||||
	-- currently sitting on this node will be able to iterate forwards off of
 | 
			
		||||
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
 | 
			
		||||
	-- when no more fire calls are sitting on it.
 | 
			
		||||
	if self._signal._handlerListHead == self then
 | 
			
		||||
		self._signal._handlerListHead = self._next
 | 
			
		||||
	else
 | 
			
		||||
		local prev = self._signal._handlerListHead
 | 
			
		||||
		while prev and prev._next ~= self do
 | 
			
		||||
			prev = prev._next
 | 
			
		||||
		end
 | 
			
		||||
		if prev then
 | 
			
		||||
			prev._next = self._next
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
Connection.Destroy = Connection.Disconnect
 | 
			
		||||
 | 
			
		||||
-- Make Connection strict
 | 
			
		||||
setmetatable(Connection, {
 | 
			
		||||
	__index = function(_tb, key)
 | 
			
		||||
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
 | 
			
		||||
	end,
 | 
			
		||||
	__newindex = function(_tb, key, _value)
 | 
			
		||||
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
 | 
			
		||||
	end,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@within Signal
 | 
			
		||||
	@type ConnectionFn (...any) -> ()
 | 
			
		||||
 | 
			
		||||
	A function connected to a signal.
 | 
			
		||||
]=]
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@class Signal
 | 
			
		||||
 | 
			
		||||
	Signals allow events to be dispatched and handled.
 | 
			
		||||
 | 
			
		||||
	For example:
 | 
			
		||||
	```lua
 | 
			
		||||
	local signal = Signal.new()
 | 
			
		||||
 | 
			
		||||
	signal:Connect(function(msg)
 | 
			
		||||
		print("Got message:", msg)
 | 
			
		||||
	end)
 | 
			
		||||
 | 
			
		||||
	signal:Fire("Hello world!")
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
local Signal = {}
 | 
			
		||||
Signal.__index = Signal
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	Constructs a new Signal
 | 
			
		||||
 | 
			
		||||
	@return Signal
 | 
			
		||||
]=]
 | 
			
		||||
function Signal.new<T...>(): Signal<T...>
 | 
			
		||||
	local self = setmetatable({
 | 
			
		||||
		_handlerListHead = false,
 | 
			
		||||
		_proxyHandler = nil,
 | 
			
		||||
	}, Signal)
 | 
			
		||||
	return self
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	Constructs a new Signal that wraps around an RBXScriptSignal.
 | 
			
		||||
 | 
			
		||||
	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
 | 
			
		||||
	@return Signal
 | 
			
		||||
 | 
			
		||||
	For example:
 | 
			
		||||
	```lua
 | 
			
		||||
	local signal = Signal.Wrap(workspace.ChildAdded)
 | 
			
		||||
	signal:Connect(function(part) print(part.Name .. " added") end)
 | 
			
		||||
	Instance.new("Part").Parent = workspace
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
 | 
			
		||||
	assert(
 | 
			
		||||
		typeof(rbxScriptSignal) == "RBXScriptSignal",
 | 
			
		||||
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
 | 
			
		||||
	)
 | 
			
		||||
	local signal = Signal.new()
 | 
			
		||||
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
 | 
			
		||||
		signal:Fire(...)
 | 
			
		||||
	end)
 | 
			
		||||
	return signal
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	Checks if the given object is a Signal.
 | 
			
		||||
 | 
			
		||||
	@param obj any -- Object to check
 | 
			
		||||
	@return boolean -- `true` if the object is a Signal.
 | 
			
		||||
]=]
 | 
			
		||||
function Signal.Is(obj: any): boolean
 | 
			
		||||
	return type(obj) == "table" and getmetatable(obj) == Signal
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@param fn ConnectionFn
 | 
			
		||||
	@return SignalConnection
 | 
			
		||||
 | 
			
		||||
	Connects a function to the signal, which will be called anytime the signal is fired.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:Connect(function(msg, num)
 | 
			
		||||
		print(msg, num)
 | 
			
		||||
	end)
 | 
			
		||||
 | 
			
		||||
	signal:Fire("Hello", 25)
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:Connect(fn)
 | 
			
		||||
	local connection = Connection.new(self, fn)
 | 
			
		||||
	if self._handlerListHead then
 | 
			
		||||
		connection._next = self._handlerListHead
 | 
			
		||||
		self._handlerListHead = connection
 | 
			
		||||
	else
 | 
			
		||||
		self._handlerListHead = connection
 | 
			
		||||
	end
 | 
			
		||||
	return connection
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
 | 
			
		||||
	@param fn ConnectionFn
 | 
			
		||||
	@return SignalConnection
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:ConnectOnce(fn)
 | 
			
		||||
	return self:Once(fn)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@param fn ConnectionFn
 | 
			
		||||
	@return SignalConnection
 | 
			
		||||
 | 
			
		||||
	Connects a function to the signal, which will be called the next time the signal fires. Once
 | 
			
		||||
	the connection is triggered, it will disconnect itself.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:Once(function(msg, num)
 | 
			
		||||
		print(msg, num)
 | 
			
		||||
	end)
 | 
			
		||||
 | 
			
		||||
	signal:Fire("Hello", 25)
 | 
			
		||||
	signal:Fire("This message will not go through", 10)
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:Once(fn)
 | 
			
		||||
	local connection
 | 
			
		||||
	local done = false
 | 
			
		||||
	connection = self:Connect(function(...)
 | 
			
		||||
		if done then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
		done = true
 | 
			
		||||
		connection:Disconnect()
 | 
			
		||||
		fn(...)
 | 
			
		||||
	end)
 | 
			
		||||
	return connection
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function Signal:GetConnections()
 | 
			
		||||
	local items = {}
 | 
			
		||||
	local item = self._handlerListHead
 | 
			
		||||
	while item do
 | 
			
		||||
		table.insert(items, item)
 | 
			
		||||
		item = item._next
 | 
			
		||||
	end
 | 
			
		||||
	return items
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Disconnect all handlers. Since we use a linked list it suffices to clear the
 | 
			
		||||
-- reference to the head handler.
 | 
			
		||||
--[=[
 | 
			
		||||
	Disconnects all connections from the signal.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:DisconnectAll()
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:DisconnectAll()
 | 
			
		||||
	local item = self._handlerListHead
 | 
			
		||||
	while item do
 | 
			
		||||
		item.Connected = false
 | 
			
		||||
		item = item._next
 | 
			
		||||
	end
 | 
			
		||||
	self._handlerListHead = false
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Signal:Fire(...) implemented by running the handler functions on the
 | 
			
		||||
-- coRunnerThread, and any time the resulting thread yielded without returning
 | 
			
		||||
-- to us, that means that it yielded to the Roblox scheduler and has been taken
 | 
			
		||||
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
 | 
			
		||||
--[=[
 | 
			
		||||
	@param ... any
 | 
			
		||||
 | 
			
		||||
	Fire the signal, which will call all of the connected functions with the given arguments.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:Fire("Hello")
 | 
			
		||||
 | 
			
		||||
	-- Any number of arguments can be fired:
 | 
			
		||||
	signal:Fire("Hello", 32, {Test = "Test"}, true)
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:Fire(...)
 | 
			
		||||
	local item = self._handlerListHead
 | 
			
		||||
	while item do
 | 
			
		||||
		if item.Connected then
 | 
			
		||||
			if not freeRunnerThread then
 | 
			
		||||
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
 | 
			
		||||
			end
 | 
			
		||||
			task.spawn(freeRunnerThread, item._fn, ...)
 | 
			
		||||
		end
 | 
			
		||||
		item = item._next
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@param ... any
 | 
			
		||||
 | 
			
		||||
	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:FireDeferred("Hello")
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:FireDeferred(...)
 | 
			
		||||
	local item = self._handlerListHead
 | 
			
		||||
	while item do
 | 
			
		||||
		task.defer(item._fn, ...)
 | 
			
		||||
		item = item._next
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	@return ... any
 | 
			
		||||
	@yields
 | 
			
		||||
 | 
			
		||||
	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
 | 
			
		||||
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
 | 
			
		||||
	fired, using `Once` might be a better solution.
 | 
			
		||||
	```lua
 | 
			
		||||
	task.spawn(function()
 | 
			
		||||
		local msg, num = signal:Wait()
 | 
			
		||||
		print(msg, num) --> "Hello", 32
 | 
			
		||||
	end)
 | 
			
		||||
	signal:Fire("Hello", 32)
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:Wait()
 | 
			
		||||
	local waitingCoroutine = coroutine.running()
 | 
			
		||||
	local connection
 | 
			
		||||
	local done = false
 | 
			
		||||
	connection = self:Connect(function(...)
 | 
			
		||||
		if done then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
		done = true
 | 
			
		||||
		connection:Disconnect()
 | 
			
		||||
		task.spawn(waitingCoroutine, ...)
 | 
			
		||||
	end)
 | 
			
		||||
	return coroutine.yield()
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[=[
 | 
			
		||||
	Cleans up the signal.
 | 
			
		||||
 | 
			
		||||
	Technically, this is only necessary if the signal is created using
 | 
			
		||||
	`Signal.Wrap`. Connections should be properly GC'd once the signal
 | 
			
		||||
	is no longer referenced anywhere. However, it is still good practice
 | 
			
		||||
	to include ways to strictly clean up resources. Calling `Destroy`
 | 
			
		||||
	on a signal will also disconnect all connections immediately.
 | 
			
		||||
	```lua
 | 
			
		||||
	signal:Destroy()
 | 
			
		||||
	```
 | 
			
		||||
]=]
 | 
			
		||||
function Signal:Destroy()
 | 
			
		||||
	self:DisconnectAll()
 | 
			
		||||
	local proxyHandler = rawget(self, "_proxyHandler")
 | 
			
		||||
	if proxyHandler then
 | 
			
		||||
		proxyHandler:Disconnect()
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Make signal strict
 | 
			
		||||
setmetatable(Signal, {
 | 
			
		||||
	__index = function(_tb, key)
 | 
			
		||||
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
 | 
			
		||||
	end,
 | 
			
		||||
	__newindex = function(_tb, key, _value)
 | 
			
		||||
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
 | 
			
		||||
	end,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
return {
 | 
			
		||||
	new = Signal.new,
 | 
			
		||||
	Wrap = Signal.Wrap,
 | 
			
		||||
	Is = Signal.Is,
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in a new issue