This commit is contained in:
Aaron Smith 2023-07-31 16:24:50 +01:00
parent 96e577a626
commit bf1b1978d9
8 changed files with 279 additions and 226 deletions

View file

@ -1,10 +1,9 @@
# Vide Crash Course # Vide Crash Course
Hello! This is a brief tutorial designed to give you a quick runthrough of the usage of Vide. This is a brief tutorial designed to give you a quick run through the usage of
Vide.
Vide is inspired by the popular libraries Vue and Fusion. Vide is largely inspired by Solid.
- Note that this tutorial assumes that you are familiar with Luau and the Roblox UI system.
<br> <br>
@ -12,21 +11,22 @@ Vide is inspired by the popular libraries Vue and Fusion.
In Vide, it is intended to create all UI instances through code. In Vide, it is intended to create all UI instances through code.
Instances are created using [`vide.create`](../api/creation#create). Instances are created using [`vide.create()`](../api/creation#create).
```lua ```lua
local vide = require(...) local vide = require(vide)
local create = vide.create local create = vide.create
``` ```
```lua ```lua
local frame = create("Frame") { local frame = create "Frame" {
Name = "Background", Name = "Background",
Position = UDim2.fromScale(0.5, 0.5) Position = UDim2.fromScale(0.5, 0.5)
} }
``` ```
The function returns a constructor for a given class which then takes a table of properties to assign to create a new instance for that class. `create()` returns a constructor for a given class which then takes a table of
properties to assign when creating a new instance for that class.
Sometimes you want to do more than setting properties, such as setting children or connecting to events. Sometimes you want to do more than setting properties, such as setting children or connecting to events.
Vide uses special keys called *symbols* which provide unique functionality like the above mentioned. Vide uses special keys called *symbols* which provide unique functionality like the above mentioned.
@ -250,33 +250,33 @@ Often, you need components that maintain their own internal state, such as a tog
Below you can see how a simple counter component can be implemented. Below you can see how a simple counter component can be implemented.
```lua ```lua
local function Counter(args) local function Counter(props: {
Layout: Layout
})
-- create internal state unique to each component instance -- create internal state unique to each component instance
local count = wrap(0) local count = source(0)
return create("TextButton") { return create "TextButton" {
Name = "Counter", Text = function()
return "Count: " .. count
Text = "Count: " .. count
[Event.Activated] = function()
count.value += 1
end end
[Layout] = args[Layout] Activated = function()
count(count() + 1)
end,
Layout = props.Layout
} }
end end
create "ScreenGui" { create "ScreenGui" {
Parent = game.StarterGui, Parent = game.StarterGui,
[Children] = { Counter {
Counter { Layout = {
[Layout] = { AnchorPoint = Vector2.new(0.5, 0),
AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.fromScale(0.5, 0),
Position = UDim2.fromScale(0.5, 0), Size = UDim2.fromScale(0.3, 0.1)
Size = UDim2.fromScale(0.3, 0.1)
}
} }
} }
} }

View file

@ -47,7 +47,7 @@ local function traceback() -- ensures trace begins outside of any vide library f
return debug.traceback("", s) return debug.traceback("", s)
end end
function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) -> ()) function setup(instance: Instance, setter: (Instance) -> ())
if flags.strict then if flags.strict then
local fn = setter local fn = setter
local trace = traceback() local trace = traceback()
@ -70,7 +70,8 @@ function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) ->
weak[key] = instance weak[key] = instance
local function ref() local function ref()
local _ = nodes -- prevent gc of state while instance exists --todo:local _ = nodes -- prevent gc of state while instance exists
local _ = setter
local instance = weak[key] :: Instance local instance = weak[key] :: Instance
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
end end
@ -79,8 +80,10 @@ function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) ->
instance:GetPropertyChangedSignal("Parent"):Connect(ref) instance:GetPropertyChangedSignal("Parent"):Connect(ref)
end end
-- todo: move `fn` as arg?
local function bind_property(instance: Instance, property: string, fn: () -> unknown) local function bind_property(instance: Instance, property: string, fn: () -> unknown)
setup(instance, fn, function(instance_weak: any) setup(instance, function(instance_weak: any)
instance_weak[property] = fn() instance_weak[property] = fn()
end) end)
end end
@ -89,7 +92,7 @@ local function bind_parent(instance: Instance, fn: () -> Instance?)
instance.Destroying:Connect(function() instance.Destroying:Connect(function()
instance= nil :: any -- allow gc when destroyed instance= nil :: any -- allow gc when destroyed
end) end)
setup(instance, fn, function(instance) setup(instance, function(instance)
local _ = instance -- state will strongly reference instance when parent is bound local _ = instance -- state will strongly reference instance when parent is bound
instance.Parent = fn() instance.Parent = fn()
end) end)
@ -99,7 +102,7 @@ local function bind_children(parent: Instance, fn: () -> { Instance })
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update
setup(parent, fn, function(parent_weak) setup(parent, function(parent_weak)
local newChildren = fn() -- all (and only) children that should be parented after this update local newChildren = fn() -- all (and only) children that should be parented after this update
if newChildren and type(newChildren) ~= "table" then if newChildren and type(newChildren) ~= "table" then
throw(`Cannot parent instance of type { type(newChildren) } `) throw(`Cannot parent instance of type { type(newChildren) } `)

View file

@ -6,7 +6,7 @@
if not game then script = (require :: any) "test/wrap-require" end if not game then script = (require :: any) "test/wrap-require" end
local create = require(script.create) local create = require(script.create)
local wrap = require(script.wrap) local source = require(script.source)
local watch = require(script.watch) local watch = require(script.watch)
local derive = require(script.derive) local derive = require(script.derive)
local map = require(script.map) local map = require(script.map)
@ -14,18 +14,17 @@ local map = require(script.map)
-- local Changed = require(script.Change) -- local Changed = require(script.Change)
-- local Created = require(script.Created) -- local Created = require(script.Created)
local spring, updateSprings = require(script.spring)() local spring, update_springs = require(script.spring)()
local flags = require(script.flags) local flags = require(script.flags)
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
type Unwrapper = <T>(T) -> T
type Setter<T> = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, force: true?) -> T ) type Setter<T> = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, force: true?) -> T )
local vide = { local vide = {
-- core -- core
create = create, create = create,
wrap = wrap, source = source,
derive = derive, derive = derive,
map = map, map = map,
watch = watch, watch = watch,
@ -45,7 +44,7 @@ local vide = {
-- test -- test
step = function(dt: number) step = function(dt: number)
updateSprings(dt) update_springs(dt)
end end
} }

View file

@ -13,7 +13,7 @@ local set = graph.set
type State<T> = (() -> T) & ((T) -> T) type State<T> = (() -> T) & ((T) -> T)
local function wrap<T>(value: T | () -> T): State<T> local function source<T>(value: T | () -> T): State<T>
if type(value) == "function" then value = value() end if type(value) == "function" then value = value() end
local node = create(value :: T) local node = create(value :: T)
@ -29,4 +29,4 @@ local function wrap<T>(value: T | () -> T): State<T>
end end
end end
return wrap return source

View file

@ -29,18 +29,20 @@ local create = graph.create
local get = graph.get local get = graph.get
local set = graph.set local set = graph.set
type Node<T> = graph.Node<T>
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
type SpringData<T> = { type SpringData<T> = {
Alpha: number; alpha: number,
Duration: number; duration: number,
Period: number; period: number,
Damping: number; damping_ratio: number,
Velocity: number; velocity: number,
InitialVelocity: number; initial_velocity: number,
Initial: T; initial_position: T,
Target: T; target_position: T,
Input: State<T>; target: () -> T
} }
type Lerp<T> = (initial: T, target: T, alpha: number) -> T type Lerp<T> = (initial: T, target: T, alpha: number) -> T
@ -88,78 +90,76 @@ local lerpable: { [string]: Lerp<any> } = {
end :: Lerp<Vector3>, end :: Lerp<Vector3>,
} }
local activeSprings: { [State<any>]: SpringData<any> } = {} local springs: { [SpringData<any>]: Node<any> } = {}
setmetatable(activeSprings, { __mode = "ks" }) setmetatable(springs, { __mode = "vs" })
local function spring<T>(input: MaybeState<T>, period: number, damping: number?): State<T> local function spring<T>(target: () -> T, period: number, damping_ratio: number?): () -> T
local initial = unwrap(input) :: T local initial_position = target()
local output = create(initial) local node = create(initial_position)
local springData: SpringData<T> = { local data: SpringData<T> = {
Alpha = 0, alpha = 0,
Duration = 0, duration = 0,
Period = period, period = period,
Damping = damping or 1, damping_ratio = damping_ratio or 1,
Velocity = 0, velocity = 0,
InitialVelocity = 0, initial_velocity = 0,
Initial = initial, initial_position = initial_position,
Target = initial, target_position = initial_position,
Input = input :: any, target = target
LastInput = initial,
LastOutput = initial
} }
activeSprings[output] = springData springs[data] = node
return output return function()
end return get(node)
local function updateSprings(dt: number)
for node, data in next, activeSprings do
local currentTarget = get(data.Input)
if currentTarget ~= data.Target then
data.Target = currentTarget
data.Initial = get(node)
data.Target = get(data.Input)
data.Alpha = 0
data.Duration = 0
data.InitialVelocity = data.Velocity
end
local initial: Animatable = data.Initial
local target: Animatable = data.Target
local targetType: string = typeof(target)
if targetType ~= typeof(initial) then
activeSprings[node] = nil
warn(string.format(
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
typeof(initial),
targetType
))
throw(`Cannot tween type { typeof(initial) } and { targetType }`)
continue
end
local lerp: Lerp<Animatable> = lerpable[targetType]
if lerp == nil then
activeSprings[node] = nil
throw(`Cannot animate type { targetType }`)
continue
end
local newTime = data.Duration + dt
local newAlpha = solve(data.Period, data.Damping, data.InitialVelocity, newTime)
data.Velocity = -(newAlpha - data.Alpha)/dt
data.Alpha = newAlpha
data.Duration = newTime
local value = lerp(initial, target, newAlpha)
set(node, value)
end end
end end
return function() return spring, updateSprings end local function update_springs(dt: number)
for data, output in next, springs do
if data.target() ~= data.target_position then
data.target = data.target()
data.initial_position = get(output)
data.alpha = 0
data.duration = 0
data.initial_velocity = data.velocity
end
local initial_position: Animatable = data.initial_position
local target_position: Animatable = data.target_position
local target_type: string = typeof(target_position)
if target_type ~= typeof(initial_position) then
springs[data] = nil
warn(string.format(
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
typeof(initial_position),
target_type
))
throw(`Cannot tween type { typeof(initial_position) } and { target_type }`)
continue
end
local lerp: Lerp<Animatable> = lerpable[target_type]
if lerp == nil then
springs[data] = nil
throw(`Cannot animate type { target_type }`)
continue
end
local new_time = data.duration + dt
local new_alpha = solve(data.period, data.damping_ratio, data.initial_velocity, new_time)
data.velocity = -(new_alpha - data.alpha)/dt
data.alpha = new_alpha
data.duration = new_time
local value = lerp(initial_position, target_position, new_alpha)
set(output, value)
end
end
return function() return spring, update_springs end

View file

@ -319,9 +319,10 @@ end
local function print2(v: unknown) local function print2(v: unknown)
type Buffer = { n: number, [number]: string } type Buffer = { n: number, [number]: string }
type Cyclic = { [{}]: true }
-- overkill concatenationless string buffer -- overkill concatenationless string buffer
local function tos(value: any, stack: number, str: Buffer) local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
local TAB = " " local TAB = " "
local indent = table.concat(table.create(stack, TAB)) local indent = table.concat(table.create(stack, TAB))
@ -339,10 +340,18 @@ local function print2(v: unknown)
local n = str.n local n = str.n
str[n + 1] = "{}" str[n + 1] = "{}"
str.n = n + 1 str.n = n + 1
else else -- is table
local tabbed_indent = indent .. TAB local tabbed_indent = indent .. TAB
str.n += 1 str.n += 1
if cyclic[value] then
str[str.n] = color.gray "*cyclic reference*"
return
else
cyclic[value] = true
end
str[str.n] = "{\n" str[str.n] = "{\n"
local i, v = next(value, nil) local i, v = next(value, nil)
@ -363,7 +372,7 @@ local function print2(v: unknown)
str[n + 1] = " = " str[n + 1] = " = "
str.n = n + 1 str.n = n + 1
tos(v, stack + 1, str) tos(v, stack + 1, str, cyclic)
i, v = next(value, i) i, v = next(value, i)
@ -380,7 +389,8 @@ local function print2(v: unknown)
end end
local str = { n = 0 } local str = { n = 0 }
tos(v, 0, str) local cyclic = {}
tos(v, 0, str, cyclic)
print(table.concat(str)) print(table.concat(str))
end end

View file

@ -1,12 +1,12 @@
---------------------------------------------------------------------------------------------------------------------- local testkit = require("test/testkit")
-- unit.lua local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
----------------------------------------------------------------------------------------------------------------------
local TEST, CASE, CHECK, FINISH, SKIP = require("test/testkit").test()
local mock = require "test/mock"
local Signal = require "test/goodsignal" local Signal = require "test/goodsignal"
local Instance, Vector3, Color3, Vector2, UDim2 = mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2 local mock = require "test/mock"
local Instance, Vector3, Color3, Vector2, UDim2 =
mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2
local vide = require "src/init" local vide = require "src/init"
@ -187,23 +187,23 @@ TEST("graph", function()
end end
end) end)
TEST("wrap()", function() TEST("source()", function()
local wrap = vide.wrap local source = vide.source
local watch = vide.watch local watch = vide.watch
do CASE "Wrap value" do CASE "source value"
local state = wrap(1) local state = source(1)
CHECK(state() == 1) CHECK(state() == 1)
end end
do CASE "Setter" do CASE "Setter"
local state = wrap(1) local state = source(1)
state(2) -- set directly state(2) -- set directly
CHECK(state() == 2) CHECK(state() == 2)
end end
do CASE "Does not update if same value" do CASE "Does not update if same value"
local state = wrap(1) local state = source(1)
local updates = -1 local updates = -1
watch(function() watch(function()
@ -218,11 +218,11 @@ TEST("wrap()", function()
end) end)
TEST("derive()", function() TEST("derive()", function()
local wrap = vide.wrap local source = vide.source
local derive = vide.derive local derive = vide.derive
do CASE "Derive new value on state change" do CASE "Derive new value on state change"
local state = wrap(1) local state = source(1)
local derived = derive(function() local derived = derive(function()
return tostring(state()) return tostring(state())
@ -235,7 +235,7 @@ TEST("derive()", function()
do CASE "Derive from updated" do CASE "Derive from updated"
do do
local a = wrap(1) local a = source(1)
local b = derive(function() local b = derive(function()
return a() + 1 return a() + 1
@ -253,7 +253,7 @@ TEST("derive()", function()
--[[ --[[
do CASE "Cleanup" do CASE "Cleanup"
local count, set = wrap(1) local count, set = source(1)
local derived = derive(function(from) local derived = derive(function(from)
return { Value = from(count), Destroyed = false } return { Value = from(count), Destroyed = false }
@ -271,7 +271,7 @@ TEST("derive()", function()
do CASE "Garbage collection" do CASE "Garbage collection"
do -- check that `b` does not allow gc of `a` do -- check that `b` does not allow gc of `a`
local a = wrap(1) local a = source(1)
local _b = derive(function() local _b = derive(function()
return a() return a()
@ -286,7 +286,7 @@ TEST("derive()", function()
end end
do -- check that `a` allows gc of `b` do -- check that `a` allows gc of `b`
local a = wrap(1) local a = source(1)
local b = derive(function() local b = derive(function()
return a() return a()
@ -303,7 +303,7 @@ TEST("derive()", function()
do CASE "Garbage collection 2" do CASE "Garbage collection 2"
-- creats a chain `a -> b -> c` where `a` is the root -- creats a chain `a -> b -> c` where `a` is the root
local function setup() local function setup()
local a = wrap(1) local a = source(1)
local b = derive(function() local b = derive(function()
return a() return a()
@ -350,12 +350,12 @@ TEST("derive()", function()
end) end)
TEST("watch()", function() TEST("watch()", function()
local wrap = vide.wrap local source = vide.source
local watch = vide.watch local watch = vide.watch
do CASE "Capture states" do CASE "Capture states"
local a = wrap(1) local a = source(1)
local b = wrap(1) local b = source(1)
local runcount = 0 local runcount = 0
watch(function() watch(function()
@ -372,8 +372,8 @@ TEST("watch()", function()
end end
do CASE "Stop watch" do CASE "Stop watch"
local a = wrap(1) local a = source(1)
local b = wrap(1) local b = source(1)
local runcount = 0 local runcount = 0
local unwatch = watch(function() local unwatch = watch(function()
@ -388,7 +388,7 @@ TEST("watch()", function()
end end
do CASE "Side-effect cleanup" do CASE "Side-effect cleanup"
local state = wrap(1) local state = source(1)
local effect_runcount = 0 local effect_runcount = 0
local cleanup_runcount = 0 local cleanup_runcount = 0
@ -417,7 +417,7 @@ TEST("watch()", function()
end end
do -- state prevents gc of watcher do -- state prevents gc of watcher
local state = wrap(1) local state = source(1)
local wref local wref
@ -433,7 +433,7 @@ TEST("watch()", function()
end end
do -- watcher can gc if stopped do -- watcher can gc if stopped
local state = wrap(1) local state = source(1)
local effect = factory(state) local effect = factory(state)
local unwatch = watch(effect) local unwatch = watch(effect)
@ -453,7 +453,7 @@ TEST("watch()", function()
local wref local wref
do do
local state = wrap(1) local state = source(1)
local effect = factory(state) local effect = factory(state)
watch(effect) watch(effect)
wref = weak { state } wref = weak { state }
@ -480,7 +480,7 @@ end)
TEST("create()", function() TEST("create()", function()
local create = vide.create local create = vide.create
local wrap = vide.wrap local source = vide.source
do CASE "Apply default properties" do CASE "Apply default properties"
local defaults = require("src/defaults") local defaults = require("src/defaults")
@ -528,8 +528,8 @@ TEST("create()", function()
end end
do CASE "Binding properties to state" do CASE "Binding properties to state"
local name = wrap("Hi") local name = source("Hi")
local text = wrap("Bye") local text = source("Bye")
local label = create "TextLabel" { local label = create "TextLabel" {
Name = name, Name = name,
@ -548,7 +548,7 @@ TEST("create()", function()
do CASE "Binding garbage collection" do CASE "Binding garbage collection"
do -- instance should gc despite property bound to state do -- instance should gc despite property bound to state
local state = wrap("Hi") local state = source("Hi")
local wref = weak { local wref = weak {
create "TextLabel" { create "TextLabel" {
@ -561,7 +561,7 @@ TEST("create()", function()
end end
do -- instance should NOT gc despite property bound to state when parented do -- instance should NOT gc despite property bound to state when parented
local state = wrap("Hi") local state = source("Hi")
local parent = create "Frame" {} local parent = create "Frame" {}
@ -588,18 +588,20 @@ TEST("create()", function()
end end
do -- state should not gc once exits scope while instance still exists do -- state should not gc once exits scope while instance still exists
local _label local label
local wref local wref
do do
local state = wrap("Hi") local state = source("Hi")
wref = weak { state } label = create "TextLabel" {
_label = create "TextLabel" {
Name = state, Name = state,
} }
wref = weak { state :: any, label }
end end
gc() gc()
CHECK(wref[2])
CHECK(wref[1]) CHECK(wref[1])
end end
@ -607,7 +609,7 @@ TEST("create()", function()
local wref local wref
do do
local text = wrap("Hi") local text = source("Hi")
local box = create "TextLabel" { local box = create "TextLabel" {
Text = text, Text = text,
@ -624,7 +626,7 @@ TEST("create()", function()
--[[ --[[
do -- binding should gc despite state still existing after instance is gc do -- binding should gc despite state still existing after instance is gc
local state = wrap("Hi") local state = source("Hi")
do do
local instance = create "TextLabel" { local instance = create "TextLabel" {
@ -650,7 +652,7 @@ TEST("create()", function()
end end
do CASE "Bind same state to multiple instance properties" do CASE "Bind same state to multiple instance properties"
local state = wrap "1" local state = source "1"
local text = create "TextBox" { local text = create "TextBox" {
Name = state, Name = state,
@ -666,7 +668,7 @@ TEST("create()", function()
end end
do CASE "Bind children" do CASE "Bind children"
local state = wrap({} :: {}?) local state = source({} :: {}?)
local a, b, c = local a, b, c =
create "TextLabel" { Name = "A" }, create "TextLabel" { Name = "A" },
@ -699,7 +701,7 @@ TEST("create()", function()
do CASE "Parent set to nil by state does not allow gc" do CASE "Parent set to nil by state does not allow gc"
-- this is technically a bug but we test for this anyways to confirm behavior -- this is technically a bug but we test for this anyways to confirm behavior
local parent = create "Frame" { Name = "Parent" } local parent = create "Frame" { Name = "Parent" }
local state = wrap(parent :: Frame?) local state = source(parent :: Frame?)
do do
wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame? wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame?
@ -721,35 +723,38 @@ TEST("create()", function()
]] ]]
do CASE "GC test" do CASE "GC test"
local data = setmetatable({}, {}) local wref
local proxy = setmetatable({}, { __mode = "v" })
local ref = setmetatable({}, { __mode = "v" }) do
local data = setmetatable({}, {})
local proxy = setmetatable({}, { __mode = "v" })
--proxy.data = data --? (this line should not affect outcome) local ref = setmetatable({}, { __mode = "v" })
-- `data` strongly references `proxy`
data.connection = proxy
-- `ref` strongly references `data` --proxy.data = data --? (this line should not affect outcome)
ref[data] = proxy -- `data` strongly references `proxy`
data.connection = proxy
-- although `ref` is weak to values, `data` keeps `proxy` alive -- `ref` strongly references `data`
-- this forms a sort of cyclic reference that the luau gc is unable to detect ref[data] = proxy
wref.data = data -- although `ref` is weak to values, `data` keeps `proxy` alive
wref.proxy = proxy -- this forms a sort of cyclic reference that the luau gc is unable to detect
data = nil :: any
proxy = nil :: any wref = { data = data, proxy = proxy }
end
gc() gc()
CHECK(wref.data and wref.proxy) CHECK(wref.data and wref.proxy)
end end
end) end)
TEST("map()", function() TEST("map()", function()
local wrap = vide.wrap local source = vide.source
local unwrap = vide.unwrap local unsource = vide.unsource
local map = vide.map local map = vide.map
do CASE "Use integer" do CASE "Use integer"
@ -777,7 +782,7 @@ TEST("map()", function()
end end
do CASE "Use state" do CASE "Use state"
local state = wrap { 1, 2, 3 } local state = source { 1, 2, 3 }
local derived = map(state, function(_, v) local derived = map(state, function(_, v)
return tostring(v) return tostring(v)
@ -791,7 +796,7 @@ TEST("map()", function()
end end
do CASE "Cache result" do CASE "Cache result"
local state, set = wrap { 1, 2, 3 } local state, set = source { 1, 2, 3 }
local runcount = table.create(3, 0) local runcount = table.create(3, 0)
@ -815,7 +820,7 @@ TEST("map()", function()
end end
do CASE "Removal reflected" do CASE "Removal reflected"
local state, set = wrap { 1, 2, 3 } local state, set = source { 1, 2, 3 }
local derived = map(state, function(i, v) local derived = map(state, function(i, v)
return tostring(v) return tostring(v)
@ -835,7 +840,7 @@ TEST("map()", function()
local Children = vide.Children local Children = vide.Children
do CASE "Bind children" do CASE "Bind children"
local state, set = wrap { "A", "B", "C" } local state, set = source { "A", "B", "C" }
local derived = map(state, function(i, v) local derived = map(state, function(i, v)
return create "TextLabel" { return create "TextLabel" {
@ -866,7 +871,7 @@ TEST("map()", function()
end end
do CASE "Use optional destructor" do CASE "Use optional destructor"
local state, set = wrap { 1, 2, 3 } local state, set = source { 1, 2, 3 }
local derived = map(state, function(i, v) local derived = map(state, function(i, v)
return { Value = v, Destroyed = false } return { Value = v, Destroyed = false }
end, function(v) end, function(v)
@ -888,7 +893,7 @@ TEST("map()", function()
do CASE "Garbage collection" do CASE "Garbage collection"
do -- check that `derived` does not allow gc of `state` do -- check that `derived` does not allow gc of `state`
local state = wrap {} local state = source {}
local derived = map(state, function(i, v) local derived = map(state, function(i, v)
return v return v
@ -902,7 +907,7 @@ TEST("map()", function()
end end
do -- check that `state` allows gc of `derived` do -- check that `state` allows gc of `derived`
local state = wrap {} local state = source {}
local derived = map(state, function(i, v) local derived = map(state, function(i, v)
return i, v return i, v
@ -917,33 +922,15 @@ TEST("map()", function()
end end
end) end)
TEST("apply()", function()
local apply = vide.apply
-- uses same application method as `create()` internally, further testing unnecessary
do CASE "Apply properties"
local part = Instance.new("Part") :: Part
apply(part) {
Position = Vector3.new(1, 1, 1),
Color = Color3.new(1, 0, 0)
}
CHECK(part.Position == Vector3.new(1, 1, 1))
CHECK(part.Color == Color3.new(1, 0, 0))
end
end)
TEST("spring()", function() TEST("spring()", function()
local wrap = vide.wrap local source = vide.source
local unwrap = vide.unwrap
local spring = vide.spring local spring = vide.spring
do CASE "Update state (on next hearbeat resumption cycle)" do CASE "Update state (on next hearbeat resumption cycle)"
local number, set = wrap(10) local value = source(10)
local springed = spring(number, 1, 1) local springed = spring(value, 1, 1)
set(20) value(20)
CHECK(springed() == 10) CHECK(springed() == 10)
vide.step(1/60) vide.step(1/60)
CHECK(springed() ~= 10) CHECK(springed() ~= 10)
@ -952,7 +939,7 @@ TEST("spring()", function()
do CASE "Garbage collection" do CASE "Garbage collection"
do -- `spring` should not allow gc of `state` do -- `spring` should not allow gc of `state`
local state = wrap(10) local state = source(10)
local _springed = spring(state, 1, 1) local _springed = spring(state, 1, 1)
wref.state, state = state, nil :: any wref.state, state = state, nil :: any
@ -962,9 +949,9 @@ TEST("spring()", function()
end end
do -- `number` should allow gc of `spring` do -- `value` should allow gc of `spring`
local number = wrap(10) local value = source(10)
local springed = spring(number, 1, 1) :: State? local springed = spring(value, 1, 1) :: State?
wref.springed, springed = springed, nil wref.springed, springed = springed, nil
@ -977,7 +964,7 @@ TEST("spring()", function()
local create = vide.create local create = vide.create
do CASE "Garbage collection (binded)" do CASE "Garbage collection (binded)"
local number = wrap(10) local number = source(10)
local springed = spring(number, 1, 1) :: State? local springed = spring(number, 1, 1) :: State?
local _label = create "TextLabel" { local _label = create "TextLabel" {
@ -994,7 +981,7 @@ end)
TEST("Event", function() TEST("Event", function()
local create = vide.create local create = vide.create
local Event = vide.Event local Event = vide.Event
local wrap = vide.wrap local source = vide.source
do CASE "Connect event" do CASE "Connect event"
local connected = false local connected = false
@ -1018,7 +1005,7 @@ TEST("Event", function()
do CASE "Bind connection to state" do CASE "Bind connection to state"
local countA = 0 local countA = 0
local countB = 0 local countB = 0
local listener, set = wrap(function() countA += 1 end :: () -> ()?) local listener, set = source(function() countA += 1 end :: () -> ()?)
local event = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any } local event = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any }
@ -1049,7 +1036,7 @@ end)
TEST("Changed", function() TEST("Changed", function()
local create = vide.create local create = vide.create
local Changed = vide.Changed local Changed = vide.Changed
local wrap = vide.wrap local source = vide.source
do CASE "Connects event" do CASE "Connects event"
local connected = false local connected = false
@ -1067,7 +1054,7 @@ TEST("Changed", function()
do CASE "Bind connection to state" do CASE "Bind connection to state"
local countA = 0 local countA = 0
local countB = 0 local countB = 0
local listener, set = wrap(function() countA += 1 end :: () -> ()?) local listener, set = source(function() countA += 1 end :: () -> ()?)
local label = create "TextLabel" { local label = create "TextLabel" {
@ -1118,13 +1105,13 @@ end)]]
TEST("strict", function() TEST("strict", function()
vide.strict = true vide.strict = true
local wrap = vide.wrap local source = vide.source
local unwrap = vide.unwrap local unsource = vide.unsource
local derive = vide.derive local derive = vide.derive
local watch = vide.watch local watch = vide.watch
do CASE "Error on derived callback yield" do CASE "Error on derived callback yield"
local state = wrap(1) local state = source(1)
local ok = pcall(function() local ok = pcall(function()
local _derived = derive(function(from) local _derived = derive(function(from)
@ -1137,7 +1124,7 @@ TEST("strict", function()
end end
do CASE "Error on watcher callback yield" do CASE "Error on watcher callback yield"
local state = wrap(1) local state = source(1)
local ok = pcall(function() local ok = pcall(function()
local _derived = watch(function(from) local _derived = watch(function(from)
@ -1150,7 +1137,7 @@ TEST("strict", function()
end end
do CASE "Run derived callback twice" do CASE "Run derived callback twice"
local state, set = wrap(1) local state, set = source(1)
local runcount = 0 local runcount = 0
local derived = derive(function(from) local derived = derive(function(from)
@ -1165,7 +1152,7 @@ TEST("strict", function()
end end
do CASE "Run watcher callback twice" do CASE "Run watcher callback twice"
local state, set = wrap(1) local state, set = source(1)
local runcount = 0 local runcount = 0
watch(function(from) watch(function(from)
@ -1192,7 +1179,7 @@ TEST("strict", function()
end end
do CASE "Does not allow same table set" do CASE "Does not allow same table set"
local _, set = wrap() local _, set = source()
local t = {} local t = {}

64
todo.md
View file

@ -1,6 +1,60 @@
# Todo ```lua
function TextInput(p: {
DefaultText: string,
Output: (string) -> ()
} & Layout)
return create "TextBox" {
Layout = p.Layout,
Children = p.Children
BackgroundText = DefaultText,
[{"Changed"}] = function(self)
p.Output(self.Text)
end
}
end
function Counter()
local count = source(0)
return create "TextButton" {
Text = count
}
end
source
derive
map
spring
```
onCleanup
Index
For
untrack
batch
## version 1
```lua
map(items, function(item, i)
return Item {
Item = item, -- primitive
LayoutOrder = i
}
end)
```
## version 2
```lua
each(items, function(item, i)
return Item {
Item = item, -- state
LayoutOrder = i
}
end)
```
- investigate solid js `for` and batching
- equality checking of derived state updates to prevent redundant updates of derived state
- watcher callbacks when watcher watches a parent and child
- stores