Compare commits

...

13 commits
Alpha ... main

Author SHA1 Message Date
ClintUNI
eb61666031 CleanableObject Interface Type 2024-07-09 02:48:37 +02:00
Clint
c4e9020157
Update Manager to Modern V2 2024-06-28 15:51:24 +02:00
Clint
0c0b71d20a
Update Button to Modern V2 2024-06-28 15:50:28 +02:00
Clint
bf2709874a
Update Component to Modern V2 2024-06-28 15:50:10 +02:00
Clint
207dcc009d
Create ObjectList.luau 2024-06-28 15:49:50 +02:00
Clint
43f697915d
Update Page to Modern V2 2024-06-28 15:49:24 +02:00
Clint
a2bc7709eb
Create Template.luau 2024-06-28 15:49:07 +02:00
Clint
9e7c924c79
Update Window to Modern V2 2024-06-28 15:48:42 +02:00
Clint
b59f501180
Improve Typings to Modern V2 2024-06-28 15:48:26 +02:00
ClintUNI
f5268009cd Signals Class 2024-05-05 00:07:37 +02:00
ClintUNI
787ed32c9a Source Name 2024-05-05 00:07:24 +02:00
ClintUNI
eeb1218b58 Window Event signals 2024-05-05 00:07:14 +02:00
ClintUNI
477c16f4bc Page Event signals 2024-05-05 00:07:06 +02:00
14 changed files with 823 additions and 412 deletions

View file

@ -1,5 +1,5 @@
{
"name": "RNG Fight",
"name": "UIM Manager",
"tree": {
"$className": "DataModel",

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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 {}

View file

@ -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

View file

@ -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

View 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,
}