Initial commit

This commit is contained in:
aaron 2023-04-06 02:54:21 +01:00
commit e76234feb5
52 changed files with 5235 additions and 0 deletions

55
src/Change.lua Normal file
View file

@ -0,0 +1,55 @@
------------------------------------------------------------------------------------------
-- vide/Change.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local memoize = require(script.Parent.memoize)
local bind = require(script.Parent.bind)
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
local create = graph.create
local get = graph.get
local link = graph.link
local wrapped = graph.wrapped
local Types = require(script.Parent.Types)
type Listener = (unknown) -> ()
local getChangeSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
return {
priority = 2,
run = function(instance: Instance, listener: MaybeState<Listener>)
local event: RBXScriptSignal<nil> = instance:GetPropertyChangedSignal(name)
if type(listener) == "function" then
event:Connect(function()
listener( (instance :: any)[name] )
end)
elseif wrapped(listener) then
local state = create(nil)
link(listener :: State<Listener>, state, function()
local newListener = get(listener :: State<Listener>)
return newListener and function()
newListener( (instance :: any)[name] )
end
end)
state.updated = true
bind.event(state :: State<any>, instance, event)
else
error("Attempt to connect non-function to changed event", 2)
end
end
}
end)
local Changed = table.freeze(setmetatable({}, {__index = function(_, index: string)
return getChangeSymbol(index)
end})) :: any
return Changed :: { [string]: unknown }

39
src/Children.lua Normal file
View file

@ -0,0 +1,39 @@
------------------------------------------------------------------------------------------
-- vide/Children.lua
------------------------------------------------------------------------------------------
if not game then
script = (require :: any) "test/wrap-require"
typeof = require "test/mock".typeof
end
local graph = require(script.Parent.graph)
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local Types = require(script.Parent.Types)
type Children = Types.Children
local function setChildren(instance: Instance, children: Children)
if typeof(children) == "Instance" then
if children.Parent then throw(`Cannot parent instance { instance.Name }, instance already parented`) end
children.Parent = instance
elseif wrapped(children) then
bind.children(children :: any, instance )
elseif type(children) == "table" then
for _, child: Children in next, children do
setChildren(instance, child)
end
else
throw(`Cannot parent non-instance { typeof(children) }`)
end
end
local Children = {
priority = 1,
run = setChildren
} :: Types.Symbol<Children>
return Children :: unknown

16
src/Created.lua Normal file
View file

@ -0,0 +1,16 @@
------------------------------------------------------------------------------------------
-- vide/Created.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local Types = require(script.Parent.Types)
local Created = {
priority = 3,
run = function(instance: Instance, callback: (Instance) -> ())
callback(instance)
end
} :: Types.Symbol<(Instance) -> ()>
return Created :: unknown

41
src/Event.lua Normal file
View file

@ -0,0 +1,41 @@
------------------------------------------------------------------------------------------
-- vide/Event.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local memoize = require(script.Parent.memoize)
local bind = require(script.Parent.bind)
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
local wrapped = graph.wrapped
local Types = require(script.Parent.Types)
type Listener = (unknown) -> ()
local getEventSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
return {
priority = 2,
run = function(instance: Instance, listener: MaybeState<Listener>)
local event: RBXScriptSignal<...unknown> = (instance :: any)[name]
if type(listener) == "function" then
event:Connect(listener)
elseif wrapped(listener) then
bind.event(listener :: State<Listener>, instance, event)
else
error("Attempt to connect non-function to event", 2)
end
end
}
end)
local Event = table.freeze(setmetatable({}, {__index = function(_, index: string)
return getEventSymbol(index)
end})) :: any
return Event :: { [string]: unknown }

46
src/Layout.lua Normal file
View file

@ -0,0 +1,46 @@
------------------------------------------------------------------------------------------
-- vide/Layout.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local flags = require(script.Parent.flags)
local Types = require(script.Parent.Types)
local layoutProperties = {
Parent = true,
AnchorPoint = true,
LayoutOrder = true,
Position = true,
Rotation = true,
Size = true,
SizeConstraint = true,
Visible = true,
ZIndex = true
}
local function setLayout(instance: Instance, properties: { [string]: unknown })
for property, value in next, properties do
if flags.strict and not layoutProperties[property] then
throw(`{ property } is not a valid layout property`)
end
if wrapped(value) then
bind.property(value :: graph.State<unknown>, instance, property)
else
(instance :: any)[property] = value
end
end
end
local Layout = {
priority = 1,
run = setLayout
} :: Types.Symbol<{ [string]: unknown }>
return Layout :: unknown

