2013/Libraries/Fusion/Instances/Children.luau

158 lines
4.1 KiB
Plaintext

--!strict
--[[
A special key for property tables, which parents any given descendants into
an instance.
]]
local PubTypes = require "../PubTypes"
local External = require "../External"
local logWarn = require "../Logging/logWarn"
local Observer = require "../State/Observer"
local peek = require "../State/peek"
local isState = require "../State/isState"
local typeof = require "../../../Modules/Polyfill/typeof"
type Set<T> = { [T]: boolean }
-- Experimental flag: name children based on the key used in the [Children] table
local EXPERIMENTAL_AUTO_NAMING = false
local Children = {}
Children.type = "SpecialKey"
Children.kind = "Children"
Children.stage = "descendants"
function Children:apply(
propValue: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
local newParented: Set<Instance> = {}
local oldParented: Set<Instance> = {}
-- save disconnection functions for state object observers
local newDisconnects: { [PubTypes.StateObject<any>]: () -> () } = {}
local oldDisconnects: { [PubTypes.StateObject<any>]: () -> () } = {}
local updateQueued = false
local queueUpdate: () -> ()
-- Rescans this key's value to find new instances to parent and state objects
-- to observe for changes; then unparents instances no longer found and
-- disconnects observers for state objects no longer present.
local function updateChildren()
if not updateQueued then
return -- this update may have been canceled by destruction, etc.
end
updateQueued = false
oldParented, newParented = newParented, oldParented
oldDisconnects, newDisconnects = newDisconnects, oldDisconnects
-- table.clear(newParented)
for i, _ in pairs(newParented) do
newParented[i] = nil
end
-- table.clear(newDisconnects)
for i, _ in pairs(newDisconnects) do
newDisconnects[i] = nil
end
local function processChild(child: any, autoName: string?)
local childType = typeof(child)
if childType == "Instance" then
-- case 1; single instance
newParented[child] = true
if oldParented[child] == nil then
-- wasn't previously present
-- TODO: check for ancestry conflicts here
child.Parent = applyTo
else
-- previously here; we want to reuse, so remove from old
-- set so we don't encounter it during unparenting
oldParented[child] = nil
end
if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then
child.Name = autoName
end
elseif isState(child) then
-- case 2; state object
local value = peek(child)
-- allow nil to represent the absence of a child
if value ~= nil then
processChild(value, autoName)
end
local disconnect = oldDisconnects[child]
if disconnect == nil then
-- wasn't previously present
disconnect = Observer(child):onChange(queueUpdate)
else
-- previously here; we want to reuse, so remove from old
-- set so we don't encounter it during unparenting
oldDisconnects[child] = nil
end
newDisconnects[child] = disconnect
elseif childType == "table" then
-- case 3; table of objects
for key, subChild in pairs(child) do
local keyType = typeof(key)
local subAutoName: string? = nil
if keyType == "string" then
subAutoName = key
elseif keyType == "number" and autoName ~= nil then
subAutoName = autoName .. "_" .. key
end
processChild(subChild, subAutoName)
end
else
logWarn("unrecognisedChildType", childType)
end
end
if propValue ~= nil then
-- `propValue` is set to nil on cleanup, so we don't process children
-- in that case
processChild(propValue)
end
-- unparent any children that are no longer present
for oldInstance in pairs(oldParented) do
oldInstance.Parent = nil
end
-- disconnect observers which weren't reused
for _, disconnect in pairs(oldDisconnects) do
disconnect()
end
end
queueUpdate = function()
if not updateQueued then
updateQueued = true
External.doTaskDeferred(updateChildren)
end
end
table.insert(cleanupTasks, function()
propValue = nil
updateQueued = true
updateChildren()
end)
-- perform initial child parenting
updateQueued = true
updateChildren()
end
return Children :: PubTypes.SpecialKey