quad/src/class.lua

553 lines
16 KiB
Lua

local module = {}
local pack = table.pack
local match = string.match
local insert = table.insert
local gsub = string.gsub
---@param shared quad_export
---@return quad_module_class
function module.init(shared)
local warn = shared.warn
---@class quad_module_class
local new = {__type = "quad_module_class"}
local InstanceNew = Instance.new
local event = shared.Event ---@type quad_module_event
local bind = event.Bind
local store = shared.Store ---@type quad_module_store
local initStoreRegisterBinding = store.__initStoreRegisterBinding
local addObject = store.AddObject
local storeNew = store.New
local mount = shared.Mount ---@type quad_module_mount
local getHolder = mount.GetHolder
local mountfunc = mount.MountOne
local style = shared.Style ---@type quad_module_style
local styleList = style.Styles
local parseStyles = style.ParseStyles
local advancedTween = shared.Tween ---@type quad_module_tween
local round = shared.Round ---@type quad_module_round
local signal = shared.Signal ---@type quad_module_signal
local bindable = signal.Bindable
local function InstanceNewWithName(classname,parent,name)
local item = InstanceNew(classname,parent)
item.Name = name
return item
end
local function GetProperty(item,index,ClassName)
if type(item) == "table" then return item[index] end
ClassName = ClassName or item.ClassName
local isImage = (ClassName == "ImageLabel" or ClassName == "ImageButton")
if (index == "RoundSize") and isImage then
if not round then
error("module 'round' needs to be loaded for get image round size but it is not found on 'src.libs'. you should adding that to src.libs directory")
end
return round.GetRound(item)
elseif index == "Corner" then
local target = (ClassName == "UICorner" and item) or item:FindFirstChildOfClass("UICorner")
return (target and target.CornerRadius.Offset) or 0
elseif index == "PaddingAll" then
local target = (ClassName == "UIPadding" and item) or item:FindFirstChildOfClass("UIPadding")
return (target and target.PaddingLeft) or UDim.new(0,0)
elseif index == "PaddingAllOffset" then
local target = (ClassName == "UIPadding" and item) or item:FindFirstChildOfClass("UIPadding")
return (target and target.PaddingLeft.Offset) or 0
elseif index == "Scale" then
local target = (ClassName == "UIScale" and item) or item:FindFirstChildOfClass("UIScale")
return (target and target.Scale) or 1
end
return item[index]
end
new.GetProperty = GetProperty
local function SetProperty(item,index,value,ClassName)
-- if it is not a roblox instance, just call __index
if type(item) == "table" then
item[index] = value
return
end
-- check class name
ClassName = ClassName or item.ClassName
-- check is ImageLabel or ImageButton
local isImage = (ClassName == "ImageLabel" or ClassName == "ImageButton")
-- Round Image
if index == "RoundSize" and isImage then
if not round then
error("[QUAD] module 'round' needs to be loaded for set image round size but it is not found on 'src.libs'. you should adding that to src.libs directory")
end
round.SetRound(item,value)
-- UIRound
elseif index == "Corner" then
local uiCorner = (ClassName == "UICorner" and item) or item:FindFirstChildOfClass("UICorner") or InstanceNewWithName("UICorner",item,"_quad_round")
uiCorner.CornerRadius = UDim.new(0,value)
-- PaddingAll
elseif index == "PaddingAll" then
local target = (ClassName == "UIPadding" and item) or item:FindFirstChildOfClass("UIPadding") or InstanceNewWithName("UIPadding",item,"_quad_padding")
target.PaddingLeft = value
target.PaddingRight = value
target.PaddingTop = value
target.PaddingBottom = value
-- PaddingAll with Offset
elseif index == "PaddingAllOffset" then
local target = (ClassName == "UIPadding" and item) or item:FindFirstChildOfClass("UIPadding") or InstanceNewWithName("UIPadding",item,"_quad_padding")
local padding = UDim.new(0,value)
target.PaddingLeft = padding
target.PaddingRight = padding
target.PaddingTop = padding
target.PaddingBottom = padding
-- Scale
elseif index == "Scale" then
local target = (ClassName == "UIScale" and item) or item:FindFirstChildOfClass("UIScale") or InstanceNewWithName("UIScale",item,"_quad_scale")
target.Scale = value
else
-- other, just set property
item[index] = value
end
end
new.SetProperty = SetProperty
local function RawGetProperty(item,index)
return item[index]
end
local function PcallGetProperty(item,index)
local ok,err = pcall(RawGetProperty,item,index)
if ok then return err end
return nil
end
new.PcallGetProperty = PcallGetProperty
local function Link(linker,item,index,indexType)
local target = linker.target
local name = linker.name
if indexType == "number" then
-- set
if rawget(target,name) ~= nil then
warn(("%s is exist already. Overwrite it"))
end
rawset(target,name,item)
-- target[name] = item
elseif indexType == "string" then
local targetSignal = target:GetPropertyChangedSignal(name)
local fire = PcallGetProperty(targetSignal,"Fire")
local ok = fire and bind(item,index,function(...)
fire(targetSignal,...)
end,"function")
if not ok then
warn("Failed to bind linker event")
end
end
end
local function ProcessQuadProperty(processedProperty,iprop,holder,item,className,index,value)
if processedProperty[index] then
return
end
local valueType = typeof(value)
local indexType = typeof(index)
local quadType = valueType == "table" and PcallGetProperty(value,"__type")
if quadType == "quad_linker" then
-- linking
Link(value,item,index,indexType)
elseif indexType == "string" and quadType == "quad_register" then
-- register (bind to store event)
processedProperty[index] = true -- ignore next
-- store reading
do
local setValue = value:CalcWithDefault(item)
if setValue == nil then
warn "[Quad] register return value must not be nil, but got nil. did you inited values before?"
end
SetProperty(item,index,setValue,className)
end
-- adding handle function (binding)
local function regFn(_,newValue,key)
local setValue,tween = value:CalcWithNewValue(item,newValue,key)
if tween then
if not advancedTween then
return warn "[QUAD] module 'AdvancedTween' needs to be loaded for tween properties but it is not found on 'src.libs'. you should adding that to src.libs directory"
end
-- local ended, onStepped
-- if tween.Ended then
-- ended = function (...)
-- tween.Ended(item,...)
-- end
-- end
-- if tween.OnStepped then
-- onStepped = function (...)
-- tween.OnStepped(item,...)
-- end
-- end
advancedTween.RunTween(item,tween,{[index] = setValue},nil,nil,SetProperty,GetProperty)
else
SetProperty(item,index,setValue,className)
end
end
value:Register(regFn)
-- this is using hacky of roblox instance
-- this is will keep reference from week table until
-- inscance got GCed
item:GetPropertyChangedSignal("ClassName"):Connect(regFn)
elseif (valueType == "function" or valueType == "table") and indexType == "string" and bind(item,index,value,valueType) then -- connect event
-- event binding
elseif indexType == "string" then
processedProperty[index] = true -- ignore next
-- prop set
SetProperty(item,index,value,className)
elseif indexType == "number" and valueType == "table" and quadType == "quad_style" then -- style
-- style parsing
for _,thisStyle in ipairs(parseStyles(value)) do
if not processedProperty[thisStyle] then
processedProperty[thisStyle] = true
for styleIndex,styleValue in pairs(thisStyle) do
if type(styleValue) ~= "table" or pcall(styleValue,"__type") ~= "quad_style" then
ProcessQuadProperty(processedProperty,iprop,holder,item,className,styleIndex,styleValue)
end
end
end
end
elseif indexType == "number" and valueType ~= "boolean" then -- object
-- child object
if iprop ~= 1 then
value = value:Clone()
end
mountfunc(item,value,holder,true)
end
end
-- make object that from instance, class and more
function new.Make(ClassName,...) -- render object
-- make object from ClassName
local item
local classOfClassName = type(ClassName)
if classOfClassName == "string" then -- if classname is a string value, cdall instance.new for making new roblox instance
item = InstanceNew(ClassName)
elseif classOfClassName == "function" then -- if classname is a function value, call it for making new object (OOP object)
item = ClassName()
elseif classOfClassName == "table" then -- if classname is a calss what is included new function, call it for making new object (object)
local func = ClassName.new or ClassName.New or ClassName.__new
if ClassName.__noSetup then -- if is support initing props
local childs,props,parsed,binds,links = {},pack(...),{},{},{}
for iprop = props.n,1,-1 do
local prop = props[iprop]
if prop then
for i,v in pairs(prop) do
if type(i) == "number" then
insert(childs[i],(iprop == 1) and v or v:Clone())
elseif bind(i) then
binds[i] = v
elseif PcallGetProperty(v,"__type") == "quad_linker" then
links[i] = v
else
parsed[i] = v
end
end
end
end
item = func(parsed)
local holder = getHolder(item)
for _,v in ipairs(childs) do
mountfunc(item,v,holder)
end
for i,v in pairs(links) do
Link(v,item,i,type(i))
end
for i,v in pairs(binds) do
bind(item,i,v)
end
return item
end
item = func()
end
if not item then -- if cannot make new object, ignore this call
local str = tostring(ClassName)
warn(("[Quad] fail to make item '%s', did you forget to checking the class '%s' is exist?"):format(str,str))
end
-- set property and adding child and binding functions
local holder = getHolder(item) -- to __holder or holder or it self (for tabled)
-- for iprop,prop in ipairs({...}) do
local propsListArray = pack(...)
local processedProperty = {}
for iprop = 1,propsListArray.n do
local prop = propsListArray[iprop]
if prop then
local binding = {}
-- set property
for index,value in pairs(prop) do
if bind(index) then
binding[index] = value
elseif type(index) ~= "number" then
ProcessQuadProperty(processedProperty,iprop,holder,item,ClassName,index,value)
end
end
-- childs
for index,value in ipairs(prop) do
ProcessQuadProperty(processedProperty,iprop,holder,item,ClassName,index,value)
end
-- bindings
for index,value in pairs(binding) do
bind(item,index,value)
end
end
end
return item
end
local make = new.Make
-- import quad object
function new.Import(ClassName,defaultProperties) -- make new quad class object
if type(ClassName) == "userdata" and ClassName.ClassName == "ModuleScript" then
ClassName = require(ClassName)
end
local this = defaultProperties or {}
setmetatable(this,{
__type = "quad_imported_class",
__call = function (self,prop,...)
local propType = type(prop)
if propType == "string" then
local lastName = prop
return function (nprop,...)
nprop = nprop or {}
if not nprop.Name then
nprop.Name = gsub(gsub(match(lastName,"[^,]+")," +$",""),"^ +","")
end
for styleName,styleObj in pairs(styleList) do
if match(prop,styleName) then
insert(nprop,styleObj)
end
end
local item = make(ClassName,nprop,...,this)
addObject(lastName,item)
return item
end
elseif propType == "nil" then
return make(ClassName,this)
end
return make(ClassName,prop,...,this)
end
})
return this
end
-- set module calling function
setmetatable(new,{
__call = function (self,...)
return new.Import(...)
end
})
local function Linker(target,name)
return {
__type = "quad_linker";
target = target;
name = name;
}
end
-- make class
function new.Extend()
local this = {__type = "quad_extend"}
--- make new object
function this.New(prop)
-- make metatable
prop = storeNew(prop,nil)
local parent = prop and (prop.Parent or prop.parent)
local self = {
__prop = prop;
__parent = parent;
__propertyChangedSignals = {};
}
local init = this.Init
if init then
init(self,prop)
end
setmetatable(self,this)
initStoreRegisterBinding(prop,this)
-- render that
local object = self:Render(prop)
rawset(self,"__object",object)
rawset(self,"__holder",object)
if parent then
rawset(self,"__parent",parent)
mountfunc(self,parent)
end
-- prevent gc
((type(object) == "table" and object.__object) or object)
:GetPropertyChangedSignal "ClassName":Connect(function()
return self
end)
--after render
local afterRender = this.AfterRender
if afterRender then
afterRender(self,object,prop)
end
return self
end
function this:GetPropertyChangedSignal(propertyName)
local thisSignal = self.__propertyChangedSignals[propertyName]
if thisSignal then
return thisSignal
end
thisSignal = bindable.New()
self.__propertyChangedSignals[propertyName] = thisSignal
return thisSignal
end
function this:EmitPropertyChangedSignal(propertyName,value)
local thisSignal = self.__propertyChangedSignals[propertyName]
if not thisSignal then
return
end
thisSignal:Fire(value)
end
--- re-render object (if some props chaged, call this to render again)
function this:Update() -- swap object
local object = rawget(self,"__object") -- get last instance
local parent = object and object.Parent -- date parent
if not object then
parent = rawget(self,"__parent")
end
local lastObject = object
if lastObject then
lastObject.Parent = nil
end
object = self:Render(self.__prop) -- new instance
rawset(self,"__holder",object)
rawset(self,"__object",object)
object.Parent = getHolder(parent) -- update parent
-- restore childs
local child = rawget(self,"__child")
if child then
for _,v in pairs(child) do
if v then
v.Parent = object
end
end
end
-- prevnet gc
((type(object) == "table" and object.__object) or object):GetPropertyChangedSignal "ClassName":Connect(function()
return self
end)
if lastObject then -- remove old instance
self:Destroy(lastObject)
end
--after render
local afterRender = this.AfterRender
if afterRender then
afterRender(self,object)
end
end
-- define destroy method
function this:Destroy(object)
local unload = this.Unload
object = object or rawget(self,"__object")
if object then
if unload then
unload(self,object)
else
local destroy = (object.Destroy or object.destroy)
if destroy then
pcall(destroy,object)
end
end
end
self = nil
end
--- link to new
function this.__call(self,...)
if self == this then
return (self.new or self.New or self.__new)(...)
end
local name = select(1,...)
if type(name) ~= "string" then
error(("Name must be string, but got %s"):format(type(name)))
end
return Linker(self,name)
end
-- indexer (getter)
function this:__index(k)
local getter = this.Getter[k]
if getter then
return getter(self,rawget(self,"__object"))
end
-- from this (have more priority)
local fromThis = this[k]
if fromThis then
return fromThis
end
-- from prop, not start with '_' (private value)
if k:sub(1,1) ~= "_" then
return self.__prop[k]
end
end
--- new indexer (setter)
function this:__newindex(k,v)
-- from setter
local setter = this.Setter[k]
if setter then
return setter(self,v,rawget(self,"__object"))
end
-- is private (self value)
if k == "holder" or k:sub(1,1) == "_" then
return rawset(self,k,v)
end
-- prop update
self.__prop[k] = v
if this.UpdateTriggers[k] then
self:Update()
end
-- call PropertyChangedSignal
if self.__propertyChangedSignals[k] and self[k] ~= v then
self:EmitPropertyChangedSignal(k,v)
end
end
this.Getter = {
Parent = function(self)
return rawget(self,"__parent")
end
}
this.Setter = {
Parent = function(self,parent,object)
rawset(self,"__parent",parent)
if object then
object.Parent = getHolder(parent)
end
end
}
this.UpdateTriggers = {}
this.__noSetup = true
return this
end
return new
end
return module