19
src/Types.lua Normal file
View file

@ -0,0 +1,19 @@
--------------------------------------------------------------------------------------------------------------
-- vide/Types.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
export type Symbol<T> = {
priority: number,
run: (Instance, T) -> ()
}
export type Children = Instance | State<Children> | { Children }
return {}

15
src/apply.lua Normal file
View file

@ -0,0 +1,15 @@
------------------------------------------------------------------------------------------
-- vide/apply.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local applyProperties = require(script.Parent.applyProperties)
local function apply<T>(instance: T & Instance)
return function(properties: { [any]: unknown }): T
return applyProperties(instance, properties)
end
end
return apply

66
src/applyProperties.lua Normal file
View file

@ -0,0 +1,66 @@
------------------------------------------------------------------------------------------
-- vide/applyProperties.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local Types = require(script.Parent.Types)
local function applyProperty(instance: Instance, property: string, value: unknown)
end
local function applyProperties<T>(instance: T & Instance, properties: { [string|Types.Symbol<unknown> ]: unknown }): T
local parent: unknown = properties.Parent
if parent then properties.Parent = nil end
local eventBuffer: { [(Instance, () -> ()) -> ()]: () -> () } = {} -- connect events after setting properties
local postCreation: (Instance) -> ()? = nil -- buffer for post-creation callback
for property, value in next, properties do
if type(property) == "string" then
if wrapped(value) then
bind.property(value :: State<unknown>, instance, property)
else
(instance :: any)[property] = value
end
elseif type(property) == "table" then
local priority = property.priority
if priority == 1 then
property.run(instance, value)
elseif priority == 2 then
eventBuffer[property.run] = value :: () -> ()
elseif priority == 3 then
assert(not postCreation)
postCreation = value :: () -> ()
else
error("invalid priority")
end
else throw(`Invalid property { tostring(property) }, expected string or symbol`) end
end
for fn, v in next, eventBuffer do
fn(instance, v)
end
if parent then
applyProperty(instance, "Parent", parent)
if wrapped(parent) then
bind.parent(parent :: State<Instance?>, instance)
else
instance.Parent = parent :: Instance
end
end
if postCreation then postCreation(instance) end
return instance
end
return applyProperties

139
src/bind.lua Normal file
View file

