mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge branch 'centau:main' into main
This commit is contained in:
commit
92cf612cef
7 changed files with 221 additions and 45 deletions
|
|
@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
effects to set children.
|
effects to set children.
|
||||||
- `spring()` returns a second value, a setter to set position, velocity and
|
- `spring()` returns a second value, a setter to set position, velocity and
|
||||||
impulse.
|
impulse.
|
||||||
|
- `show()` now receives a source to its callback returning the current value
|
||||||
|
of the condition.
|
||||||
|
- Ignore `false` passed as a child.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
@ -25,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
destroyed.
|
destroyed.
|
||||||
- Error reporting should be improved with better formatting when effects invoke
|
- Error reporting should be improved with better formatting when effects invoke
|
||||||
other effects and no more loss of stack traces.
|
other effects and no more loss of stack traces.
|
||||||
|
- Nesting parent properties now work, and they are now also checked for
|
||||||
|
duplicates like other properties.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ But the disconnecting of many signals and connections is tedious and verbose.
|
||||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||||
API, given that you follow a few rules.
|
API, given that you follow a few rules.
|
||||||
|
|
||||||
Thre are two types of scopes: stable and reactive.
|
There are two types of scopes: stable and reactive.
|
||||||
|
|
||||||
- A scope must be created within another scope.
|
- A scope must be created within another scope.
|
||||||
- Stable scopes never rerun.
|
- Stable scopes never rerun.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ type Cache = {
|
||||||
Array<(Instance) -> ()> -- action callbacks
|
Array<(Instance) -> ()> -- action callbacks
|
||||||
>,
|
>,
|
||||||
|
|
||||||
|
-- what to parent the instance to after running actions
|
||||||
|
parent: unknown,
|
||||||
|
|
||||||
-- cache to detect duplicate property setting at same nesting depth
|
-- cache to detect duplicate property setting at same nesting depth
|
||||||
nested_debug: Map<
|
nested_debug: Map<
|
||||||
number, -- depth
|
number, -- depth
|
||||||
|
|
@ -47,6 +50,7 @@ local function borrow_cache(): Cache
|
||||||
actions = setmetatable({} :: any, { -- lazy init
|
actions = setmetatable({} :: any, { -- lazy init
|
||||||
__index = function(self, i) self[i] = {}; return self[i] end
|
__index = function(self, i) self[i] = {}; return self[i] end
|
||||||
}),
|
}),
|
||||||
|
parent = nil,
|
||||||
nested_debug = setmetatable({} :: any, {
|
nested_debug = setmetatable({} :: any, {
|
||||||
__index = function(self, i: number) self[i] = {}; return self[i] end
|
__index = function(self, i: number) self[i] = {}; return self[i] end
|
||||||
}),
|
}),
|
||||||
|
|
@ -61,8 +65,6 @@ end
|
||||||
|
|
||||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
||||||
for property, value in properties do
|
for property, value in properties do
|
||||||
if property == "Parent" then continue end
|
|
||||||
|
|
||||||
if type(property) == "string" then
|
if type(property) == "string" then
|
||||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
if flags.strict then -- check for duplicate property assignment at nesting depth
|
||||||
if cache.nested_debug[depth][property] then
|
if cache.nested_debug[depth][property] then
|
||||||
|
|
@ -71,6 +73,11 @@ local function process_properties(properties: Map<unknown, unknown>, instance: I
|
||||||
cache.nested_debug[depth][property] = true
|
cache.nested_debug[depth][property] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if property == "Parent" then
|
||||||
|
cache.parent = value
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
if type(value) == "function" then
|
if type(value) == "function" then
|
||||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||||
table.insert(cache.events, property) -- add event name to buffer
|
table.insert(cache.events, property) -- add event name to buffer
|
||||||
|
|
@ -93,7 +100,7 @@ local function process_properties(properties: Map<unknown, unknown>, instance: I
|
||||||
else
|
else
|
||||||
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
|
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
|
||||||
end
|
end
|
||||||
else
|
elseif type(value) == "userdata" then
|
||||||
(value :: Instance).Parent = instance -- parent child
|
(value :: Instance).Parent = instance -- parent child
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -106,9 +113,6 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
error "attempt to call a constructor returned by create() with no properties"
|
error "attempt to call a constructor returned by create() with no properties"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- queue parent assignment if any for last
|
|
||||||
local parent: unknown = properties.Parent
|
|
||||||
|
|
||||||
local caches = borrow_cache()
|
local caches = borrow_cache()
|
||||||
local events = caches.events
|
local events = caches.events
|
||||||
local actions = caches.actions
|
local actions = caches.actions
|
||||||
|
|
@ -135,6 +139,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local parent = caches.parent
|
||||||
if parent then
|
if parent then
|
||||||
if type(parent) == "function" then
|
if type(parent) == "function" then
|
||||||
implicit_effect.parent(instance, parent :: () -> Instance)
|
implicit_effect.parent(instance, parent :: () -> Instance)
|
||||||
|
|
@ -145,6 +150,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
|
|
||||||
table.clear(events)
|
table.clear(events)
|
||||||
for _, queued in next, actions do table.clear(queued) end
|
for _, queued in next, actions do table.clear(queued) end
|
||||||
|
caches.parent = nil
|
||||||
if flags.strict then table.clear(nested_debug) end
|
if flags.strict then table.clear(nested_debug) end
|
||||||
table.clear(nested_stack)
|
table.clear(nested_stack)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
local switch = require "./switch"
|
local source = require "./source"
|
||||||
|
local derive = require "./derive"
|
||||||
|
local effect = require "./effect"
|
||||||
|
local untrack = require "./untrack"
|
||||||
|
|
||||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
local function show<T, U>(input: () -> T?, component: (() -> T) -> U, fallback: (() -> U)?): () -> U?
|
||||||
local function truthy()
|
local filtered_input = source()
|
||||||
return not not source()
|
|
||||||
end
|
|
||||||
|
|
||||||
return switch(truthy) {
|
effect(function()
|
||||||
[true] = component,
|
local v = input()
|
||||||
[false] = fallback,
|
if v then
|
||||||
}
|
filtered_input(v)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local input_is_truthy = derive(function()
|
||||||
|
return not not input()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- todo: is this needed?
|
||||||
|
-- local filtered_input_is_truthy = derive(function()
|
||||||
|
-- return not not filtered_input()
|
||||||
|
-- end)
|
||||||
|
|
||||||
|
return derive(function()
|
||||||
|
return
|
||||||
|
if input_is_truthy() then untrack(function() return component(filtered_input :: () -> T) end)
|
||||||
|
elseif fallback then untrack(fallback)
|
||||||
|
else nil
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return show ::
|
return show
|
||||||
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
|
|
||||||
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)
|
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@ local function untrack<T>(source: () -> T): T
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return untrack
|
return untrack :: ( <T>(fn: () -> T) -> T ) & ( (fn: () -> ()) -> () )
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,9 @@ local Instance = {} :: any do
|
||||||
local proxies = {} :: { [Data]: userdata? }
|
local proxies = {} :: { [Data]: userdata? }
|
||||||
setmetatable(proxies :: any, { __mode = "v" })
|
setmetatable(proxies :: any, { __mode = "v" })
|
||||||
|
|
||||||
|
-- allocate variables for the metamethods as __index and get_proxy cross refrences each other
|
||||||
|
local __index, __newindex
|
||||||
|
|
||||||
local function get_data(userdata: userdata): Data
|
local function get_data(userdata: userdata): Data
|
||||||
local function f(userdata: userdata): ProxyMT
|
local function f(userdata: userdata): ProxyMT
|
||||||
return getmetatable(userdata :: any)
|
return getmetatable(userdata :: any)
|
||||||
|
|
@ -94,6 +97,19 @@ local Instance = {} :: any do
|
||||||
return f(userdata).data
|
return f(userdata).data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function get_proxy(data: Data): userdata
|
||||||
|
return proxies[data] or (function()
|
||||||
|
local userdata = newproxy(true)
|
||||||
|
local proxy = getmetatable(userdata)
|
||||||
|
proxy.proxy = userdata
|
||||||
|
proxy.data = data
|
||||||
|
proxy.__index = __index
|
||||||
|
proxy.__newindex = __newindex
|
||||||
|
proxies[data] = userdata
|
||||||
|
return userdata
|
||||||
|
end)()
|
||||||
|
end
|
||||||
|
|
||||||
local function is_instance(value: unknown): boolean
|
local function is_instance(value: unknown): boolean
|
||||||
local mt = getmetatable(value :: any)
|
local mt = getmetatable(value :: any)
|
||||||
return mt and mt.data and mt.data.type == "Instance"
|
return mt and mt.data and mt.data.type == "Instance"
|
||||||
|
|
@ -101,16 +117,16 @@ local Instance = {} :: any do
|
||||||
|
|
||||||
local methods = {}
|
local methods = {}
|
||||||
|
|
||||||
local function __index(userdata: userdata, property: string): ()
|
__index = function(userdata: userdata, property: string): ()
|
||||||
local data = get_data(userdata)
|
local data = get_data(userdata)
|
||||||
return if methods[property] then methods[property]
|
return if methods[property] then methods[property]
|
||||||
elseif property == "Name" then data.name
|
elseif property == "Name" then data.name
|
||||||
elseif property == "Parent" then data.parent
|
elseif property == "Parent" then (data.parent and get_proxy(data.parent))
|
||||||
elseif property == "Destroying" then data.destroying
|
elseif property == "Destroying" then data.destroying
|
||||||
else data.properties[property]
|
else data.properties[property]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
__newindex = function(userdata: userdata, property: string, value: unknown)
|
||||||
local data = get_data(userdata)
|
local data = get_data(userdata)
|
||||||
if property == "Name" then
|
if property == "Name" then
|
||||||
if type(value) ~= "string" then error("name must be a string", 2) end
|
if type(value) ~= "string" then error("name must be a string", 2) end
|
||||||
|
|
@ -135,19 +151,6 @@ local Instance = {} :: any do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_proxy(data: Data): userdata
|
|
||||||
return proxies[data] or (function()
|
|
||||||
local userdata = newproxy(true)
|
|
||||||
local proxy = getmetatable(userdata)
|
|
||||||
proxy.proxy = userdata
|
|
||||||
proxy.data = data
|
|
||||||
proxy.__index = __index
|
|
||||||
proxy.__newindex = __newindex
|
|
||||||
proxies[data] = userdata
|
|
||||||
return userdata
|
|
||||||
end)()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Instance.new(class: string): Instance
|
function Instance.new(class: string): Instance
|
||||||
local data = {
|
local data = {
|
||||||
name = "UNNAMED",
|
name = "UNNAMED",
|
||||||
|
|
|
||||||
163
test/tests.luau
163
test/tests.luau
|
|
@ -759,6 +759,13 @@ TEST("create()", wrap_root(function()
|
||||||
CHECK(text.Text == "test")
|
CHECK(text.Text == "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "set nested parent"
|
||||||
|
local frame = create "Frame" {}
|
||||||
|
local text = create "TextLavel" { { Parent = frame } }
|
||||||
|
CHECK(frame:GetChildren()[1] == text)
|
||||||
|
CHECK(text.Parent == frame)
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "nested deferred"
|
do CASE "nested deferred"
|
||||||
local text = create "TextLabel" {
|
local text = create "TextLabel" {
|
||||||
{
|
{
|
||||||
|
|
@ -819,6 +826,20 @@ TEST("create()", wrap_root(function()
|
||||||
CHECK(frame:FindFirstChild "G")
|
CHECK(frame:FindFirstChild "G")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "set false as child"
|
||||||
|
create "Frame" {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
create "Frame" {
|
||||||
|
function() return false end
|
||||||
|
}
|
||||||
|
|
||||||
|
create "Frame" {
|
||||||
|
function() return { false } end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "binding properties to source"
|
do CASE "binding properties to source"
|
||||||
local name = source("Hi")
|
local name = source("Hi")
|
||||||
local text = source("Bye")
|
local text = source("Bye")
|
||||||
|
|
@ -1073,20 +1094,147 @@ TEST("show()", wrap_root(function()
|
||||||
local show = vide.show
|
local show = vide.show
|
||||||
local root = vide.root
|
local root = vide.root
|
||||||
|
|
||||||
do CASE "main"
|
do CASE "show component"
|
||||||
-- uses switch() internally, more extensive testing of scoping not needed
|
local input = source(true)
|
||||||
local value = source("truey" :: unknown)
|
local function one() return 1 end
|
||||||
|
|
||||||
|
local output = show(input, one)
|
||||||
|
|
||||||
|
CHECK(output() == 1)
|
||||||
|
input(false)
|
||||||
|
CHECK(output() == nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "fallback component"
|
||||||
|
local input = source(true)
|
||||||
local function one() return 1 end
|
local function one() return 1 end
|
||||||
local function two() return 2 end
|
local function two() return 2 end
|
||||||
|
|
||||||
local output = show(value, one, two)
|
local output = show(input, one, two)
|
||||||
|
|
||||||
CHECK(output() == 1)
|
CHECK(output() == 1)
|
||||||
value(nil)
|
input(false)
|
||||||
CHECK(output() == 2)
|
CHECK(output() == 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "alt"
|
do CASE "updating truth to truthy does not rerun"
|
||||||
|
local input = source(1)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
local function component()
|
||||||
|
count += 1
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = show(input, component)
|
||||||
|
|
||||||
|
CHECK(count == 1)
|
||||||
|
CHECK(output() == 1)
|
||||||
|
input(2)
|
||||||
|
CHECK(count == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "updating source passed to component"
|
||||||
|
local input = source(1 :: number?)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
show(input :: () -> number?, function(value: () -> number)
|
||||||
|
vide.cleanup(function() print "destroyed" end)
|
||||||
|
effect(function()
|
||||||
|
local v = value()
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
CHECK(v == count)
|
||||||
|
if v ~= count then error(count) end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
input(2)
|
||||||
|
CHECK(count == 2)
|
||||||
|
input(3)
|
||||||
|
CHECK(count == 3)
|
||||||
|
input(nil)
|
||||||
|
CHECK(count == 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "special strict case"
|
||||||
|
type Weapon = {
|
||||||
|
id: string,
|
||||||
|
enchant: string?
|
||||||
|
}
|
||||||
|
|
||||||
|
vide.strict = true
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
local branch = 0
|
||||||
|
|
||||||
|
local weapon = source(nil :: Weapon?)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
weapon()
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
weapon()
|
||||||
|
end)
|
||||||
|
|
||||||
|
show(weapon, function(weapon: () -> Weapon)
|
||||||
|
local enchant = function() return weapon().enchant end
|
||||||
|
|
||||||
|
show(enchant, function(enchant: () -> string)
|
||||||
|
effect(function()
|
||||||
|
local e = enchant()
|
||||||
|
count += 1
|
||||||
|
CHECK(e ~= nil)
|
||||||
|
if branch == 1 then
|
||||||
|
CHECK(e == "fire")
|
||||||
|
elseif branch == 2 then
|
||||||
|
CHECK(e == "poison")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
weapon()
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
weapon()
|
||||||
|
end)
|
||||||
|
|
||||||
|
branch = 1
|
||||||
|
weapon { id = "1", enchant = "fire" }
|
||||||
|
CHECK(count == 8)
|
||||||
|
|
||||||
|
branch = 2
|
||||||
|
weapon { id = "1", enchant = "poison" }
|
||||||
|
CHECK(count == 10)
|
||||||
|
|
||||||
|
weapon { id = "1", enchant = nil }
|
||||||
|
CHECK(count == 10)
|
||||||
|
|
||||||
|
branch = 1
|
||||||
|
weapon { id = "1", enchant = "fire" }
|
||||||
|
CHECK(count == 14)
|
||||||
|
|
||||||
|
weapon(nil)
|
||||||
|
|
||||||
|
branch = 2
|
||||||
|
weapon { id = "1", enchant = "poison" }
|
||||||
|
CHECK(count == 22)
|
||||||
|
|
||||||
|
vide.strict = false
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "alt" -- todo: move test
|
||||||
local visible = vide.source(true)
|
local visible = vide.source(true)
|
||||||
local count = vide.source(0)
|
local count = vide.source(0)
|
||||||
|
|
||||||
|
|
@ -1109,7 +1257,6 @@ TEST("show()", wrap_root(function()
|
||||||
destroyed += 1
|
destroyed += 1
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -2722,7 +2869,6 @@ TEST("strict", wrap_root(function()
|
||||||
vide.cleanup(function() count_2 += 1 end)
|
vide.cleanup(function() count_2 += 1 end)
|
||||||
return {}
|
return {}
|
||||||
end)
|
end)
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -2749,7 +2895,6 @@ TEST("strict", wrap_root(function()
|
||||||
vide.cleanup(function() count_2 += 1 end)
|
vide.cleanup(function() count_2 += 1 end)
|
||||||
return {}
|
return {}
|
||||||
end)
|
end)
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue