From bf1b1978d9591cc4eac4531416ad720d13d27e4a Mon Sep 17 00:00:00 2001 From: Aaron Smith <83140718+centau@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:24:50 +0100 Subject: [PATCH] --- docs/tut/crash-course.md | 50 ++++----- src/bind.lua | 13 ++- src/init.lua | 9 +- src/{wrap.lua => source.lua} | 4 +- src/spring.lua | 152 +++++++++++++-------------- test/testkit.luau | 18 +++- test/tests.luau | 195 ++++++++++++++++------------------- todo.md | 64 +++++++++++- 8 files changed, 279 insertions(+), 226 deletions(-) rename src/{wrap.lua => source.lua} (91%) diff --git a/docs/tut/crash-course.md b/docs/tut/crash-course.md index 95551aa..b148bc4 100644 --- a/docs/tut/crash-course.md +++ b/docs/tut/crash-course.md @@ -1,10 +1,9 @@ # 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. - -- Note that this tutorial assumes that you are familiar with Luau and the Roblox UI system. +Vide is largely inspired by Solid.
@@ -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. -Instances are created using [`vide.create`](../api/creation#create). +Instances are created using [`vide.create()`](../api/creation#create). ```lua -local vide = require(...) +local vide = require(vide) local create = vide.create ``` ```lua -local frame = create("Frame") { +local frame = create "Frame" { Name = "Background", 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. 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. ```lua -local function Counter(args) +local function Counter(props: { + Layout: Layout +}) -- create internal state unique to each component instance - local count = wrap(0) + local count = source(0) - return create("TextButton") { - Name = "Counter", - - Text = "Count: " .. count - - [Event.Activated] = function() - count.value += 1 + return create "TextButton" { + Text = function() + return "Count: " .. count end - [Layout] = args[Layout] + Activated = function() + count(count() + 1) + end, + + Layout = props.Layout } end create "ScreenGui" { Parent = game.StarterGui, - [Children] = { - Counter { - [Layout] = { - AnchorPoint = Vector2.new(0.5, 0), - Position = UDim2.fromScale(0.5, 0), - Size = UDim2.fromScale(0.3, 0.1) - } + Counter { + Layout = { + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.fromScale(0.5, 0), + Size = UDim2.fromScale(0.3, 0.1) } } } diff --git a/src/bind.lua b/src/bind.lua index c6f840d..705985a 100644 --- a/src/bind.lua +++ b/src/bind.lua @@ -47,7 +47,7 @@ local function traceback() -- ensures trace begins outside of any vide library f return debug.traceback("", s) end -function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) -> ()) +function setup(instance: Instance, setter: (Instance) -> ()) if flags.strict then local fn = setter local trace = traceback() @@ -70,7 +70,8 @@ function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) -> weak[key] = instance 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 hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented end @@ -79,8 +80,10 @@ function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) -> instance:GetPropertyChangedSignal("Parent"):Connect(ref) end +-- todo: move `fn` as arg? + 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() end) end @@ -89,7 +92,7 @@ local function bind_parent(instance: Instance, fn: () -> Instance?) instance.Destroying:Connect(function() instance= nil :: any -- allow gc when destroyed end) - setup(instance, fn, function(instance) + setup(instance, function(instance) local _ = instance -- state will strongly reference instance when parent is bound instance.Parent = fn() 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 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 if newChildren and type(newChildren) ~= "table" then throw(`Cannot parent instance of type { type(newChildren) } `) diff --git a/src/init.lua b/src/init.lua index eddf429..18d9958 100644 --- a/src/init.lua +++ b/src/init.lua @@ -6,7 +6,7 @@ if not game then script = (require :: any) "test/wrap-require" end local create = require(script.create) -local wrap = require(script.wrap) +local source = require(script.source) local watch = require(script.watch) local derive = require(script.derive) local map = require(script.map) @@ -14,18 +14,17 @@ local map = require(script.map) -- local Changed = require(script.Change) -- local Created = require(script.Created) -local spring, updateSprings = require(script.spring)() +local spring, update_springs = require(script.spring)() local flags = require(script.flags) type Map = { [K]: V } -type Unwrapper = (T) -> T type Setter = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, force: true?) -> T ) local vide = { -- core create = create, - wrap = wrap, + source = source, derive = derive, map = map, watch = watch, @@ -45,7 +44,7 @@ local vide = { -- test step = function(dt: number) - updateSprings(dt) + update_springs(dt) end } diff --git a/src/wrap.lua b/src/source.lua similarity index 91% rename from src/wrap.lua rename to src/source.lua index b822720..e47cd9e 100644 --- a/src/wrap.lua +++ b/src/source.lua @@ -13,7 +13,7 @@ local set = graph.set type State = (() -> T) & ((T) -> T) -local function wrap(value: T | () -> T): State +local function source(value: T | () -> T): State if type(value) == "function" then value = value() end local node = create(value :: T) @@ -29,4 +29,4 @@ local function wrap(value: T | () -> T): State end end -return wrap +return source diff --git a/src/spring.lua b/src/spring.lua index 5ef50c5..8495897 100644 --- a/src/spring.lua +++ b/src/spring.lua @@ -29,18 +29,20 @@ local create = graph.create local get = graph.get local set = graph.set +type Node = graph.Node + type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 type SpringData = { - Alpha: number; - Duration: number; - Period: number; - Damping: number; - Velocity: number; - InitialVelocity: number; - Initial: T; - Target: T; - Input: State; + alpha: number, + duration: number, + period: number, + damping_ratio: number, + velocity: number, + initial_velocity: number, + initial_position: T, + target_position: T, + target: () -> T } type Lerp = (initial: T, target: T, alpha: number) -> T @@ -88,78 +90,76 @@ local lerpable: { [string]: Lerp } = { end :: Lerp, } -local activeSprings: { [State]: SpringData } = {} -setmetatable(activeSprings, { __mode = "ks" }) +local springs: { [SpringData]: Node } = {} +setmetatable(springs, { __mode = "vs" }) -local function spring(input: MaybeState, period: number, damping: number?): State - local initial = unwrap(input) :: T - local output = create(initial) +local function spring(target: () -> T, period: number, damping_ratio: number?): () -> T + local initial_position = target() + local node = create(initial_position) - local springData: SpringData = { - Alpha = 0, - Duration = 0, - Period = period, - Damping = damping or 1, - Velocity = 0, - InitialVelocity = 0, - Initial = initial, - Target = initial, - Input = input :: any, - LastInput = initial, - LastOutput = initial + local data: SpringData = { + alpha = 0, + duration = 0, + period = period, + damping_ratio = damping_ratio or 1, + velocity = 0, + initial_velocity = 0, + initial_position = initial_position, + target_position = initial_position, + target = target } - activeSprings[output] = springData + springs[data] = node - return output -end - -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 = 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) + return function() + return get(node) 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 = 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 diff --git a/test/testkit.luau b/test/testkit.luau index e7ba3c9..d244d7f 100644 --- a/test/testkit.luau +++ b/test/testkit.luau @@ -319,9 +319,10 @@ end local function print2(v: unknown) type Buffer = { n: number, [number]: string } + type Cyclic = { [{}]: true } -- 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 indent = table.concat(table.create(stack, TAB)) @@ -339,10 +340,18 @@ local function print2(v: unknown) local n = str.n str[n + 1] = "{}" str.n = n + 1 - else + else -- is table local tabbed_indent = indent .. TAB str.n += 1 + + if cyclic[value] then + str[str.n] = color.gray "*cyclic reference*" + return + else + cyclic[value] = true + end + str[str.n] = "{\n" local i, v = next(value, nil) @@ -363,7 +372,7 @@ local function print2(v: unknown) str[n + 1] = " = " str.n = n + 1 - tos(v, stack + 1, str) + tos(v, stack + 1, str, cyclic) i, v = next(value, i) @@ -380,7 +389,8 @@ local function print2(v: unknown) end local str = { n = 0 } - tos(v, 0, str) + local cyclic = {} + tos(v, 0, str, cyclic) print(table.concat(str)) end diff --git a/test/tests.luau b/test/tests.luau index 7310f82..926d4c0 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,12 +1,12 @@ ----------------------------------------------------------------------------------------------------------------------- --- unit.lua ----------------------------------------------------------------------------------------------------------------------- +local testkit = require("test/testkit") +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 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" @@ -187,23 +187,23 @@ TEST("graph", function() end end) -TEST("wrap()", function() - local wrap = vide.wrap +TEST("source()", function() + local source = vide.source local watch = vide.watch - do CASE "Wrap value" - local state = wrap(1) + do CASE "source value" + local state = source(1) CHECK(state() == 1) end do CASE "Setter" - local state = wrap(1) + local state = source(1) state(2) -- set directly CHECK(state() == 2) end do CASE "Does not update if same value" - local state = wrap(1) + local state = source(1) local updates = -1 watch(function() @@ -218,11 +218,11 @@ TEST("wrap()", function() end) TEST("derive()", function() - local wrap = vide.wrap + local source = vide.source local derive = vide.derive do CASE "Derive new value on state change" - local state = wrap(1) + local state = source(1) local derived = derive(function() return tostring(state()) @@ -235,7 +235,7 @@ TEST("derive()", function() do CASE "Derive from updated" do - local a = wrap(1) + local a = source(1) local b = derive(function() return a() + 1 @@ -253,7 +253,7 @@ TEST("derive()", function() --[[ do CASE "Cleanup" - local count, set = wrap(1) + local count, set = source(1) local derived = derive(function(from) return { Value = from(count), Destroyed = false } @@ -271,7 +271,7 @@ TEST("derive()", function() do CASE "Garbage collection" do -- check that `b` does not allow gc of `a` - local a = wrap(1) + local a = source(1) local _b = derive(function() return a() @@ -286,7 +286,7 @@ TEST("derive()", function() end do -- check that `a` allows gc of `b` - local a = wrap(1) + local a = source(1) local b = derive(function() return a() @@ -303,7 +303,7 @@ TEST("derive()", function() do CASE "Garbage collection 2" -- creats a chain `a -> b -> c` where `a` is the root local function setup() - local a = wrap(1) + local a = source(1) local b = derive(function() return a() @@ -350,12 +350,12 @@ TEST("derive()", function() end) TEST("watch()", function() - local wrap = vide.wrap + local source = vide.source local watch = vide.watch do CASE "Capture states" - local a = wrap(1) - local b = wrap(1) + local a = source(1) + local b = source(1) local runcount = 0 watch(function() @@ -372,8 +372,8 @@ TEST("watch()", function() end do CASE "Stop watch" - local a = wrap(1) - local b = wrap(1) + local a = source(1) + local b = source(1) local runcount = 0 local unwatch = watch(function() @@ -388,7 +388,7 @@ TEST("watch()", function() end do CASE "Side-effect cleanup" - local state = wrap(1) + local state = source(1) local effect_runcount = 0 local cleanup_runcount = 0 @@ -417,7 +417,7 @@ TEST("watch()", function() end do -- state prevents gc of watcher - local state = wrap(1) + local state = source(1) local wref @@ -433,7 +433,7 @@ TEST("watch()", function() end do -- watcher can gc if stopped - local state = wrap(1) + local state = source(1) local effect = factory(state) local unwatch = watch(effect) @@ -453,7 +453,7 @@ TEST("watch()", function() local wref do - local state = wrap(1) + local state = source(1) local effect = factory(state) watch(effect) wref = weak { state } @@ -480,7 +480,7 @@ end) TEST("create()", function() local create = vide.create - local wrap = vide.wrap + local source = vide.source do CASE "Apply default properties" local defaults = require("src/defaults") @@ -528,8 +528,8 @@ TEST("create()", function() end do CASE "Binding properties to state" - local name = wrap("Hi") - local text = wrap("Bye") + local name = source("Hi") + local text = source("Bye") local label = create "TextLabel" { Name = name, @@ -548,7 +548,7 @@ TEST("create()", function() do CASE "Binding garbage collection" do -- instance should gc despite property bound to state - local state = wrap("Hi") + local state = source("Hi") local wref = weak { create "TextLabel" { @@ -561,7 +561,7 @@ TEST("create()", function() end do -- instance should NOT gc despite property bound to state when parented - local state = wrap("Hi") + local state = source("Hi") local parent = create "Frame" {} @@ -588,18 +588,20 @@ TEST("create()", function() end do -- state should not gc once exits scope while instance still exists - local _label + local label local wref do - local state = wrap("Hi") - wref = weak { state } - _label = create "TextLabel" { + local state = source("Hi") + label = create "TextLabel" { Name = state, } + wref = weak { state :: any, label } + end gc() + CHECK(wref[2]) CHECK(wref[1]) end @@ -607,7 +609,7 @@ TEST("create()", function() local wref do - local text = wrap("Hi") + local text = source("Hi") local box = create "TextLabel" { Text = text, @@ -624,7 +626,7 @@ TEST("create()", function() --[[ do -- binding should gc despite state still existing after instance is gc - local state = wrap("Hi") + local state = source("Hi") do local instance = create "TextLabel" { @@ -650,7 +652,7 @@ TEST("create()", function() end do CASE "Bind same state to multiple instance properties" - local state = wrap "1" + local state = source "1" local text = create "TextBox" { Name = state, @@ -666,7 +668,7 @@ TEST("create()", function() end do CASE "Bind children" - local state = wrap({} :: {}?) + local state = source({} :: {}?) local a, b, c = create "TextLabel" { Name = "A" }, @@ -699,7 +701,7 @@ TEST("create()", function() 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 local parent = create "Frame" { Name = "Parent" } - local state = wrap(parent :: Frame?) + local state = source(parent :: Frame?) do wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame? @@ -721,35 +723,38 @@ TEST("create()", function() ]] do CASE "GC test" - local data = setmetatable({}, {}) - local proxy = setmetatable({}, { __mode = "v" }) + local wref - local ref = setmetatable({}, { __mode = "v" }) + do + local data = setmetatable({}, {}) + local proxy = setmetatable({}, { __mode = "v" }) - --proxy.data = data --? (this line should not affect outcome) - -- `data` strongly references `proxy` - data.connection = proxy + local ref = setmetatable({}, { __mode = "v" }) - -- `ref` strongly references `data` - ref[data] = proxy + --proxy.data = data --? (this line should not affect outcome) + -- `data` strongly references `proxy` + data.connection = proxy - -- although `ref` is weak to values, `data` keeps `proxy` alive - -- this forms a sort of cyclic reference that the luau gc is unable to detect + -- `ref` strongly references `data` + ref[data] = proxy - wref.data = data - wref.proxy = proxy - data = nil :: any - proxy = nil :: any + -- although `ref` is weak to values, `data` keeps `proxy` alive + -- this forms a sort of cyclic reference that the luau gc is unable to detect + + wref = { data = data, proxy = proxy } + end gc() CHECK(wref.data and wref.proxy) end + + end) TEST("map()", function() - local wrap = vide.wrap - local unwrap = vide.unwrap + local source = vide.source + local unsource = vide.unsource local map = vide.map do CASE "Use integer" @@ -777,7 +782,7 @@ TEST("map()", function() end do CASE "Use state" - local state = wrap { 1, 2, 3 } + local state = source { 1, 2, 3 } local derived = map(state, function(_, v) return tostring(v) @@ -791,7 +796,7 @@ TEST("map()", function() end do CASE "Cache result" - local state, set = wrap { 1, 2, 3 } + local state, set = source { 1, 2, 3 } local runcount = table.create(3, 0) @@ -815,7 +820,7 @@ TEST("map()", function() end 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) return tostring(v) @@ -835,7 +840,7 @@ TEST("map()", function() local Children = vide.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) return create "TextLabel" { @@ -866,7 +871,7 @@ TEST("map()", function() end 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) return { Value = v, Destroyed = false } end, function(v) @@ -888,7 +893,7 @@ TEST("map()", function() do CASE "Garbage collection" do -- check that `derived` does not allow gc of `state` - local state = wrap {} + local state = source {} local derived = map(state, function(i, v) return v @@ -902,7 +907,7 @@ TEST("map()", function() end do -- check that `state` allows gc of `derived` - local state = wrap {} + local state = source {} local derived = map(state, function(i, v) return i, v @@ -917,33 +922,15 @@ TEST("map()", function() 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() - local wrap = vide.wrap - local unwrap = vide.unwrap + local source = vide.source local spring = vide.spring do CASE "Update state (on next hearbeat resumption cycle)" - local number, set = wrap(10) - local springed = spring(number, 1, 1) + local value = source(10) + local springed = spring(value, 1, 1) - set(20) + value(20) CHECK(springed() == 10) vide.step(1/60) CHECK(springed() ~= 10) @@ -952,7 +939,7 @@ TEST("spring()", function() do CASE "Garbage collection" do -- `spring` should not allow gc of `state` - local state = wrap(10) + local state = source(10) local _springed = spring(state, 1, 1) wref.state, state = state, nil :: any @@ -962,9 +949,9 @@ TEST("spring()", function() end - do -- `number` should allow gc of `spring` - local number = wrap(10) - local springed = spring(number, 1, 1) :: State? + do -- `value` should allow gc of `spring` + local value = source(10) + local springed = spring(value, 1, 1) :: State? wref.springed, springed = springed, nil @@ -977,7 +964,7 @@ TEST("spring()", function() local create = vide.create do CASE "Garbage collection (binded)" - local number = wrap(10) + local number = source(10) local springed = spring(number, 1, 1) :: State? local _label = create "TextLabel" { @@ -994,7 +981,7 @@ end) TEST("Event", function() local create = vide.create local Event = vide.Event - local wrap = vide.wrap + local source = vide.source do CASE "Connect event" local connected = false @@ -1018,7 +1005,7 @@ TEST("Event", function() do CASE "Bind connection to state" local countA = 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 } @@ -1049,7 +1036,7 @@ end) TEST("Changed", function() local create = vide.create local Changed = vide.Changed - local wrap = vide.wrap + local source = vide.source do CASE "Connects event" local connected = false @@ -1067,7 +1054,7 @@ TEST("Changed", function() do CASE "Bind connection to state" local countA = 0 local countB = 0 - local listener, set = wrap(function() countA += 1 end :: () -> ()?) + local listener, set = source(function() countA += 1 end :: () -> ()?) local label = create "TextLabel" { @@ -1118,13 +1105,13 @@ end)]] TEST("strict", function() vide.strict = true - local wrap = vide.wrap - local unwrap = vide.unwrap + local source = vide.source + local unsource = vide.unsource local derive = vide.derive local watch = vide.watch do CASE "Error on derived callback yield" - local state = wrap(1) + local state = source(1) local ok = pcall(function() local _derived = derive(function(from) @@ -1137,7 +1124,7 @@ TEST("strict", function() end do CASE "Error on watcher callback yield" - local state = wrap(1) + local state = source(1) local ok = pcall(function() local _derived = watch(function(from) @@ -1150,7 +1137,7 @@ TEST("strict", function() end do CASE "Run derived callback twice" - local state, set = wrap(1) + local state, set = source(1) local runcount = 0 local derived = derive(function(from) @@ -1165,7 +1152,7 @@ TEST("strict", function() end do CASE "Run watcher callback twice" - local state, set = wrap(1) + local state, set = source(1) local runcount = 0 watch(function(from) @@ -1192,7 +1179,7 @@ TEST("strict", function() end do CASE "Does not allow same table set" - local _, set = wrap() + local _, set = source() local t = {} diff --git a/todo.md b/todo.md index 3c6a999..29e2de1 100644 --- a/todo.md +++ b/todo.md @@ -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