@ -0,0 +1,139 @@
--------------------------------------------------------------------------------------------------------------
-- vide/bind.lua
--------------------------------------------------------------------------------------------------------------
local warn = warn -- todo
if not game then
script = (require :: any) "test/wrap-require"
warn = print
end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
local get = graph.get
local setEffect = graph.setEffect
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
local hold: { Instance? } = {}
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
local bindcount = 0
local srcs do
local src1 = debug.info(1, "s")
local srctrunc = string.sub(src1, 1, #src1-4)
srcs = {
src1,
srctrunc .. "applyProperties",
srctrunc .. "create",
srctrunc .. "apply"
}
end
local function traceback() -- ensures trace begins outside of any vide library file
local s = 1
repeat
s += 1
local src = debug.info(s, "s")
until not table.find(srcs, src)
return debug.traceback("", s)
end
function setup(state: State<any>, instance: Instance, updateInstance: (Instance) -> ())
if flags.strict then
local fn = updateInstance
local trace = traceback()
updateInstance = function(instance)
local ok, err: string? = pcall(fn, instance)
if not ok then warn(`error occured updating state binding:\n{err}\nset from:{trace}`) end
end
end
updateInstance(instance)
setEffect(state, updateInstance, instance)
bindcount += 1
local key = bindcount
weak[key] = instance
local function ref()
local _ = state -- prevent gc of state while instance exists
local instance = weak[key] :: Instance
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
end
ref()
instance:GetPropertyChangedSignal("Parent"):Connect(ref)
end
local function bindProperty(state: State<unknown>, instance_STRONG: Instance, property: string)
setup(state, instance_STRONG, function(instance)
(instance :: any)[property] = get(state)
end)
end
local function bindParent(state: State<Instance?>, instance_STRONG)
instance_STRONG.Destroying:Connect(function()
instance_STRONG = nil :: any -- allow gc when destroyed
end)
setup(state, instance_STRONG, function(instance)
local _ = instance_STRONG -- state will strongly reference instance when parent is bound
instance.Parent = get(state)
end)
end
local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance)
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update
setup(state, parent_STRONG, function(parent)
local newChildren = get(state) -- 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) } `)
end
if newChildren then
for _, child in next, newChildren do
newChildrenSet[child] = true -- record child set from this update
if not currentChildrenSet[child] then
child.Parent = parent -- if child wasn't already parented then parent it
else
currentChildrenSet[child] = nil -- remove child from cache if it was already in cache
end
end
end
for child in next, currentChildrenSet do
child.Parent = nil -- unparent all children that weren't in the new children set
end
table.clear(currentChildrenSet) -- clear cache, preserve capacity
currentChildrenSet, newChildrenSet = newChildrenSet, currentChildrenSet
end)
end
local function bindEvent(state: State<() -> ()?>, instance_STRONG: Instance, event: RBXScriptSignal)
local current: RBXScriptConnection? = nil
setup(state, instance_STRONG, function(instance)
if current then
current:Disconnect()
current = nil
end
local listener = get(state)
if listener then
current = event:Connect(listener)
end
end)
end
return {
property = bindProperty,
parent = bindParent,
children = bindChildren,
event = bindEvent
}

78
src/create.lua Normal file
View file

@ -0,0 +1,78 @@
--------------------------------------------------------------------------------------------------------------
-- vide/create.lua
--------------------------------------------------------------------------------------------------------------
if not game then
script = (require :: any) "test/wrap-require"
Instance = require("test/mock").Instance
end
local throw = require(script.Parent.throw)
local defaults = require(script.Parent.defaults)
local applyProperties = require(script.Parent.applyProperties)
local memoize = require(script.Parent.memoize)
local function createInstance(className: string)
local success, instance: Instance = pcall(Instance.new, className :: any)
if success == false then throw(`invalid class name, could not create instance of class { className }`) end
local default: { [string]: unknown }? = defaults[className]
if default then
for i, v in next, default do
(instance :: any)[i] = v
end
end
return function(properties: { [any]: unknown }): Instance
return applyProperties(instance:Clone(), properties)
end
end; createInstance = memoize(createInstance)
local function cloneInstance(instance: Instance)
return function(properties: { [any]: unknown }): Instance
local clone = instance:Clone()
if not clone then error("Attempt to clone a non-archivable instance", 3) end
return applyProperties(clone, properties)
end
end
local function create(classNameOrInstance: string|Instance)
if type(classNameOrInstance) == "string" then
return createInstance(classNameOrInstance)
elseif typeof(classNameOrInstance) == "Instance" then
return cloneInstance(classNameOrInstance)
else
error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2)
end
end
type Props = { [any]: any }
return (create :: any) ::
( <T>(T & Instance) -> (Props) -> T ) &
( ("Folder") -> (Props) -> Folder ) &
( ("BillboardGui") -> (Props) -> BillboardGui ) &
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
( ("Frame") -> (Props) -> Frame ) &
( ("ImageButton") -> (Props) -> ImageButton ) &
( ("ImageLabel") -> (Props) -> ImageLabel ) &
( ("ScreenGui") -> (Props) -> ScreenGui ) &
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
( ("TextBox") -> (Props) -> TextBox ) &
( ("TextButton") -> (Props) -> TextButton ) &
( ("TextLabel") -> (Props) -> TextLabel ) &
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
( ("UICorner") -> (Props) -> UICorner ) &
( ("UIGradient") -> (Props) -> UIGradient ) &
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
( ("UIListLayout") -> (Props) -> UIListLayout ) &
( ("UIPadding") -> (Props) -> UIPadding ) &
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
( ("UIScale") -> (Props) -> UIScale ) &
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
( ("UIStroke") -> (Props) -> UIStroke ) &
( ("UITableLayout") -> (Props) -> UITableLayout ) &
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
( ("VideoFrame") -> (Props) -> VideoFrame ) &
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
( (string) -> (Props) -> Instance )

112
src/defaults.lua Normal file
View file

@ -0,0 +1,112 @@
--------------------------------------------------------------------------------------------------------------
-- vide/defaults.lua
--------------------------------------------------------------------------------------------------------------
-- todo
local Enum = Enum
local Color3 = Color3
local Vector3 = Vector3
if not game then
local mock = require "test/mock"
Enum = mock.Enum :: any
Color3 = mock.Color3 :: any
Vector3 = mock.Vector3 :: any
end
return {
Part = {
Material = Enum.Material.SmoothPlastic,
Size = Vector3.new(1, 1, 1),
Anchored = true
},
BillboardGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
},
CanvasGroup = nil,
Frame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
},
ImageButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false
},
ImageLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
},
ScreenGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
},
ScrollingFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
ScrollBarImageColor3 = Color3.new(0, 0, 0)
},
SurfaceGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
PixelsPerStud = 50,
SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
},
TextBox = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
ClearTextOnFocus = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
TextButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
TextLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
-- UIComponent instances
VideoFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
},
ViewportFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
}
}

32
src/derive.lua Normal file
View file

@ -0,0 +1,32 @@
------------------------------------------------------------------------------------------
-- vide/derive.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type Unwrapper = graph.Unwrapper
local create = graph.create
local captureAndLink = graph.captureAndLink
local function derive<T>(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): State<T>
local node = create((nil :: any) :: T)
if cleanup then
local fn = deriveValue
local last: T? = nil
deriveValue = function(from)
if last ~= nil then cleanup(last) end
last = fn(from)
return last :: T
end
end
local value: T = captureAndLink(node, deriveValue)
node.cache = value
return node :: State<T>
end
return derive

24
src/each.lua Normal file
View file

@ -0,0 +1,24 @@
--------------------------------------------------------------------------------------------------------------
-- vide/each.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type Node<T> = graph.Node<T>
local create = graph.create
local get = graph.get
local wrapped = graph.wrapped
local link = graph.link
type Map<K, V> = { [K]: V }
local function each<K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
end
return (each :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
-> State<Map<K, VO>> )

5
src/flags.lua Normal file
View file

@ -0,0 +1,5 @@
--------------------------------------------------------------------------------------------------------------------------------
-- vide/flags.lua
--------------------------------------------------------------------------------------------------------------------------------
return { strict = false }

231
src/graph.lua Normal file
View file

@ -0,0 +1,231 @@
--------------------------------------------------------------------------------------------------------------
-- vide/graph.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local flags = require(script.Parent.flags)
export type State<T> = typeof(setmetatable(
{} :: {
cache: T,
updated: boolean,
derive: (any) -> T,
effects: { [(unknown) -> ()]: unknown } | false, -- weak values
children: { State<T> } | false -- weak values
}, {} :: {
__concat: (any, any) -> any,
__add: (any, any) -> any,
__sub: (any, any) -> any,
__mul: (any, any) -> any,
__div: (any, any) -> any,
--__pow
--__mod
--__unm
--__eq: (unknown, unknown) -> State<boolean>
--__lt
--__le
}
))
export type MaybeState<T> = State<T> | T
export type Unwrapper = <T>(T) -> T
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
local EVALUATION_ERR = "Error while evaluating state:\n\n"
local State = {}
local function wrapped(value: any): boolean
return getmetatable(value) == State
end
local unwrap: <T>(T) -> T;
local checkForYield do
local t = { __mode = "kv" }
setmetatable(t, t)
checkForYield = function(fn: (Unwrapper) -> ())
t.__unm = function()
fn(unwrap)
end
local ok, err = pcall(function()
return -t :: any
end)
if not ok then
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
error(EVALUATION_ERR .. "Cannot yield when deriving state in watcher", 3)
else
error(EVALUATION_ERR..err, 3)
end
end
end
end
local function setEffect<T>(state: State<unknown>, fn: (T) -> (), key: T)
if not state.effects then
state.effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
else
state.effects[fn :: () -> ()] = key
end
end
local function runEffects(state: State<unknown>)
if state.effects then
for effect, key in next, state.effects do
if flags.strict then effect(key) end
effect(key)
end
end
end
-- retrieves a state's cached value
-- recalculates value if an ancestor was updated
local function get<T>(state: State<T>): T
if state.updated then
state.updated = false
if flags.strict then checkForYield(state.derive) end
local ok, result: T|string? = pcall(state.derive, unwrap); if ok then
rawset(state :: any, "cache", result :: T)
else error(EVALUATION_ERR .. result :: string, 2) end
end
return state.cache
end
-- utility function for retrieving a value from state and allowing passthrough of non-state
unwrap = function<T>(value: MaybeState<T>): T
if wrapped(value) then
return get(value :: State<T>)
else
return value :: T
end
end
local function addChild(parent: State<unknown>, child: State<unknown>)
if parent.children then
table.insert(parent.children, child)
else
parent.children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
end
end
-- marks all state descendants for recalculation and runs effects
local function update(state: State<unknown>)
runEffects(state)
if state.children then
for _, child in state.children do
if not child.updated then
child.updated = true
update(child)
end
end
end
end
-- sets a state's cached value and updates all descendants
local function set<T>(state: State<T>, value: T)
state.cache = value
update(state)
end
-- links two states as parent-child
local function link(parent: State<unknown>, child: State<unknown>, derive: () -> unknown)
child.derive = derive
addChild(parent, child)
end
-- detect what states were referenced in the given callback and returns them in an array
local function capture<T>(callback: (Unwrapper) -> T): ({ State<unknown> }, T)
if flags.strict then checkForYield(callback) end
local states = table.create(2)
local ok: boolean, result: T|string = pcall(callback, function<T>(value: MaybeState<T>): T
if wrapped(value) then
table.insert(states, value :: State<T>)
return get(value :: State<T>)
else
return value :: T
end
end)
if not ok then error("Error while detecting watcher: " .. result :: string, 2) end
return states, result :: T
end
-- captures and links any detected states
local function captureAndLink<T>(child: State<T>, callback: (Unwrapper) -> T): T
local states, value = capture(callback)
child.derive = callback
for _, parent: State<unknown> in next, states do
addChild(parent, child)
end
return value :: T
end
local create: <T>(value: T) -> State<T>
-- factory function for creating operator overloads for shorthands to derive state
local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any
return function(a: MaybeState<unknown>, b: MaybeState<unknown>): State<unknown>
local derived: State<unknown> = create(nil :: any)
local aIsState = wrapped(a)
local bIsState = wrapped(b)
if aIsState and bIsState then
local function derive() return op(get(a :: State<unknown>), get(b :: State<unknown>)) end
link(a :: State<unknown>, derived, derive)
link(b :: State<unknown>, derived, derive)
elseif aIsState then
link(a :: State<unknown>, derived, function() return op(get(a :: State<unknown>), b) end)
else--if bIsState then
link(b :: State<unknown>, derived, function() return op(a, get(b :: State<unknown>)) end)
end
derived.updated = true
return derived
end
end
function State.__index(_, index)
if index == "cache" then return nil end -- todo: better solution
error("attempt to index state", 2)
end
State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end)
State.__add = overload(function(a: any, b: any) return a + b end)
State.__sub = overload(function(a: any, b: any) return a - b end)
State.__mul = overload(function(a: any, b: any) return a * b end)
State.__div = overload(function(a: any, b: any) return a / b end)
--State.__eq = overload(function(a: any, b: any) return a == b end)
function create<T>(value: T): State<T>
return setmetatable({
cache = value,
updated = false,
derive = function() return nil end :: any,
effects = false :: false,
children = false :: false
}, State)
end
return table.freeze {
setEffect = setEffect,
get = get,
set = set,
unwrap = unwrap,
link = link,
capture = capture,
captureAndLink = captureAndLink,
wrapped = wrapped,
create = create,
}

84
src/init.lua Normal file
View file

@ -0,0 +1,84 @@
--------------------------------------------------------------------------------------------------------------
-- vide.lua
-- v0.1.0
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local create = require(script.create)
local apply = require(script.apply)
local wrap = require(script.wrap)
local watch = require(script.watch)
local derive = require(script.derive)
local map = require(script.map)
local unwrap = require(script.unwrap)
local wrapped = require(script.wrapped)
local Layout = require(script.Layout)
local Children = require(script.Children)
local Event = require(script.Event)
local Changed = require(script.Change)
local Created = require(script.Created)
local spring, updateSprings = require(script.spring)()
local flags = require(script.flags)
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 )
local vide = {
-- core
create = create,
apply = apply,
wrap = (wrap :: any) :: <T>(value: T?) -> (T, Setter<T>),
derive = (derive :: any) :: <T>(deriver: (from: <U>(U) -> U) -> T, cleanup: (T) -> ()?) -> T,
map = (map :: any) :: ( <V>(input: number, transform: (number) -> V, cleanup: (V) -> ()?) -> Map<number, V> ) & ( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?) -> Map<K, VO> ),
watch = watch :: ((Unwrapper) -> ()) -> () -> (),
-- util
unwrap = unwrap,
wrapped = wrapped,
-- animations
spring = (spring :: any) :: <T>(input: T, period: number, damping: number?) -> T,
-- symbols
Event = Event,
Changed = Changed,
Layout = Layout,
Children = Children,
Created = Created,
-- flags
strict = (nil :: any) :: boolean,
-- test
step = function(dt: number)
updateSprings(dt)
end
}
setmetatable(vide :: any, {
__index = function(_, index: unknown)
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
end,
__newindex = function(_, index: unknown, value: unknown)
if index == "strict" then
flags.strict = if type(value) == "boolean" then value else error("strict must be a boolean", 2)
else
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
end
end
})
if game then
game:GetService("RunService").Heartbeat:Connect(function(dt: number)
task.defer(vide.step, dt)
end)
end
return vide

76
src/map.lua Normal file
View file

@ -0,0 +1,76 @@
--------------------------------------------------------------------------------------------------------------
-- vide/map.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
local create = graph.create
local get = graph.get
local wrapped = graph.wrapped
local link = graph.link
type Map<K, V> = { [K]: V }
local function map<K, VI, VO>(input: unknown, transform: (K, VI) -> VO, cleanup: (VO) -> ()?): (MaybeState<Map<K, VO>>)
if type(input) == "number" then
local output: Map<K, VO> = table.create(input) :: {}
for i = 1, input do
local v = transform(i :: any, nil :: any)
output[i :: any] = v
end
return output
elseif wrapped(input) then
local lastInput: Map<K, VI> = {}
local lastOutput: Map<K, VO> = {}
local output = create(lastOutput)
local function derive()
local newInput = get(input :: State<Map<K, VI>>)
if type(newInput) ~= "table" then error("Attempt to run map on a non-table state", 0) end
local lastInputClone = table.clone(lastInput)
for k, vi in next, newInput do
if vi ~= lastInputClone[k] then
local vo = transform(k, vi)
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
lastInput[k] = vi
lastOutput[k] = vo
end
lastInputClone[k] = nil
end
for k, v in next, lastInputClone do
lastInput[k] = nil
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
lastOutput[k] = nil
end
return table.clone(lastOutput)
end
link(input :: State<Map<K, VI>>, output, derive)
output.updated = true
return output
elseif type(input) == "table" then
local output: Map<K, VO> = table.create(#input :: any) :: {}
for k, vi in input :: Map<K, VI> do
local vo = transform(k, vi)
output[k] = vo
end
return output
else error(string.format("Invalid type arg #1, expected number or table or state (got %s)", tostring(input)), 2) end
end
return (map :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
-> State<Map<K, VO>> )

21
src/memoize.lua Normal file
View file

@ -0,0 +1,21 @@
--------------------------------------------------------------------------------------------------------------
-- vide/memoize.lua
--------------------------------------------------------------------------------------------------------------
local function memoize<X, Y>(f: (X) -> Y): ((X) -> Y, { [X]: Y })
local cache: { [X]: Y } = {}
return function(x: X): Y
local y: Y? = cache[x]
if not y then
y = f(x)
cache[x] = y
end
return y :: Y
end, cache
end
return memoize

169
src/spring.lua Normal file
View file

@ -0,0 +1,169 @@
--------------------------------------------------------------------------------------------------------------
-- vide/spring.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
--[[
Spring animation library adapted from RDL::spring v1.0
Supported datatypes:
*number
!bool
*CFrame
?Rect
*Color3
*UDim
*UDim2
*Vector2
!Vector2int16
*Vector3
!Vector3int16
!EnumItem
]]
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
local create = graph.create
local get = graph.get
local set = graph.set
local unwrap = graph.unwrap
local wrapped = graph.wrapped
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
type SpringData<T> = {
Alpha: number;
Duration: number;
Period: number;
Damping: number;
Velocity: number;
InitialVelocity: number;
Initial: T;
Target: T;
Input: State<T>;
}
type Lerp<T> = (initial: T, target: T, alpha: number) -> T
local function solve(T: number, z: number, u: number, t: number): number -- alpha
local wn = 2*math.pi / T
local wd = wn * math.sqrt(1 - z^2)
local a = z * wn
local s = math.exp(-a*t) * math.cos(wd*t)
local v = (u/wn) * math.exp(-a*t) * math.sin(wn*t)
return (1-s) + v
end
local lerpable: { [string]: Lerp<any> } = {
number = function(v1, v2, a)
return v1 + (v2 - v1)*a
end :: Lerp<number>,
CFrame = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<CFrame>,
Color3 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Color3>,
UDim = function(v1, v2, a)
return UDim.new(
v1.Scale + (v2.Scale - v1.Scale)*a,
v1.Offset + (v2.Offset - v1.Offset)*a
)
end :: Lerp<UDim>,
UDim2 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<UDim2>,
Vector2 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Vector2>,
Vector3 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Vector3>,
}
local activeSprings: { [State<any>]: SpringData<any> } = {}
setmetatable(activeSprings, { __mode = "ks" })
local function spring<T>(input: MaybeState<T>, period: number, damping: number?): State<T>
local initial = unwrap(input) :: T
local output = create(initial)
local springData: SpringData<T> = {
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
}
activeSprings[output] = springData
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<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
return function() return spring, updateSprings end

15
src/throw.lua Normal file
View file

@ -0,0 +1,15 @@
------------------------------------------------------------------------------------------
-- vide/throw.lua
------------------------------------------------------------------------------------------
local function throw(msg: string)
local stack = 1
while debug.info(stack, "s") == debug.info(1, "s") do
stack += 1
end
error(msg, stack)
end
return throw

9
src/unwrap.lua Normal file
View file

@ -0,0 +1,9 @@
--------------------------------------------------------------------------------------------------------------
-- vide/unwrap.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
return graph.unwrap

36
src/watch.lua Normal file
View file

@ -0,0 +1,36 @@
--------------------------------------------------------------------------------------------------------------
-- vide/watch.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type Unwrapper = graph.Unwrapper
local setEffect = graph.setEffect
local capture = graph.capture
local unwrap = graph.unwrap
local function watch(effect: (Unwrapper) -> ()): () -> ()
local states, cleanup = capture(effect :: () -> (() -> ()?))
local function fn()
if cleanup then cleanup(); cleanup = nil end
cleanup = effect(unwrap)
end
for _, state in next, states do
setEffect(state, fn, true)
end
local function unwatch()
for _, state in next, states do
setEffect(state, fn, nil)
end
if cleanup then cleanup(); cleanup = nil end
end
return unwatch
end
return watch

42
src/wrap.lua Normal file
View file

@ -0,0 +1,42 @@
------------------------------------------------------------------------------------------
-- vide/wrap.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
local create = graph.create
local get = graph.get
local set = graph.set
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
type Setter<T> = (value: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?) -> T
local function wrap<T>(value: MaybeState<T>?): (State<T>, Setter<T>)
local state = create(if wrapped(value) then get(value :: State<T>) else value :: T)
local function setter(vi: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?): T
if type(vi) == "function" then
vi = vi(get(state))
end
local v = if wrapped(vi) then get(vi :: State<T>) else vi :: T
if v ~= state.cache or force then
set(state, v)
elseif flags.strict and type(v) == "table" then
throw("attempt to set same table object")
end
return v
end
return state, setter
end
return wrap

9
src/wrapped.lua Normal file
View file

@ -0,0 +1,9 @@
------------------------------------------------------------------------------------------
-- vide/wrapped.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
return graph.wrapped