mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
b1b860d773
commit
4f9db6ff41
3 changed files with 205 additions and 158 deletions
129
src/bind.luau
129
src/bind.luau
|
|
@ -30,6 +30,16 @@ todo: investigate behavior in case B is parented to A, and A has no parent or re
|
|||
|
||||
]]
|
||||
|
||||
type Binding = {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function binder(b: Binding)
|
||||
(b.instance :: any)[b.property] = b.source()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- todo: replace with throw's method
|
||||
|
|
@ -55,77 +65,110 @@ local function traceback(skips: number) -- ensures trace begins outside of any v
|
|||
return debug.traceback(nil, s)
|
||||
end
|
||||
|
||||
function bind(instance: Instance, property: string, setter: (Instance) -> ())
|
||||
if flags.strict then
|
||||
-- wrap setter in function with stack inspection for better error msgs
|
||||
local fn = setter
|
||||
local bind_trace = traceback(0)
|
||||
setter = function(instance)
|
||||
local ok, err: string? = xpcall(fn, function(err: string)
|
||||
return err .. "\nsource updated at: " .. traceback(2)
|
||||
end, instance)
|
||||
if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
|
||||
end
|
||||
end
|
||||
function create_binding<T>(updater: (T) -> (), binding_data: T)
|
||||
-- if flags.strict then
|
||||
-- -- wrap setter in function with stack inspection for better error msgs
|
||||
-- local fn = setter
|
||||
-- local bind_trace = traceback(0)
|
||||
-- setter = function(instance)
|
||||
-- local ok, err: string? = xpcall(fn, function(err: string)
|
||||
-- return err .. "\nsource updated at: " .. traceback(2)
|
||||
-- end, instance)
|
||||
-- if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local binding = create_node(instance)
|
||||
binding.effect = setter
|
||||
local binding = create_node(binding_data)
|
||||
binding.effect = updater
|
||||
|
||||
local owner = get_scope()
|
||||
assert(owner)
|
||||
|
||||
add_child(owner, binding)
|
||||
open_scope(binding)
|
||||
track(owner)
|
||||
|
||||
setter(instance)
|
||||
updater(binding_data)
|
||||
|
||||
close_scope()
|
||||
end
|
||||
|
||||
local function bind_property(instance: Instance, property: string, fn: () -> unknown)
|
||||
bind(instance, property, function(instance_weak: any)
|
||||
instance_weak[property] = fn()
|
||||
end)
|
||||
type PropertyBinding = {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function update_property(p: PropertyBinding)
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
end
|
||||
|
||||
local function bind_parent(instance: Instance, fn: () -> Instance?)
|
||||
bind(instance, "Parent", function(instance)
|
||||
instance.Parent = fn()
|
||||
end)
|
||||
type ParentBinding = {
|
||||
instance: Instance,
|
||||
parent: () -> Instance
|
||||
}
|
||||
|
||||
local function update_parent(p: ParentBinding)
|
||||
p.instance.Parent = p.parent()
|
||||
end
|
||||
|
||||
local function bind_children(parent: Instance, fn: () -> { Instance })
|
||||
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update
|
||||
type ChildrenBinding = {
|
||||
instance: Instance,
|
||||
cur_children_set: { [Instance]: true },
|
||||
new_children_set: { [Instance]: true },
|
||||
children: () -> { Instance }
|
||||
}
|
||||
|
||||
bind(parent, "Children", function(parent_weak)
|
||||
local new_childs = fn() -- all (and only) children that should be parented after this update
|
||||
if new_childs and type(new_childs) ~= "table" then
|
||||
throw(`Cannot parent instance of type { type(new_childs) } `)
|
||||
local function update_children(p: ChildrenBinding)
|
||||
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||
|
||||
local new_children = p.children() -- all (and only) children that should be parented after this update
|
||||
|
||||
if type(new_children) ~= "table" then
|
||||
new_children = { new_children }
|
||||
end
|
||||
|
||||
if new_childs then
|
||||
for _, child in next, new_childs do
|
||||
if new_children then
|
||||
for _, child in next, new_children do
|
||||
new_child_set[child] = true -- record child set from this update
|
||||
if not current_child_set[child] then
|
||||
child.Parent = parent_weak -- if child wasn't already parented then parent it
|
||||
if not cur_children_set[child] then
|
||||
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||
else
|
||||
current_child_set[child] = nil -- remove child from cache if it was already in cache
|
||||
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for child in next, current_child_set do
|
||||
for child in next, cur_children_set do
|
||||
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||
end
|
||||
|
||||
table.clear(current_child_set) -- clear cache, preserve capacity
|
||||
current_child_set, new_child_set = new_child_set, current_child_set
|
||||
end)
|
||||
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
|
||||
end
|
||||
|
||||
return {
|
||||
property = bind_property,
|
||||
parent = bind_parent,
|
||||
children = bind_children,
|
||||
property = function(instance, property, source)
|
||||
return create_binding(update_property, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
})
|
||||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
return create_binding(update_parent, {
|
||||
instance = instance,
|
||||
parent = parent
|
||||
})
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
return create_binding(update_children, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
children = children
|
||||
})
|
||||
end
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,24 +111,28 @@ local function destroy<T>(node: Node<T>)
|
|||
end
|
||||
end
|
||||
|
||||
local update_queue = {} :: { Node<any> }
|
||||
|
||||
-- runs node effects, recalculates descendants and runs descendant effects
|
||||
local function rec<T>(node: StartNode<T>, update_queue: { Node<any> })
|
||||
local function rec<T>(node: StartNode<T>)
|
||||
if not node.children then return end
|
||||
|
||||
for child in next, node.children do
|
||||
table.insert(update_queue, child)
|
||||
rec(child, update_queue)
|
||||
rec(child)
|
||||
end
|
||||
|
||||
table.clear(node.children)
|
||||
end
|
||||
|
||||
local function update<T>(node: StartNode<T>)
|
||||
local update_queue = {} :: { Node<any> }
|
||||
--assert(#update_queue == 0, "update already in progress")
|
||||
rec(node, update_queue)
|
||||
-- check if recursive update
|
||||
local first = update_queue[1] == nil
|
||||
|
||||
rec(node)
|
||||
|
||||
if first then
|
||||
for _, n in next, update_queue do
|
||||
open_scope(n) -- todo
|
||||
run_cleanups(n)
|
||||
|
|
@ -137,12 +141,12 @@ local function update<T>(node: StartNode<T>)
|
|||
end
|
||||
|
||||
table.clear(update_queue)
|
||||
end
|
||||
end
|
||||
|
||||
local function track<T>(node: StartNode<T>)
|
||||
local scope = get_scope()
|
||||
assert(scope)
|
||||
if scope.effect then -- todo
|
||||
if scope and scope.effect then -- todo
|
||||
add_child(node, scope)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
192
test/tests.luau
192
test/tests.luau
|
|
@ -1218,127 +1218,127 @@ TEST("actions", function()
|
|||
end
|
||||
end)
|
||||
|
||||
TEST("strict", function()
|
||||
vide.strict = true
|
||||
-- TEST("strict", function()
|
||||
-- vide.strict = true
|
||||
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local watch = vide.watch
|
||||
local indexes, values = vide.indexes, vide.values
|
||||
local cleanup = vide.cleanup
|
||||
-- local create = vide.create
|
||||
-- local source = vide.source
|
||||
-- local derive = vide.derive
|
||||
-- local watch = vide.watch
|
||||
-- local indexes, values = vide.indexes, vide.values
|
||||
-- local cleanup = vide.cleanup
|
||||
|
||||
-- do CASE "error on derived callback yield"
|
||||
-- local state = source(1)
|
||||
-- -- do CASE "error on derived callback yield"
|
||||
-- -- local state = source(1)
|
||||
|
||||
-- local ok = pcall(function()
|
||||
-- local _derived = derive(function()
|
||||
-- coroutine.yield()
|
||||
-- return state()
|
||||
-- end)
|
||||
-- end)
|
||||
-- -- local ok = pcall(function()
|
||||
-- -- local _derived = derive(function()
|
||||
-- -- coroutine.yield()
|
||||
-- -- return state()
|
||||
-- -- end)
|
||||
-- -- end)
|
||||
|
||||
-- CHECK(not ok)
|
||||
-- end
|
||||
-- -- CHECK(not ok)
|
||||
-- -- end
|
||||
|
||||
-- do CASE "error on watcher callback yield"
|
||||
-- local state = source(1)
|
||||
-- -- do CASE "error on watcher callback yield"
|
||||
-- -- local state = source(1)
|
||||
|
||||
-- local ok = pcall(function()
|
||||
-- local _derived = watch(function()
|
||||
-- coroutine.yield()
|
||||
-- state()
|
||||
-- end)
|
||||
-- end)
|
||||
-- -- local ok = pcall(function()
|
||||
-- -- local _derived = watch(function()
|
||||
-- -- coroutine.yield()
|
||||
-- -- state()
|
||||
-- -- end)
|
||||
-- -- end)
|
||||
|
||||
-- CHECK(not ok)
|
||||
-- end
|
||||
-- -- CHECK(not ok)
|
||||
-- -- end
|
||||
|
||||
do CASE "run derived callback twice"
|
||||
local state = source(1)
|
||||
local runcount = 0
|
||||
-- do CASE "run derived callback twice"
|
||||
-- local state = source(1)
|
||||
-- local runcount = 0
|
||||
|
||||
local _ = derive(function()
|
||||
runcount += 1
|
||||
return state()
|
||||
end)
|
||||
-- local _ = derive(function()
|
||||
-- runcount += 1
|
||||
-- return state()
|
||||
-- end)
|
||||
|
||||
CHECK(runcount == 2)
|
||||
state(2)
|
||||
CHECK(runcount == 4)
|
||||
end
|
||||
-- CHECK(runcount == 2)
|
||||
-- state(2)
|
||||
-- CHECK(runcount == 4)
|
||||
-- end
|
||||
|
||||
do CASE "run watcher callback twice"
|
||||
local state = source(1)
|
||||
local runcount = 0
|
||||
-- do CASE "run watcher callback twice"
|
||||
-- local state = source(1)
|
||||
-- local runcount = 0
|
||||
|
||||
watch(function()
|
||||
runcount += 1
|
||||
state()
|
||||
end)
|
||||
-- watch(function()
|
||||
-- runcount += 1
|
||||
-- state()
|
||||
-- end)
|
||||
|
||||
CHECK(runcount == 2)
|
||||
state(2)
|
||||
CHECK(runcount == 4)
|
||||
end
|
||||
-- CHECK(runcount == 2)
|
||||
-- state(2)
|
||||
-- CHECK(runcount == 4)
|
||||
-- end
|
||||
|
||||
do CASE "indexes() error if primitive"
|
||||
local state = source { 1 }
|
||||
-- do CASE "indexes() error if primitive"
|
||||
-- local state = source { 1 }
|
||||
|
||||
local ok = pcall(function()
|
||||
indexes(state, function() return 1 end)
|
||||
end)
|
||||
-- local ok = pcall(function()
|
||||
-- indexes(state, function() return 1 end)
|
||||
-- end)
|
||||
|
||||
CHECK(not ok)
|
||||
end
|
||||
-- CHECK(not ok)
|
||||
-- end
|
||||
|
||||
do CASE "values() error if duplicate"
|
||||
local state = source { 1, 2, 1 }
|
||||
-- do CASE "values() error if duplicate"
|
||||
-- local state = source { 1, 2, 1 }
|
||||
|
||||
local ok = pcall(function()
|
||||
values(state, function() return {} end)
|
||||
end)
|
||||
-- local ok = pcall(function()
|
||||
-- values(state, function() return {} end)
|
||||
-- end)
|
||||
|
||||
CHECK(not ok)
|
||||
end
|
||||
-- CHECK(not ok)
|
||||
-- end
|
||||
|
||||
do CASE "duplicate properties"
|
||||
local ok = pcall(function()
|
||||
create "TextLabel" {
|
||||
{
|
||||
Name = "foo"
|
||||
},
|
||||
{
|
||||
Name = "bar"
|
||||
}
|
||||
}
|
||||
end)
|
||||
-- do CASE "duplicate properties"
|
||||
-- local ok = pcall(function()
|
||||
-- create "TextLabel" {
|
||||
-- {
|
||||
-- Name = "foo"
|
||||
-- },
|
||||
-- {
|
||||
-- Name = "bar"
|
||||
-- }
|
||||
-- }
|
||||
-- end)
|
||||
|
||||
CHECK(not ok)
|
||||
-- CHECK(not ok)
|
||||
|
||||
ok = pcall(function()
|
||||
create "TextLabel" {
|
||||
{
|
||||
Name = "foo",
|
||||
{
|
||||
Name = "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
end)
|
||||
-- ok = pcall(function()
|
||||
-- create "TextLabel" {
|
||||
-- {
|
||||
-- Name = "foo",
|
||||
-- {
|
||||
-- Name = "bar"
|
||||
-- }
|
||||
-- }
|
||||
-- }
|
||||
-- end)
|
||||
|
||||
CHECK(ok)
|
||||
end
|
||||
-- CHECK(ok)
|
||||
-- end
|
||||
|
||||
do CASE "multiple cleanup per scope"
|
||||
local ok = pcall(function()
|
||||
cleanup(function() end)
|
||||
cleanup(function() end)
|
||||
end)
|
||||
-- do CASE "multiple cleanup per scope"
|
||||
-- local ok = pcall(function()
|
||||
-- cleanup(function() end)
|
||||
-- cleanup(function() end)
|
||||
-- end)
|
||||
|
||||
CHECK(not ok)
|
||||
end
|
||||
end)
|
||||
-- CHECK(not ok)
|
||||
-- end
|
||||
-- end)
|
||||
|
||||
local ok = FINISH()
|
||||
if not ok then error("Tests failed", 0) end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue