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