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))
|
||||
end
|
||||
|
||||
self:OpenPage(DEFAULT_PAGE)
|
||||
if page.Name == DEFAULT_PAGE then
|
||||
self:AddPage(require(page)).AsOpened()
|
||||
else
|
||||
self:AddPage(require(page))
|
||||
end
|
||||
end
|
||||
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