Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
|
eb61666031 | ||
|
c4e9020157 | ||
|
0c0b71d20a | ||
|
bf2709874a | ||
|
207dcc009d | ||
|
43f697915d | ||
|
a2bc7709eb | ||
|
9e7c924c79 | ||
|
b59f501180 | ||
|
f5268009cd | ||
|
787ed32c9a | ||
|
eeb1218b58 | ||
|
477c16f4bc |
14 changed files with 823 additions and 412 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "RNG Fight",
|
||||
"name": "UIM Manager",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
local types = require(script.Parent.Types)
|
||||
|
||||
local button: types.ButtonClass = {} :: types.ButtonClass
|
||||
local button = {} :: types.ButtonClass
|
||||
button["__index"] = button
|
||||
|
||||
local module = {}
|
||||
|
||||
function module.new(name: string): types.Button
|
||||
local self = setmetatable({
|
||||
Status = "Stored",
|
||||
Name= name,
|
||||
_ = {},
|
||||
}, button)
|
||||
|
@ -34,6 +35,14 @@ function button:Build(parent: types.Page | types.Component)
|
|||
end
|
||||
end
|
||||
|
||||
function button:GetStatus(): "Built" | "Stored"
|
||||
return self._.Status
|
||||
end
|
||||
|
||||
function button:StatusIs(status: "Built" | "Stored")
|
||||
return status == self._.Status
|
||||
end
|
||||
|
||||
function button:Open()
|
||||
if self.ButtonGui then
|
||||
self.ButtonGui.Active = true
|
||||
|
@ -104,4 +113,4 @@ function button:Remove()
|
|||
end
|
||||
end
|
||||
|
||||
return module
|
||||
return module
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
local objectList = require(script.Parent.ObjectList)
|
||||
local types = require(script.Parent.Types)
|
||||
|
||||
local component: types.ComponentClass = {} :: types.ComponentClass
|
||||
local component = {} :: types.ComponentClass
|
||||
component["__index"] = component
|
||||
|
||||
local module = {}
|
||||
|
@ -8,51 +9,18 @@ local module = {}
|
|||
function module.new(name: string): types.Component
|
||||
local self = setmetatable({
|
||||
Name= name,
|
||||
Buttons = {
|
||||
Active = {},
|
||||
Stored = {},
|
||||
},
|
||||
Buttons = objectList.new(),
|
||||
|
||||
_ = {
|
||||
UpdateCallbacks = {},
|
||||
},
|
||||
}, component)
|
||||
|
||||
self.Buttons.Parent = self
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[ Buttons ]]
|
||||
|
||||
function component:BuildButtons()
|
||||
for _, button: types.Button in self.Buttons.Stored do
|
||||
button:Build(self)
|
||||
end
|
||||
end
|
||||
|
||||
function component:AddButton(button: types.Button)
|
||||
self.Buttons.Stored[button.Name] = button
|
||||
end
|
||||
|
||||
function component:CloseAllButtons()
|
||||
for _, button in self.Buttons.Active do
|
||||
button:Close()
|
||||
end
|
||||
|
||||
self.Buttons.Active = {}
|
||||
end
|
||||
|
||||
function component:RemoveButtons()
|
||||
self:CloseAllButtons()
|
||||
|
||||
for _, button in self.Buttons.Stored do
|
||||
button:Remove()
|
||||
end
|
||||
|
||||
self.Buttons.Stored = {}
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
function component:OnBuild(callback: (self: types.Component) -> ()): ()
|
||||
self._.OnBuild = callback
|
||||
end
|
||||
|
@ -80,6 +48,16 @@ function component:Build(parent: types.Page): ()
|
|||
if self._.OnBuild then
|
||||
self._.OnBuild(self)
|
||||
end
|
||||
|
||||
self._.Status = "Built"
|
||||
end
|
||||
|
||||
function component:GetStatus(): "Built" | "Stored"
|
||||
return self._.Status
|
||||
end
|
||||
|
||||
function component:StatusIs(status: "Built" | "Stored")
|
||||
return status == self._.Status
|
||||
end
|
||||
|
||||
function component:Update(command: string, parameters: {}?): ()
|
||||
|
@ -111,21 +89,23 @@ function component:Close(): ()
|
|||
end
|
||||
|
||||
function component:Clean(): ()
|
||||
self:CloseAllButtons()
|
||||
self.Buttons:CleanAll()
|
||||
|
||||
if self.Container then
|
||||
self.Container:Remove()
|
||||
end
|
||||
|
||||
self._.Status = "Stored"
|
||||
end
|
||||
|
||||
function component:Remove(): ()
|
||||
self:Clean()
|
||||
|
||||
self:RemoveButtons()
|
||||
self.Buttons:RemoveAll()
|
||||
|
||||
if self.Frame then
|
||||
self.Frame:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
return module
|
||||
return module
|
||||
|
|
126
src/Shared/Modules/Managers/UIManager/ObjectList.luau
Normal file
126
src/Shared/Modules/Managers/UIManager/ObjectList.luau
Normal file
|
@ -0,0 +1,126 @@
|
|||
local types = require(script.Parent.Types)
|
||||
|
||||
local module = {}
|
||||
|
||||
local list = {}
|
||||
list["__index"] = list
|
||||
|
||||
function module.new<T, P>(): types.ObjectList<T, P>
|
||||
local self = setmetatable({
|
||||
Parent = nil,
|
||||
|
||||
Opened = {},
|
||||
Built = {},
|
||||
Stored = {},
|
||||
}, list)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function list:Add(object: types.ListableObject)
|
||||
self.Stored[object.Name] = object
|
||||
end
|
||||
|
||||
function list:Get(name: string): types.ListableObject?
|
||||
return self.Stored[name]
|
||||
end
|
||||
|
||||
function list:Remove(name: string)
|
||||
self:Clean(name)
|
||||
self.Stored[name] = nil
|
||||
end
|
||||
|
||||
function list:RemoveAll()
|
||||
self:CleanAll()
|
||||
self.Stored = {}
|
||||
end
|
||||
|
||||
function list:Open(name: string, mode: ("Weighted" | "Forced")?)
|
||||
local objectToBuild = self.Built[name]
|
||||
if objectToBuild and objectToBuild:StatusIs("Built") then
|
||||
if mode and mode == "Weighted" and objectToBuild.Weight then
|
||||
for openName: string, status: boolean in self.Opened do
|
||||
local openObject = self.Built[openName]
|
||||
if status == true and openObject.Weight and openObject.Weight < objectToBuild.Weight then
|
||||
self:Close(openName)
|
||||
end
|
||||
end
|
||||
elseif mode == "Forced" then
|
||||
self:CloseAll()
|
||||
end
|
||||
|
||||
objectToBuild:Open()
|
||||
self.Opened[name] = true
|
||||
|
||||
self.LastOpenedName = self.RecentlyOpenedName or name
|
||||
self.RecentlyOpenedName = name
|
||||
end
|
||||
end
|
||||
|
||||
function list:OpenAll()
|
||||
for name: string, _: types.ListableObject in self.Stored do
|
||||
self:Open(name)
|
||||
end
|
||||
end
|
||||
|
||||
function list:OpenPrevious()
|
||||
self:Open(self.LastOpenedName, "Forced")
|
||||
end
|
||||
|
||||
function list:Close(name: string)
|
||||
if self.Built[name] then
|
||||
self.Built[name]:Close()
|
||||
self.Opened[name] = false
|
||||
end
|
||||
end
|
||||
|
||||
function list:CloseAll()
|
||||
for name: string, status: boolean in self.Opened do
|
||||
if status == true then
|
||||
self:Close(name)
|
||||
end
|
||||
end
|
||||
|
||||
self.Opened = {}
|
||||
end
|
||||
|
||||
function list:Build(name: string)
|
||||
local object = self.Stored[name]
|
||||
if object and not object:StatusIs("Built") then
|
||||
object:Build(self.Parent)
|
||||
self.Built[name] = object
|
||||
end
|
||||
end
|
||||
|
||||
function list:BuildAll()
|
||||
for _, object in self.Stored do
|
||||
if object:StatusIs("Built") then
|
||||
continue
|
||||
end
|
||||
task.spawn(function()
|
||||
object:Build(self.Parent)
|
||||
self.Built[object.Name] = object
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function list:Clean(name: string)
|
||||
local object = self.Built[name]
|
||||
if object then
|
||||
if object["Clean"] then
|
||||
self.Built[name]:Clean()
|
||||
end
|
||||
|
||||
self.Built[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function list:CleanAll()
|
||||
for _, object in self.Stored do
|
||||
self:Clean(object.Name)
|
||||
end
|
||||
|
||||
self.Built = {}
|
||||
end
|
||||
|
||||
return module
|
|
@ -1,6 +1,7 @@
|
|||
local objectList = require(script.Parent.ObjectList)
|
||||
local types = require(script.Parent.Types)
|
||||
|
||||
local page: types.PageClass = {} :: types.PageClass
|
||||
local page = {} :: types.PageClass
|
||||
page["__index"] = page
|
||||
|
||||
local module = {}
|
||||
|
@ -10,136 +11,18 @@ function module.new(name: string): types.Page
|
|||
Name= name,
|
||||
Weight = 0,
|
||||
|
||||
Buttons = {
|
||||
Active = {},
|
||||
Stored = {},
|
||||
},
|
||||
Components = {
|
||||
Open = {},
|
||||
Stored = {},
|
||||
},
|
||||
Buttons = objectList.new() :: types.ObjectList<types.Button, types.Page>,
|
||||
Components = objectList.new(),
|
||||
|
||||
_ = {},
|
||||
}, page)
|
||||
|
||||
self.Buttons.Parent = self
|
||||
self.Components.Parent = self
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[ Buttons ]]
|
||||
|
||||
function page:BuildButtons()
|
||||
for _, button: types.Button in self.Buttons.Stored do
|
||||
button:Build(self)
|
||||
end
|
||||
end
|
||||
|
||||
function page:AddButton(button: types.Button)
|
||||
self.Buttons.Stored[button.Name] = button
|
||||
end
|
||||
|
||||
function page:GetButton(button: string): types.Component
|
||||
return self.Buttons.Stored[button]
|
||||
end
|
||||
|
||||
function page:OpenButton(component: string)
|
||||
local buttonModule= self.Buttons.Stored[component]
|
||||
if buttonModule then
|
||||
buttonModule:Build(self)
|
||||
buttonModule:Open()
|
||||
self.Buttons.Active[component] = buttonModule
|
||||
end
|
||||
end
|
||||
|
||||
function page:CloseButton(component: string)
|
||||
local buttonModule= self.Buttons.Active[component]
|
||||
if buttonModule then
|
||||
buttonModule:Close()
|
||||
buttonModule:Clean()
|
||||
self.Buttons.Active[component] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function page:CloseAllButtons()
|
||||
for _, button: types.Button in self.Buttons.Active do
|
||||
button:Close()
|
||||
end
|
||||
|
||||
self.Buttons.Active = {}
|
||||
end
|
||||
|
||||
function page:RemoveButtons()
|
||||
self:CloseAllButtons()
|
||||
|
||||
for _, button: types.Button in self.Buttons.Stored do
|
||||
button:Remove()
|
||||
end
|
||||
|
||||
self.Buttons.Stored = {}
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
--[[ Components ]]
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
function page:GetComponent(component: string): types.Component
|
||||
return self.Components.Stored[component]
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
function page:CloseComponent(component: string)
|
||||
local componentModule= self.Components.Open[component]
|
||||
if componentModule then
|
||||
componentModule:Close()
|
||||
componentModule:Clean()
|
||||
self.Components.Open[component] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function page:CleanComponents()
|
||||
for _, component: types.Component in self.Components.Open do
|
||||
component:Close()
|
||||
end
|
||||
|
||||
for _, component: types.Component in self.Components.Stored do
|
||||
component:Clean()
|
||||
end
|
||||
|
||||
self.Components.Open = {}
|
||||
end
|
||||
|
||||
function page:RemoveComponents()
|
||||
for _, component: types.Component in self.Components.Open do
|
||||
component:Close()
|
||||
end
|
||||
|
||||
for _, component: types.Component in self.Components.Stored do
|
||||
component:Remove()
|
||||
end
|
||||
|
||||
self.Components.Open = {}
|
||||
self.Components.Stored = {}
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
--[[ Page ]]
|
||||
|
||||
function page:OnBuild(callback: (self: types.Page) -> ())
|
||||
|
@ -159,11 +42,22 @@ function page:Build(parent: types.Window)
|
|||
|
||||
self.Parent = parent
|
||||
|
||||
self.Container = parent.Manager.Packages.Trove.new()
|
||||
local manager = parent.Parent
|
||||
self.Container = manager.Packages.Trove.new()
|
||||
|
||||
if self._.OnBuild then
|
||||
self._.OnBuild(self)
|
||||
end
|
||||
|
||||
self._.Status = "Built"
|
||||
end
|
||||
|
||||
function page:GetStatus(): "Built" | "Stored"
|
||||
return self._.Status
|
||||
end
|
||||
|
||||
function page:StatusIs(status: "Built" | "Stored")
|
||||
return status == self._.Status
|
||||
end
|
||||
|
||||
function page:Open()
|
||||
|
@ -193,19 +87,24 @@ function page:Close()
|
|||
end
|
||||
|
||||
function page:Clean()
|
||||
self:CleanComponents()
|
||||
self:CloseAllButtons()
|
||||
self.Components:CleanAll()
|
||||
self.Buttons:CleanAll()
|
||||
|
||||
if self.Container then
|
||||
self.Container:Remove()
|
||||
self.Container:Destroy()
|
||||
end
|
||||
|
||||
self._.Status = "Stored"
|
||||
end
|
||||
|
||||
function page:Remove()
|
||||
self:Clean()
|
||||
|
||||
self:RemoveComponents()
|
||||
self:RemoveButtons()
|
||||
self.Components:CloseAll()
|
||||
self.Components:RemoveAll()
|
||||
|
||||
self.Buttons:CloseAll()
|
||||
self.Buttons:RemoveAll()
|
||||
|
||||
if self.Frame then
|
||||
self.Frame:Destroy()
|
||||
|
@ -215,7 +114,7 @@ end
|
|||
--[[ Helper ]]
|
||||
|
||||
function page:GetManager(): types.Manager
|
||||
return self.Parent.Manager
|
||||
return self.Parent.Parent
|
||||
end
|
||||
|
||||
return module
|
||||
return module
|
||||
|
|
24
src/Shared/Modules/Managers/UIManager/Template.luau
Normal file
24
src/Shared/Modules/Managers/UIManager/Template.luau
Normal file
|
@ -0,0 +1,24 @@
|
|||
local types = require(script.Parent.Types)
|
||||
|
||||
local module = {}
|
||||
|
||||
local template = {}
|
||||
template["__index"] = template
|
||||
|
||||
function module.new<Object, MethodOptions, Parameters>(): types.Template<Object, MethodOptions, Parameters>
|
||||
return setmetatable({
|
||||
Files = {},
|
||||
} :: types.TemplateClass<Object, MethodOptions, Parameters>, template) :: types.Template<Object, MethodOptions, Parameters>
|
||||
end
|
||||
|
||||
function template:Create<Object, MethodOptions, Parameters>(method: MethodOptions, callback: (self: Object, parameters: Parameters?) -> ()): ()
|
||||
self.Files[method] = callback
|
||||
end
|
||||
|
||||
function template:Fire<Object, MethodOptions, Parameters>(method: MethodOptions, object: Object, parameters: Parameters?): ()
|
||||
if self.Files[method] then
|
||||
self.Files[method](object, parameters)
|
||||
end
|
||||
end
|
||||
|
||||
return module
|
|
@ -1,33 +1,105 @@
|
|||
local replicated = game:GetService("ReplicatedStorage")
|
||||
local signal = require(replicated.Modules.Utility.Signal)
|
||||
local trove = require(replicated.Modules.Utility.Trove)
|
||||
|
||||
|
||||
--
|
||||
|
||||
--[[ == Template == ]]
|
||||
|
||||
type TemplateClass<Object, MethodOptions, Parameters> = {
|
||||
Files: {
|
||||
[string]: (self: Object, parameters: {}?) -> ()
|
||||
},
|
||||
Create: ((self: Template<Object, MethodOptions, Parameters>, method: MethodOptions, callback: (self: Object, parameters: Parameters?) -> ()) -> ()),
|
||||
Fire: ((self: Template<Object, MethodOptions, Parameters>, method: MethodOptions, object: Object, parameters: Parameters?) -> ()),
|
||||
}
|
||||
|
||||
export type Template<Object, MethodOptions, Parameters> = typeof(setmetatable({} :: TemplateClass<Object, MethodOptions, Parameters>, {}))
|
||||
|
||||
--[[ == ObjectList == ]]
|
||||
|
||||
export type ListableObject = (Button | Component | Page)
|
||||
|
||||
export type ObjectListClass<LO, P> = {
|
||||
Parent: P,
|
||||
|
||||
Opened: { [string]: boolean },
|
||||
Built: { [string]: LO },
|
||||
Stored: { [string]: LO },
|
||||
|
||||
RecentlyOpenedName: string,
|
||||
LastOpenedName: string,
|
||||
|
||||
Add: (self: ObjectList<LO, P>, object: LO) -> (),
|
||||
Get: (self: ObjectList<LO, P>, name: string) -> (LO?),
|
||||
Remove: (self: ObjectList<LO, P>, name: string) -> (),
|
||||
RemoveAll: (self: ObjectList<LO, P>) -> (),
|
||||
Build: (self: ObjectList<LO, P>, name: string) -> (),
|
||||
BuildAll: (self: ObjectList<LO, P>) -> (),
|
||||
Open: (self: ObjectList<LO, P>, name: string, mode: ("Weighted" | "Forced")?) -> (),
|
||||
OpenAll: (self: ObjectList<LO, P>) -> (),
|
||||
OpenPrevious: (self: ObjectList<LO, P>) -> (),
|
||||
Close: (self: ObjectList<LO, P>, name: string) -> (),
|
||||
CloseAll: (self: ObjectList<LO, P>) -> (),
|
||||
Clean: (self: ObjectList<LO, P>, name: string) -> (),
|
||||
CleanAll: (self: ObjectList<LO, P>) -> (),
|
||||
}
|
||||
|
||||
export type ObjectList<LO, P> = typeof(setmetatable({} :: ObjectListClass<LO, P>, {}))
|
||||
|
||||
|
||||
--[[ == Interfaces == ]]
|
||||
|
||||
type BuildableObject<Self, Parent> = {
|
||||
Parent: Parent,
|
||||
|
||||
_: {
|
||||
Status: "Stored" | "Built",
|
||||
|
||||
OnBuild: (self: Self) -> ()?,
|
||||
},
|
||||
|
||||
OnBuild: (self: Self, callback: (self: Self) -> ()) -> (),
|
||||
Build: (self: Self, parent: Parent) -> (),
|
||||
|
||||
GetStatus: (self: Self) -> ("Stored" | "Built"),
|
||||
StatusIs: (self: Self, status: "Stored" | "Built") -> (boolean),
|
||||
}
|
||||
|
||||
type CleanableObject<Self> = {
|
||||
Clean: (self: Self) -> (),
|
||||
Remove: (self: Self) -> (),
|
||||
}
|
||||
|
||||
type RenderableObject<Self> = {
|
||||
_: {
|
||||
OnOpen: (self: Self) -> ()?,
|
||||
OnClose: (self: Self) -> ()?,
|
||||
},
|
||||
|
||||
OnOpen: (self: Self, callback: (self: Self) -> ()) -> (),
|
||||
OnClose: (self: Self, callback: (self: Self) -> ()) -> (),
|
||||
|
||||
Open: (self: Self) -> (),
|
||||
Close: (self: Self) -> (),
|
||||
}
|
||||
|
||||
--
|
||||
|
||||
export type ButtonEvents = "Pressed" | "Released" | "Hovered" | "HoverLeft"
|
||||
|
||||
export type ButtonClass = {
|
||||
Parent: Page | Component,
|
||||
Name: string,
|
||||
ButtonGui: GuiButton,
|
||||
|
||||
_: {
|
||||
OnBuild: (self: Button) -> ()?,
|
||||
OnOpen: (self: Button) -> ()?,
|
||||
OnClose: (self: Button) -> ()?,
|
||||
|
||||
OnPressed: (self: Button) -> ()?,
|
||||
OnReleased: (self: Button) -> ()?,
|
||||
OnHovered: (self: Button) -> ()?,
|
||||
OnHoverLeft: (self: Button) -> ()?,
|
||||
},
|
||||
|
||||
OnBuild: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
OnOpen: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
OnClose: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
|
||||
Build: (self: Button, parent: Page | Component) -> (),
|
||||
Open: (self: Button) -> (),
|
||||
Close: (self: Button) -> (),
|
||||
|
||||
OnPressed: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
OnReleased: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
OnHovered: (self: Button, callback: (self: Button) -> ()) -> (),
|
||||
|
@ -39,23 +111,19 @@ export type ButtonClass = {
|
|||
HoverLeft: (self: Button) -> (),
|
||||
|
||||
Remove: (self: Button) -> (),
|
||||
}
|
||||
} & BuildableObject<Button, Page | Component> & RenderableObject<Button>
|
||||
|
||||
export type Button = typeof(setmetatable({} :: ButtonClass, {}))
|
||||
|
||||
--
|
||||
|
||||
export type ComponentClass = {
|
||||
Parent: Page,
|
||||
Name: string,
|
||||
Frame: Frame,
|
||||
|
||||
Container: trove.Trove,
|
||||
|
||||
Buttons: {
|
||||
Active: { [string]: Button },
|
||||
Stored: { [string]: Button },
|
||||
},
|
||||
Buttons: ObjectList<Button, Component>,
|
||||
|
||||
BuildButtons: (self: Component) -> (),
|
||||
AddButton: (self: Component, button: Button) -> (),
|
||||
|
@ -63,143 +131,57 @@ export type ComponentClass = {
|
|||
RemoveButtons: (self: Component) -> (),
|
||||
|
||||
_: {
|
||||
OnBuild: (self: Component) -> ()?,
|
||||
OnOpen: (self: Component) -> ()?,
|
||||
OnClose: (self: Component) -> ()?,
|
||||
|
||||
UpdateCallbacks: { [string]: (self: Page, parameters: {}) -> () },
|
||||
},
|
||||
|
||||
OnBuild: (self: Component, callback: (self: Component) -> ()) -> (),
|
||||
OnUpdate: (self: Component, command: string, callback: (self: Component) -> ()) -> (),
|
||||
OnOpen: (self: Component, callback: (self: Component) -> ()) -> (),
|
||||
OnClose: (self: Component, callback: (self: Component) -> ()) -> (),
|
||||
|
||||
Build: (self: Component, parent: Page) -> (),
|
||||
Update: (self: Component, command: string, parameters: {}?) -> (),
|
||||
Open: (self: Component) -> (),
|
||||
Close: (self: Component) -> (),
|
||||
|
||||
Clean: (self: Component) -> (),
|
||||
Remove: (self: Component) -> (),
|
||||
}
|
||||
} & BuildableObject<Component, Page> & CleanableObject<Component> & RenderableObject<Component>
|
||||
|
||||
export type Component = typeof(setmetatable({} :: ComponentClass, {}))
|
||||
|
||||
--
|
||||
|
||||
export type PageClass = {
|
||||
Parent: Window,
|
||||
Name: string,
|
||||
Weight: number,
|
||||
Frame: Frame,
|
||||
|
||||
Container: trove.Trove,
|
||||
|
||||
Buttons: {
|
||||
Active: { [string]: Button },
|
||||
Stored: { [string]: Button },
|
||||
},
|
||||
|
||||
BuildButtons: (self: Page) -> (),
|
||||
AddButton: (self: Page, button: Button) -> (),
|
||||
GetButton: (self: Page, button: string) -> (),
|
||||
OpenButton: (self: Page, button: string) -> (),
|
||||
CloseButton: (self: Page, button: string) -> (),
|
||||
CloseAllButtons: (self: Page) -> (),
|
||||
RemoveButtons: (self: Page) -> (),
|
||||
|
||||
Components: {
|
||||
Open: { [string]: Component },
|
||||
Stored: { [string]: Component },
|
||||
},
|
||||
|
||||
BuildComponents: (self: Page) -> (),
|
||||
AddComponent: (self: Page, component: Component) -> (),
|
||||
GetComponent: (self: Page, component: string) -> (),
|
||||
OpenComponent: (self: Page, component: string) -> (),
|
||||
CloseComponent: (self: Page, component: string) -> (),
|
||||
CleanComponents: (self: Page) -> (),
|
||||
RemoveComponents: (self: Page) -> (),
|
||||
|
||||
_: {
|
||||
OnBuild: (self: Page) -> ()?,
|
||||
OnOpen: (self: Page) -> ()?,
|
||||
OnClose: (self: Page) -> ()?,
|
||||
},
|
||||
|
||||
OnBuild: (self: Page, callback: (self: Page) -> ()) -> (),
|
||||
OnOpen: (self: Page, callback: (self: Page) -> ()) -> (),
|
||||
OnClose: (self: Page, callback: (self: Page) -> ()) -> (),
|
||||
|
||||
Build: (self: Page, parent: Window) -> (),
|
||||
Open: (self: Page) -> (),
|
||||
Back: (self: Page) -> (),
|
||||
Close: (self: Page) -> (),
|
||||
|
||||
Clean: (self: Page) -> (),
|
||||
Remove: (self: Page) -> (),
|
||||
Buttons: ObjectList<Button, Page>,
|
||||
Components: ObjectList<Component, Page>,
|
||||
|
||||
GetManager: (self: Window) -> (Manager),
|
||||
}
|
||||
} & BuildableObject<Page, Window> & CleanableObject<Page> & RenderableObject<Page>
|
||||
|
||||
export type Page = typeof(setmetatable({} :: PageClass, {}))
|
||||
|
||||
--
|
||||
|
||||
export type WindowClass = {
|
||||
Manager: Manager,
|
||||
Name: string,
|
||||
ScreenGui: ScreenGui,
|
||||
Pages: {
|
||||
Open: { [string]: Page },
|
||||
Stored: { [string]: Page },
|
||||
Pages: ObjectList<Page, Window>,
|
||||
|
||||
--[[ Not used, at the moment.]]
|
||||
Events: {
|
||||
PageOpened: signal.Signal<Page, boolean>,
|
||||
PageClosed: signal.Signal<Page, boolean>,
|
||||
},
|
||||
|
||||
BuildPages: (self: Window) -> (),
|
||||
AddPage: (self: Window, page: Page) -> (),
|
||||
GetPage: (self: Window, page: string) -> (),
|
||||
OpenPage: (self: Window, page: string, command: "Weighted" | "Forced"?) -> (),
|
||||
OpenLastPage: (self: Window) -> (),
|
||||
ClosePage: (self: Window, page: string) -> (),
|
||||
|
||||
--[=[
|
||||
Will close and clean all open pages. \
|
||||
If a middleware function is given, closing will be dependent on the returned boolean value of said middleware during an iterative process. \
|
||||
An example use case may include closing pages based on their weighted value.
|
||||
|
||||
@param middleware function(Page) ```returns``` (boolean)
|
||||
]=]
|
||||
CloseAllPages: (self: Window, middleware: (Page) -> (boolean)) -> (),
|
||||
RemoveAllPages: (self: Window) -> (),
|
||||
CleanPages: (self: Window) -> (),
|
||||
|
||||
_: {
|
||||
RecentlyOpenedPageName: string,
|
||||
LastOpenedPageName: string,
|
||||
|
||||
OnBuild: (self: Window) -> ()?,
|
||||
OnOpen: (self: Window) -> ()?,
|
||||
OnClose: (self: Window) -> ()?,
|
||||
},
|
||||
|
||||
OnBuild: (self: Window, callback: (self: Window) -> ()) -> (),
|
||||
OnOpen: (self: Window, callback: (self: Window) -> ()) -> (),
|
||||
OnClose: (self: Window, callback: (self: Window) -> ()) -> (),
|
||||
|
||||
Build: (self: Window, manager: Manager) -> (),
|
||||
Open: (self: Window) -> (),
|
||||
Close: (self: Window) -> (),
|
||||
|
||||
Clean: (self: Window) -> (),
|
||||
Remove: (self: Window) -> (),
|
||||
}
|
||||
} & BuildableObject<Window, Manager> & CleanableObject<Window> & RenderableObject<Window>
|
||||
|
||||
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.
|
||||
]=]
|
||||
|
@ -231,6 +213,13 @@ export type Manager = {
|
|||
]=]
|
||||
OpenWindow: (self: Manager, window: Window) -> (),
|
||||
|
||||
--[=[
|
||||
Build and open a ```Window``` object and log its opened state based on the given name.
|
||||
|
||||
@param name ```string```
|
||||
]=]
|
||||
OpenWindowByName: (self: Manager, name: string) -> (),
|
||||
|
||||
--[=[
|
||||
Close and remove a ```Window``` object and log its state change.
|
||||
|
||||
|
@ -257,6 +246,9 @@ export type Manager = {
|
|||
Clean: (self: Manager) -> (),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
--
|
||||
|
||||
return {}
|
||||
return {}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local objectList = require(script.Parent.ObjectList)
|
||||
local signal = require(ReplicatedStorage.Modules.Utility.Signal)
|
||||
local types = require(script.Parent.Types)
|
||||
|
||||
local window: types.WindowClass = {} :: types.WindowClass
|
||||
local window = {} :: types.WindowClass
|
||||
window["__index"] = window
|
||||
|
||||
local module = {}
|
||||
|
@ -8,97 +11,22 @@ local module = {}
|
|||
function module.new(name: string): types.Window
|
||||
local self = setmetatable({
|
||||
Name= name,
|
||||
Pages = {
|
||||
Open = {},
|
||||
Stored = {},
|
||||
|
||||
Pages = objectList.new(),
|
||||
|
||||
Events = {
|
||||
PageOpened = signal.new(),
|
||||
PageClosed = signal.new(),
|
||||
},
|
||||
|
||||
_ = {},
|
||||
}, window)
|
||||
|
||||
self.Pages.Parent = self
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function window:BuildPages()
|
||||
for _, page: types.Page in self.Pages.Stored do
|
||||
page:Build(self)
|
||||
end
|
||||
end
|
||||
|
||||
function window:AddPage(page: types.Page)
|
||||
self.Pages.Stored[page.Name] = page
|
||||
end
|
||||
|
||||
function window:GetPage(page: string): types.Page
|
||||
return self.Pages.Stored[page]
|
||||
end
|
||||
|
||||
function window:OpenPage(page: string, command: "Weighted" | "Forced"?)
|
||||
local pageModule: types.Page = self.Pages.Stored[page]
|
||||
if pageModule then
|
||||
if command then
|
||||
if command:upper() == "WEIGHTED" then
|
||||
self:CloseAllPages(function(openPage: types.Page)
|
||||
return openPage.Weight < pageModule.Weight
|
||||
end)
|
||||
elseif command:upper() == "FORCED" then
|
||||
self:CloseAllPages()
|
||||
end
|
||||
end
|
||||
|
||||
pageModule:Build(self)
|
||||
pageModule:Open()
|
||||
self.Pages.Open[page] = pageModule
|
||||
|
||||
self._.LastOpenedPageName = self._.RecentlyOpenedPageName or page
|
||||
self._.RecentlyOpenedPageName = page
|
||||
end
|
||||
end
|
||||
|
||||
function window:OpenLastPage()
|
||||
self:OpenPage(self._.LastOpenedPageName)
|
||||
end
|
||||
|
||||
function window:ClosePage(page: string)
|
||||
local pageModule: types.Page = self.Pages.Open[page]
|
||||
if pageModule then
|
||||
pageModule:Close()
|
||||
pageModule:Clean()
|
||||
self.Pages.Open[page] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function window:CloseAllPages(middleware: (types.Page) -> (boolean)?)
|
||||
for _, page: types.Page in self.Pages.Open do
|
||||
if not middleware or not middleware(page) then
|
||||
continue
|
||||
end
|
||||
|
||||
page:Close()
|
||||
page:Clean()
|
||||
|
||||
self.Pages.Open[page.Name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function window:CleanPages()
|
||||
for _, page: types.Page in self.Pages.Open do
|
||||
page:Close()
|
||||
page:Clean()
|
||||
end
|
||||
|
||||
self.Pages.Open = {}
|
||||
end
|
||||
|
||||
function window:RemoveAllPages()
|
||||
for _, page: types.Page in self.Pages.Open do
|
||||
page:Remove()
|
||||
end
|
||||
|
||||
self.Pages.Open = {}
|
||||
self.Pages.Stored = {}
|
||||
end
|
||||
|
||||
function window:OnBuild(callback: (self: types.Window) -> ())
|
||||
self._.OnBuild = callback
|
||||
end
|
||||
|
@ -114,11 +42,21 @@ end
|
|||
function window:Build(manager: types.Manager)
|
||||
self:Clean()
|
||||
|
||||
self.Manager = manager
|
||||
self.Parent = manager
|
||||
|
||||
if self._.OnBuild then
|
||||
self._.OnBuild(self)
|
||||
end
|
||||
|
||||
self._.Status = "Built"
|
||||
end
|
||||
|
||||
function window:GetStatus(): "Built" | "Stored"
|
||||
return self._.Status
|
||||
end
|
||||
|
||||
function window:StatusIs(status: "Built" | "Stored"): boolean
|
||||
return status == self._.Status
|
||||
end
|
||||
|
||||
function window:Open()
|
||||
|
@ -142,16 +80,17 @@ function window:Close()
|
|||
end
|
||||
|
||||
function window:Clean()
|
||||
self:CleanPages()
|
||||
self.Pages:CleanAll()
|
||||
|
||||
self._.Status = "Stored"
|
||||
end
|
||||
|
||||
function window:Remove()
|
||||
self:Clean()
|
||||
self:RemovePages()
|
||||
|
||||
if self.ScreenGui then
|
||||
self.ScreenGui:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
return module
|
||||
return module
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local replicated = game:GetService("ReplicatedStorage")
|
||||
local signal = require(ReplicatedStorage.Modules.Utility.Signal)
|
||||
local trove = require(replicated.Modules.Utility.Trove)
|
||||
local types = require(script.Types)
|
||||
|
||||
local manager: types.Manager = {
|
||||
Events = {
|
||||
WindowOpened = signal.new(),
|
||||
WindowClosed = signal.new(),
|
||||
},
|
||||
|
||||
Packages = {Trove = trove},
|
||||
|
||||
Windows = {
|
||||
|
@ -11,6 +18,8 @@ local manager: types.Manager = {
|
|||
},
|
||||
} :: types.Manager
|
||||
|
||||
--[[ Window Methods ]]
|
||||
|
||||
function manager:GetWindow(name: string)
|
||||
return self.Windows.Stored[name]
|
||||
end
|
||||
|
@ -20,6 +29,12 @@ function manager:OpenWindow(window: types.Window)
|
|||
window:Open()
|
||||
|
||||
self.Windows.Open[window.Name] = window
|
||||
|
||||
self.Events.WindowOpened:Fire(window, true)
|
||||
end
|
||||
|
||||
function manager:OpenWindowByName(name: string)
|
||||
self:OpenWindow(self:GetWindow(name))
|
||||
end
|
||||
|
||||
function manager:CloseWindow(window: types.Window)
|
||||
|
@ -27,12 +42,13 @@ function manager:CloseWindow(window: types.Window)
|
|||
window:Remove()
|
||||
|
||||
self.Windows.Open[window.Name] = nil
|
||||
|
||||
self.Events.WindowClosed:Fire(window, true)
|
||||
end
|
||||
|
||||
function manager:CloseAllWindows()
|
||||
for _, window in self.Windows.Open do
|
||||
window:Close()
|
||||
window:Remove()
|
||||
self:CloseWindow(window)
|
||||
end
|
||||
|
||||
self.Windows.Open = {}
|
||||
|
@ -59,4 +75,4 @@ function manager:Clean()
|
|||
}
|
||||
end
|
||||
|
||||
return manager
|
||||
return manager
|
||||
|
|
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