mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge branch 'main' into fix/color
This commit is contained in:
commit
b844fd076a
67 changed files with 2598 additions and 1119 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -4,10 +4,46 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
---
|
||||
## [0.2.0] - 2023-11-22
|
||||
|
||||
## [0.1.0] - 0000-00-00
|
||||
### Added
|
||||
|
||||
- Batched updates with `batch()`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved graph updating algorithm.
|
||||
- Graph nodes when destroyed no longer destroy children; only owned.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Graph edge case where a destroyed node can be readded if it was queued for
|
||||
rerun before being destroyed.
|
||||
- Some properties not being applied when `create()` is used recursively.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.1.1] - 2023-09-30
|
||||
|
||||
### Added
|
||||
|
||||
- `cleanup()` accepts objects with a `Destroy()` or `Disconnect()` interface.
|
||||
- `read()` as a utility to read sources or passthrough a non-source value.
|
||||
|
||||
### Changed
|
||||
|
||||
- Reactive scopes created within reactive scopes are now destroyed on rerun.
|
||||
- `untrack()` can be called outside of reactive scopes.
|
||||
- `changed()` will also run its callback with the initial property value.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `show()` and `switch()` not updating when in strict mode.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.1.0] - 2023-09-20
|
||||
|
||||
- Initial release
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -1,18 +1,13 @@
|
|||
<br>
|
||||
|
||||
<div align="center">
|
||||
<img style="float: right;margin-top:50px" src="docs/public/logo.svg" width="110" />
|
||||
<img src="docs/public/full_logo.svg" width="600" />
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
### ⚠️ This library is in early stages of development with breaking changes being made often.
|
||||
|
||||
Vide is a reactive UI library.
|
||||
Vide is a reactive Luau UI library inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
- Fully Luau typecheckable
|
||||
- Declarative and concise syntax.
|
||||
- Minimal imports.
|
||||
- Reactively driven.
|
||||
|
||||
## Getting started
|
||||
|
|
@ -24,7 +19,6 @@ for a quick introduction to the library.
|
|||
## Code sample
|
||||
|
||||
```lua
|
||||
local vide = require(path_to_vide)
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { defineConfig } from "vitepress"
|
||||
//import { defineConfig } from "vitepress"
|
||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
export default withMermaid({
|
||||
title: "Vide",
|
||||
titleTemplate: ":title - A reactive UI library for Luau",
|
||||
description: "A reactive UI library for Luau.",
|
||||
|
|
@ -16,7 +17,6 @@ export default defineConfig({
|
|||
{ text: "Home", link: "/" },
|
||||
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "API", link: "/api/reactivity-core"},
|
||||
{ text: "GitHub", link: "https://github.com/centau/vide" }
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
|
|
@ -41,34 +41,31 @@ export default defineConfig({
|
|||
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
|
||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Source", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effect", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Derived Source", link: "/tut/crash-course/6-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/7-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/8-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/9-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/10-actions" },
|
||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Root Scopes", link: "/tut/crash-course/6-root" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||
{ text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/13-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Control Flow WIP",
|
||||
text: "Advanced Reactivity",
|
||||
items: [
|
||||
{ text: "switch", link: "/tut/control-flow/switch.md" },
|
||||
{ text: "indexes", link: "/tut/control-flow/indexes.md" },
|
||||
{ text: "values", link: "/tut/control-flow/values.md" },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Advanced Reactivity WIP",
|
||||
items: [
|
||||
{ text: "reactive-scopes", link: "/tut/reactive-scoping.md"}
|
||||
{ text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
// socialLinks: [
|
||||
// { icon: "github", link: "https://github.com/centau/vide" }
|
||||
// ]
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/centau/vide" }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
## mount()
|
||||
|
||||
Runs a function and applies its result to a target instance.
|
||||
Runs a function in a new reactive scope and optionally applies its result to a
|
||||
target instance.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ Runs a function and applies its result to a target instance.
|
|||
|
||||
- **Details**
|
||||
|
||||
The result of the function is applies to the target in the same way
|
||||
The result of the function is applied to a target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
The function is ran in a new reactive scope, just like
|
||||
|
|
@ -60,19 +61,16 @@ Creates a new UI element, applying any given properties.
|
|||
|
||||
- **Property setting rules**
|
||||
|
||||
- If a table index is a string:
|
||||
- If its value is a function then it will either bind that property to
|
||||
the function or connect it if the property type is a `RBXScriptSignal`.
|
||||
- If the value is not a function then the property will be set to that
|
||||
value.
|
||||
- If a table index is a number:
|
||||
- If its value is an action then that action will be queued to run after
|
||||
properties are set.
|
||||
- If its value is a table then that table will be recursively
|
||||
processed just like the outer table.
|
||||
- If its value is a function then it will bind the instances children to
|
||||
that function.
|
||||
- If its value is an instance then it will be parented to the instance.
|
||||
- **index is string:**
|
||||
- **value is function:**
|
||||
- **property is event:** connect function as callback
|
||||
- **property is not event:** create effect to update property
|
||||
- **value is not function:** set property to value
|
||||
- **index is number:**
|
||||
- **value is action:** run action
|
||||
- **value is table:** recurse table
|
||||
- **value is function:** create effect to update children
|
||||
- **value is instance:** set instance as child
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -138,9 +136,14 @@ instances.
|
|||
```lua
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
instance:GetPropertyChangedSignal("property"):Connect(function()
|
||||
local con - instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
-- disconnect on reactive scope destruction to allow gc of instance
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -151,3 +154,23 @@ instances.
|
|||
changed("Text", output)
|
||||
}
|
||||
```
|
||||
|
||||
## changed()
|
||||
|
||||
A wrapper for `action()` to listen for property changes.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function changed(property: string, callback: (...unknown) -> ()): Action
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Will run the given callback any time the property is changed, as well as
|
||||
when the action is initially run.
|
||||
|
||||
The changed connection is disconnected when the reactive scope the action is
|
||||
ran in is destroyed.
|
||||
|
||||
Runs with an action priority of 1.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
<br/>
|
||||
|
||||
:::warning
|
||||
Yielding is not allowed in any reactive scope. Strict mode can check for this.
|
||||
:::
|
||||
|
||||
## root()
|
||||
|
||||
Creates and runs a function in a new reactive scope.
|
||||
|
|
@ -14,18 +18,14 @@ Creates and runs a function in a new reactive scope.
|
|||
|
||||
- **Details**
|
||||
|
||||
Returns the result of the given function.
|
||||
|
||||
Creates a new root reactive scope, where creation and derivations of sources
|
||||
can be tracked and properly disposed of.
|
||||
|
||||
Returns the result of the given function.
|
||||
|
||||
A function to destroy the root is passed into the callback, which will run
|
||||
any cleanups and allow derived sources created to garbage collect.
|
||||
|
||||
::: warning
|
||||
`fn()` cannot yield.
|
||||
:::
|
||||
|
||||
## source()
|
||||
|
||||
Creates a new source with the given value.
|
||||
|
|
@ -44,6 +44,8 @@ Creates a new source with the given value.
|
|||
Reading from the source from within a reactive scope will cause changes
|
||||
to that source to be tracked and anything depending on it to update.
|
||||
|
||||
Sources can be created outside of reactive scopes.
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
|
|
@ -56,7 +58,7 @@ Creates a new source with the given value.
|
|||
|
||||
## effect()
|
||||
|
||||
Runs a side-effect on source update.
|
||||
Runs a side-effect in a new reactive scope on source update.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -66,14 +68,10 @@ Runs a side-effect on source update.
|
|||
|
||||
- **Details**
|
||||
|
||||
The callback is ran immediately.
|
||||
|
||||
Any time a source referenced in the callback is changed, the callback will
|
||||
be reran.
|
||||
|
||||
::: warning
|
||||
`callback()` cannot yield.
|
||||
:::
|
||||
The callback is ran to initially ran on first call to find dependent sources.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -93,7 +91,7 @@ Runs a side-effect on source update.
|
|||
|
||||
## derive()
|
||||
|
||||
Derives a new source from existing sources.
|
||||
Derives a new source in a new reactive scope from existing sources.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -109,12 +107,7 @@ Derives a new source from existing sources.
|
|||
Anytime its value is recalculated it is also cached, subsequent calls will
|
||||
retun this cached value until it recalculates again.
|
||||
|
||||
Takes a callback that is immediately run to determine what sources are being
|
||||
referenced.
|
||||
|
||||
::: warning
|
||||
`source()` cannot yield.
|
||||
:::
|
||||
The callback is ran to initially ran on first call to find dependent sources.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,33 @@
|
|||
|
||||
<br/>
|
||||
|
||||
## show()
|
||||
|
||||
Shows one of two components depending on an input source.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function show<T>(source: () -> unknown, component: () -> T): () -> T?
|
||||
function show<T, U>(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
When the input source changes from a falsey to a truthy value, the
|
||||
component will be reran under a new reactive scope. If it changes from a
|
||||
truthy to falsey value, the reactive scope the component was created in will
|
||||
be destroyed, and the returned source will output `nil`, or a fallback
|
||||
component if given.
|
||||
|
||||
The fallback component is also ran under a new reactive scope, and destroyed
|
||||
when the input source switches back to truthy.
|
||||
|
||||
## switch()
|
||||
|
||||
Changes object based on a source and a mapping table.
|
||||
Shows one of a set of components depending on an input source and a mapping table.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -14,12 +38,14 @@ Changes object based on a source and a mapping table.
|
|||
|
||||
- **Details**
|
||||
|
||||
The mapped function is ran in a new reactive scope that is destroyed when
|
||||
the source changes and maps to a different function.
|
||||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
::: warning
|
||||
Mapped functions cannot yield.
|
||||
:::
|
||||
When the input source changes, the new value will be used to lookup a given
|
||||
mapping table to get a component, which will be ran under a new reactive
|
||||
scope. If the input source changes, the reactive scope the component was
|
||||
created in will be destroyed, and a new component created under a new
|
||||
reactive scope. If no component is found for an input value, the switch will
|
||||
output `nil`.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -51,24 +77,26 @@ Maps each index in a table source to an object.
|
|||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
When the input source changes, each *index* in the new table is compared with
|
||||
the last input table.
|
||||
|
||||
- For any new index, the `transform` function is ran under a new reactive
|
||||
scope to produce a new instance.
|
||||
- For any removed index, the reactive scope for that index is destroyed.
|
||||
- Unchanged indexes are untouched.
|
||||
|
||||
The transform function is called only ever *once* for each index in the
|
||||
source table. The first argument is a source containing the index's value
|
||||
and the second argument is just the index.
|
||||
source table.
|
||||
|
||||
Anytime a new index is added, the transform function will be called again
|
||||
for that new index.
|
||||
1. First argument is a *source containing the index's value*.
|
||||
2. Second argument is the *index itself*.
|
||||
|
||||
Anytime an existing index value changes, the transform function is not rerun,
|
||||
instead the source value for that index will update, causing anything
|
||||
Anytime an existing index's value changes, the transform function is not
|
||||
rerun, instead the source value for that index will update, causing anything
|
||||
depending on it to update too.
|
||||
|
||||
Returns a state containing an array of all objects returned by the
|
||||
transform.
|
||||
|
||||
::: warning
|
||||
`transform()` cannot yield.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
||||
The intended purpose of this function is to map each index in a table to
|
||||
|
|
@ -85,14 +113,12 @@ Maps each index in a table source to an object.
|
|||
local displays = indexes(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return item().name
|
||||
return i .. ": " .. item().name
|
||||
end,
|
||||
|
||||
Image = function()
|
||||
return "rbxassetid://" .. item().icon
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
|
@ -111,28 +137,31 @@ Maps each value in a table source to an object.
|
|||
|
||||
- **Details**
|
||||
|
||||
The transform function is called only ever *once* for each value in the
|
||||
source table. The first argument is the index's value and
|
||||
the second argument is a source containing the index.
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
Anytime a new value is added, the transform function will be called again
|
||||
for that new value.
|
||||
When the input source changes, each *value* in the new table is compared with
|
||||
the last input table. Similar to `indexes()` but for values instead of indexes.
|
||||
|
||||
- For any new value, the `transform` function is ran under a new reactive
|
||||
scope to produce a new instance.
|
||||
- For any removed value, the reactive scope for that value is destroyed.
|
||||
- Unchanged values are untouched.
|
||||
|
||||
The transform function is only ever called *once* for each value in the
|
||||
source table.
|
||||
|
||||
1. First argument is the *value itself*.
|
||||
2. Second argument is a *source containing the value's index*.
|
||||
|
||||
Anytime an existing value's index changes, the transform function is not
|
||||
rerun, instead the source index for that value will update, causing anything
|
||||
depending on it to update too.
|
||||
|
||||
Returns a state containing an array of all objects returned by the
|
||||
transform.
|
||||
|
||||
::: warning
|
||||
`transform()` cannot yield.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Having primitive values in the source table can cause unexpected behavior,
|
||||
as duplicate primitives can result in multiple index sources being bound
|
||||
to the same UI element.
|
||||
Having primitive values in the input source table can cause unexpected
|
||||
behavior, as duplicate values can result in multiple tranforms being ran for
|
||||
a single value, meaning there can be multiple source indexes bound to the
|
||||
same UI element. Strict mode has checks for this.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
|
@ -150,11 +179,11 @@ Maps each value in a table source to an object.
|
|||
|
||||
local displays = values(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = item.Name
|
||||
Name = function()
|
||||
return i() .. ": " .. item.Name
|
||||
end
|
||||
|
||||
Image = "rbxassetid://" .. item.icon,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
|
@ -181,6 +210,9 @@ Maps each value in a table source to an object.
|
|||
|
||||
In most cases, both functions will appear to have the same behavior.
|
||||
The main difference is performance, picking the right function to use can
|
||||
result in less property updates and less re-renders.
|
||||
result in less property updates and less re-renders. One case to note is
|
||||
that `values()` works nicely when animating re-ordering of instances, since
|
||||
the value is not destroyed when indexes are changed, and the source index
|
||||
can easily be put through a spring.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
## cleanup()
|
||||
|
||||
Runs a callback anytime a reactive scope is re-ran.
|
||||
Runs a callback anytime a reactive scope is reran or destroyed.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function cleanup(callback: () -> ())
|
||||
function cleanup(obj: Destroyable)
|
||||
function cleanup(obj: Disconnectable)
|
||||
|
||||
type Destroyable = { destroy: () -> () }
|
||||
type Disconnectable = { disconnect: () -> () }
|
||||
```
|
||||
|
||||
- **Example**
|
||||
|
|
@ -24,23 +29,10 @@ Runs a callback anytime a reactive scope is re-ran.
|
|||
end)
|
||||
```
|
||||
|
||||
```lua
|
||||
local data = source(1)
|
||||
|
||||
derive(function()
|
||||
local label = create "TextLabel" { Text = data() }
|
||||
|
||||
cleanup(function()
|
||||
label:Destroy()
|
||||
end)
|
||||
|
||||
return label
|
||||
end)
|
||||
```
|
||||
|
||||
## untrack()
|
||||
|
||||
Runs a given function where any sources read will not track its reactive scope.
|
||||
Runs a given function where any sources read will not be tracked by a reactive
|
||||
scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -70,4 +62,33 @@ Runs a given function where any sources read will not track its reactive scope.
|
|||
print(sum()) -- 2
|
||||
```
|
||||
|
||||
## read()
|
||||
|
||||
Utility used to read a value that is either a primitive or a source. Sources
|
||||
read can still be tracked inside a reactive scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function read<T>(value: T | () -> T): T
|
||||
```
|
||||
|
||||
## batch()
|
||||
|
||||
Runs a given function where any source updates made within the function do not
|
||||
trigger effects until after the function runs.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function batch(fn: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Improves performance when an effect depends on multiple sources, and those
|
||||
sources need to be updated. Updating those sources inside a batch call will
|
||||
only cause the effect to run once after the batch call ends instead of after
|
||||
each time a source is updated.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ Strict mode is library-wide and can get set by doing:
|
|||
vide.strict = true
|
||||
```
|
||||
|
||||
It is automatically enabled when Vide is first required and not running in O2
|
||||
optimization level.
|
||||
|
||||
Strict mode is designed to help the development process by adding safety checks
|
||||
and identifying improper usage.
|
||||
|
||||
|
|
@ -15,12 +18,13 @@ Currently, strict mode will:
|
|||
2. Run effects twice when a source updates.
|
||||
3. Throw an error if yields occur where they are not allowed.
|
||||
4. Checks for `indexes()` and `values()` returning primitive values.
|
||||
5. Checks for duplicate nested properties at same depth.
|
||||
6. Better error reporting and stack traces.
|
||||
7. Checks for multiple `cleanup()` calls in the same function scope.
|
||||
5. Checks for `values()` input having duplicate values.
|
||||
6. Checks for duplicate nested properties at same depth.
|
||||
7. Better error reporting and stack traces + creation traces of property bindings.
|
||||
|
||||
By rerunning sources and effects, any side-effects are made more apparent.
|
||||
This also helps ensure that cleanups are being handled correctly.
|
||||
By rerunning derived sources and effects twice each time they update,it helps
|
||||
ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
|
||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||
which strict mode can catch.
|
||||
|
|
@ -29,5 +33,6 @@ As well as additional safety checks, Vide will dedicate extra resources to
|
|||
recording and better emitting stack traces where errors occur, particularly
|
||||
when binding properties to sources.
|
||||
|
||||
It is recommend to develop UI with strict mode and to disable it when pushing to
|
||||
production.
|
||||
It is recommended to develop UI with strict mode and to disable it when pushing to
|
||||
production. In Roblox, production code compiles at O2 by default, so you don't
|
||||
need to worry about disabling strict mode unless you have manually enabled it.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
},
|
||||
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.0.0-rc.4"
|
||||
"vitepress": "1.0.0-rc.25",
|
||||
"vitepress-plugin-mermaid": "2.0.14"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
docs/public/full_logo.svg
Normal file
67
docs/public/full_logo.svg
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<svg width="150" height="88" viewBox="15 7 115 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1_41)">
|
||||
<g clip-path="url(#clip0_1_41)">
|
||||
<rect x="18" y="12" width="110" height="48" rx="8" fill="url(#paint0_radial_1_41)"/>
|
||||
<path d="M65.4148 32.2727L68.0057 39.6193H68.108L70.6989 32.2727H71.8068L68.6023 41H67.5114L64.3068 32.2727H65.4148ZM77.223 32.2727V41H76.1662V32.2727H77.223ZM85.0352 41H82.342V32.2727H85.1545C86.0011 32.2727 86.7255 32.4474 87.3278 32.7969C87.93 33.1435 88.3917 33.642 88.7127 34.2926C89.0337 34.9403 89.1942 35.7159 89.1942 36.6193C89.1942 37.5284 89.0323 38.3111 88.7085 38.9673C88.3846 39.6207 87.913 40.1236 87.2937 40.4759C86.6744 40.8253 85.9215 41 85.0352 41ZM83.3988 40.0625H84.967C85.6886 40.0625 86.2866 39.9233 86.761 39.6449C87.2354 39.3665 87.5891 38.9702 87.8221 38.456C88.055 37.9418 88.1715 37.3295 88.1715 36.6193C88.1715 35.9148 88.0565 35.3082 87.8263 34.7997C87.5962 34.2884 87.2525 33.8963 86.7951 33.6236C86.3377 33.348 85.7681 33.2102 85.0863 33.2102H83.3988V40.0625ZM93.967 41V32.2727H99.234V33.2102H95.0238V36.1591H98.9613V37.0966H95.0238V40.0625H99.3022V41H93.967ZM109.022 32.2727H111.127V37.902C111.127 38.5526 110.973 39.1193 110.663 39.6023C110.353 40.0824 109.922 40.4545 109.368 40.7188C108.814 40.9801 108.17 41.1108 107.437 41.1108C106.696 41.1108 106.048 40.9801 105.494 40.7188C104.94 40.4545 104.51 40.0824 104.203 39.6023C103.896 39.1193 103.743 38.5526 103.743 37.902V32.2727H105.852V37.7188C105.852 38.0199 105.917 38.2884 106.048 38.5241C106.181 38.7599 106.368 38.9446 106.606 39.0781C106.845 39.2116 107.122 39.2784 107.437 39.2784C107.752 39.2784 108.028 39.2116 108.264 39.0781C108.502 38.9446 108.689 38.7599 108.822 38.5241C108.956 38.2884 109.022 38.0199 109.022 37.7188V32.2727ZM117.559 32.2727V41H115.45V32.2727H117.559Z" fill="white"/>
|
||||
<g filter="url(#filter1_ddd_1_41)">
|
||||
<path d="M39.4019 47.5C40.5566 49.5 43.4434 49.5 44.5981 47.5L56.4546 26.9639C57.2786 25.5367 56.1196 23.7812 54.4834 23.9783L48.3548 24.7164C46.9029 24.8913 45.6622 25.8444 45.1191 27.2021L42 35L38 25L29.5166 23.9783C27.8804 23.7812 26.7214 25.5367 27.5454 26.9639L39.4019 47.5Z" fill="url(#paint1_radial_1_41)"/>
|
||||
<path d="M38 25L42 35L46 45.0718C45.1412 46.5593 42.9736 46.4973 42.2012 44.9632L31.7728 24.25L38 25Z" fill="url(#paint2_radial_1_41)"/>
|
||||
<path d="M56.4546 26.9639L44.5981 47.5C43.4434 49.5 40.5566 49.5 39.4019 47.5L38 45.0718L34 38.1436C35.6267 40.9319 39.7492 40.627 40.9481 37.6298L42 35L46 25L54.4834 23.9783C56.1196 23.7812 57.2786 25.5367 56.4546 26.9639Z" fill="url(#paint3_radial_1_41)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1_41" x="0" y="0" width="150" height="88" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1_41"/>
|
||||
<feOffset dx="2" dy="8"/>
|
||||
<feGaussianBlur stdDeviation="12"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0690196 0 0 0 0 0.313726 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_41"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_41" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_ddd_1_41" x="13.2732" y="11.9631" width="61.4536" height="57.0369" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="2" dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_41"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1_41" result="effect2_dropShadow_1_41"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_1_41" result="effect3_dropShadow_1_41"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1_41" result="shape"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(73 12) rotate(90) scale(48 58.022)">
|
||||
<stop stop-color="#3D506C"/>
|
||||
<stop offset="1" stop-color="#2F415C"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(42 51.5) rotate(-90) scale(28 32.3316)">
|
||||
<stop stop-color="#2A57A2"/>
|
||||
<stop offset="1" stop-color="#407BBA"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39 24) rotate(74.3578) scale(25.9615 15.2089)">
|
||||
<stop stop-color="#1D314F"/>
|
||||
<stop offset="1" stop-color="#0D1A2E"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint3_radial_1_41" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(34 52) rotate(-49.3987) scale(36.8782 31.6434)">
|
||||
<stop stop-color="#5792D9"/>
|
||||
<stop offset="1" stop-color="#6AA8E9"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_1_41">
|
||||
<rect x="18" y="12" width="110" height="48" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
|
@ -1,4 +1,39 @@
|
|||
<svg width="150" height="300" viewBox="0 0 334 478" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 1.13748V276.5H254.402L254 276.973V278H253.127L166.5 380L78.5984 276.5H80V1.20867L0 78.4638V322.5L167 478L334 322.5V78.3926L254 1.13748Z" fill="#223450"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 278V0.715805L296.904 42V292L167 417L37.0962 292V42L80 0.715805V278H80.3975L167 362L253.603 278H254Z" fill="#4796F3"/>
|
||||
<svg width="384" height="384" viewBox="8 8 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ddd_1_134)">
|
||||
<path d="M21.4019 35.5C22.5566 37.5 25.4434 37.5 26.5981 35.5L38.4546 14.9639C39.2786 13.5367 38.1196 11.7812 36.4834 11.9783L30.3548 12.7164C28.9029 12.8913 27.6622 13.8444 27.1191 15.2021L24 23L20 13L11.5166 11.9783C9.88038 11.7812 8.72139 13.5367 9.54541 14.9639L21.4019 35.5Z" fill="url(#paint0_radial_1_134)"/>
|
||||
<path d="M20 13L24 23L28 33.0718C27.1412 34.5593 24.9736 34.4973 24.2012 32.9632L13.7728 12.25L20 13Z" fill="url(#paint1_radial_1_134)"/>
|
||||
<path d="M38.4546 14.9639L26.5981 35.5C25.4434 37.5 22.5566 37.5 21.4019 35.5L20 33.0718L16 26.1436C17.6267 28.9319 21.7492 28.627 22.9481 25.6298L24 23L28 13L36.4834 11.9783C38.1196 11.7812 39.2786 13.5367 38.4546 14.9639Z" fill="#4896F3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_1_134" x="-4.72681" y="-0.0369511" width="61.4536" height="57.037" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="2" dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_134"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1_134" result="effect2_dropShadow_1_134"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_1_134" result="effect3_dropShadow_1_134"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1_134" result="shape"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_1_134" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24 39.5) rotate(-90) scale(28 32.3316)">
|
||||
<stop stop-color="#24447F"/>
|
||||
<stop offset="1" stop-color="#3661A2"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_1_134" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21 12) rotate(74.3578) scale(25.9615 15.2089)">
|
||||
<stop stop-color="#1D314F"/>
|
||||
<stop offset="1" stop-color="#0D1A2E"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 2.8 KiB |
188
docs/tut/advanced/nested-scoping.md
Normal file
188
docs/tut/advanced/nested-scoping.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Nested Reactive Scopes
|
||||
|
||||
Nesting reactive scopes gives you finer control over the reactive graph, but
|
||||
needs more work to do. The built-in control flow functions try to cover the
|
||||
most common cases, but they do not cover all of them.
|
||||
|
||||
This tutorial will demonstrate how to implement a `show()` control flow function
|
||||
using just sources and effects.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
local toggled = source(true)
|
||||
|
||||
show(toggled, Button)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> show
|
||||
|
||||
subgraph show[show effect]
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Above is the reactive graph for `show()`. It creates a new effect depending on
|
||||
`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
|
||||
`show` effect calls `Counter`, which creates a new reactive scope to update its
|
||||
text whenever `count` changes. As per the rules of reactive scopes, a reactive
|
||||
scope rerunning will destroy any reactive scope created within it. So the text
|
||||
effect's reactive scope is destroyed whenever the show effect is rerun.
|
||||
|
||||
The same can be achieved without the use of `show()`:
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
local toggled = source(true)
|
||||
|
||||
effect(function()
|
||||
if toggled() then
|
||||
local destroy = mount(Button)
|
||||
cleanup(destroy)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> effect
|
||||
|
||||
subgraph effect
|
||||
subgraph mount2[inner mount]
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is another way to achieve the same. Here we use `mount()` within the effect
|
||||
to manually create and destroy a new reactive scope whenever the effect reruns.
|
||||
|
||||
Alternatively, instead of using `mount()`, a new reactive scope can be created
|
||||
directly within the effect:
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
local toggled = source(true)
|
||||
|
||||
effect(function()
|
||||
if toggled() then
|
||||
untrack(Button)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> effect
|
||||
|
||||
subgraph effect
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Without the use of `untrack()`, an error would occur, since Vide does not allow
|
||||
the creation of reactive scopes inside reactive scopes that are tracking. The
|
||||
reason for this, is because if the `Counter` component reads from a source
|
||||
internally, that can cause the reactive scope calling `Counter()` to track that
|
||||
source, causing unintentional reruns. As a guard against this, you are forced to
|
||||
use `untrack()` to create nested reactive scopes.
|
||||
|
||||
The final result is the same as using the `show()` component. An effect is
|
||||
created which creates the counter, which creates its own reactive scope. The
|
||||
effect rerunning causes the counter's internal reactive scope to be destroyed,
|
||||
making sure everything is cleaned up.
|
||||
|
||||
|
||||
0
docs/tut/control-flow/1-intro.md
Normal file
0
docs/tut/control-flow/1-intro.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# show()
|
||||
1
docs/tut/control-flow/3-switch.md
Normal file
1
docs/tut/control-flow/3-switch.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# switch()
|
||||
1
docs/tut/control-flow/4-indexes.md
Normal file
1
docs/tut/control-flow/4-indexes.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# indexes()
|
||||
1
docs/tut/control-flow/5-values.md
Normal file
1
docs/tut/control-flow/5-values.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# values()
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# Control Flow
|
||||
|
||||
Vide has specific functions for dealing with sources that store a table value.
|
||||
|
||||
Often, you will have a table of values that will be displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, Vide provides functions `indexes()` and `values()` to do this for
|
||||
you.
|
||||
|
||||
`indexes()` maps each *index* in a table to a UI element.
|
||||
|
||||
```lua
|
||||
local names = source { "a", "b", "c" }
|
||||
|
||||
local elements = indexes(names, function(name, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return "Name: " .. name()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
What happens here is the given callback is only ever ran *once* for each index
|
||||
in the table. The callback receives two arguments, a *source* containing the
|
||||
index's value and then the index itself.
|
||||
|
||||
Anytime the value at a corresponding index changes, the source for that index
|
||||
value is updated, causing the UI element depending on it to update too.
|
||||
|
||||
`values()` behaves similarly, except it maps each *value* in a table to a UI
|
||||
element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
Name: string,
|
||||
Icon: number
|
||||
}
|
||||
|
||||
local items = source({} :: Array<Item>)
|
||||
|
||||
local elements = values(items, function(item, i)
|
||||
return create "ImageLabel" {
|
||||
Image = "rbxassetid://" .. item.Icon,
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
The callback is again only ever ran *once* for each value in the table. The
|
||||
callback receives two arguments, a value in the table and then a *source*
|
||||
containing the value's corresponding index.
|
||||
|
||||
Any time a value in a table changes index, the source for that value is updated,
|
||||
causing the UI element position to change.
|
||||
|
||||
In certain cases `values()` can cause less recalculation and rerenders than
|
||||
`indexes()` like when items are re-arranged and shifted within a table.
|
||||
|
||||
It is important that each value in a table is unique when using `values()`,
|
||||
and for this reason always using `indexes()` if a table contains primitive
|
||||
values.
|
||||
|
||||
Both `indexes()` and `values()` return an array of all mapped UI elements.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# Control Flow
|
||||
|
||||
Vide has specific functions for dealing with sources that store a table value.
|
||||
|
||||
Often, you will have a table of values that will be displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, Vide provides functions `indexes()` and `values()` to do this for
|
||||
you.
|
||||
|
||||
`indexes()` maps each *index* in a table to a UI element.
|
||||
|
||||
```lua
|
||||
local names = source { "a", "b", "c" }
|
||||
|
||||
local elements = indexes(names, function(name, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return "Name: " .. name()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
What happens here is the given callback is only ever ran *once* for each index
|
||||
in the table. The callback receives two arguments, a *source* containing the
|
||||
index's value and then the index itself.
|
||||
|
||||
Anytime the value at a corresponding index changes, the source for that index
|
||||
value is updated, causing the UI element depending on it to update too.
|
||||
|
||||
`values()` behaves similarly, except it maps each *value* in a table to a UI
|
||||
element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
Name: string,
|
||||
Icon: number
|
||||
}
|
||||
|
||||
local items = source({} :: Array<Item>)
|
||||
|
||||
local elements = values(items, function(item, i)
|
||||
return create "ImageLabel" {
|
||||
Image = "rbxassetid://" .. item.Icon,
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
The callback is again only ever ran *once* for each value in the table. The
|
||||
callback receives two arguments, a value in the table and then a *source*
|
||||
containing the value's corresponding index.
|
||||
|
||||
Any time a value in a table changes index, the source for that value is updated,
|
||||
causing the UI element position to change.
|
||||
|
||||
In certain cases `values()` can cause less recalculation and rerenders than
|
||||
`indexes()` like when items are re-arranged and shifted within a table.
|
||||
|
||||
It is important that each value in a table is unique when using `values()`,
|
||||
and for this reason always using `indexes()` if a table contains primitive
|
||||
values.
|
||||
|
||||
Both `indexes()` and `values()` return an array of all mapped UI elements.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# Control Flow
|
||||
|
||||
Vide has specific functions for dealing with sources that store a table value.
|
||||
|
||||
Often, you will have a table of values that will be displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, Vide provides functions `indexes()` and `values()` to do this for
|
||||
you.
|
||||
|
||||
`indexes()` maps each *index* in a table to a UI element.
|
||||
|
||||
```lua
|
||||
local names = source { "a", "b", "c" }
|
||||
|
||||
local elements = indexes(names, function(name, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return "Name: " .. name()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
What happens here is the given callback is only ever ran *once* for each index
|
||||
in the table. The callback receives two arguments, a *source* containing the
|
||||
index's value and then the index itself.
|
||||
|
||||
Anytime the value at a corresponding index changes, the source for that index
|
||||
value is updated, causing the UI element depending on it to update too.
|
||||
|
||||
`values()` behaves similarly, except it maps each *value* in a table to a UI
|
||||
element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
Name: string,
|
||||
Icon: number
|
||||
}
|
||||
|
||||
local items = source({} :: Array<Item>)
|
||||
|
||||
local elements = values(items, function(item, i)
|
||||
return create "ImageLabel" {
|
||||
Image = "rbxassetid://" .. item.Icon,
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
The callback is again only ever ran *once* for each value in the table. The
|
||||
callback receives two arguments, a value in the table and then a *source*
|
||||
containing the value's corresponding index.
|
||||
|
||||
Any time a value in a table changes index, the source for that value is updated,
|
||||
causing the UI element position to change.
|
||||
|
||||
In certain cases `values()` can cause less recalculation and rerenders than
|
||||
`indexes()` like when items are re-arranged and shifted within a table.
|
||||
|
||||
It is important that each value in a table is unique when using `values()`,
|
||||
and for this reason always using `indexes()` if a table contains primitive
|
||||
values.
|
||||
|
||||
Both `indexes()` and `values()` return an array of all mapped UI elements.
|
||||
|
|
@ -1,47 +1,25 @@
|
|||
# Introduction
|
||||
|
||||
This is a brief tutorial designed to give you a quick run through the usage of
|
||||
Vide.
|
||||
This is a tutorial that introduces the concepts and usage of Vide.
|
||||
|
||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
This tutorial assumes familiarity with Luau and Roblox UI.
|
||||
|
||||
## Why Vide?
|
||||
|
||||
Creating UI is a slow and tedious process. The purpose of Vide is to make UI
|
||||
declarative and concise, making it faster to create and more importantly easier
|
||||
to maintain. Vide achieves this using a reactive style of programming which
|
||||
allows you to focus on the flow of data through your application without
|
||||
worrying about manually updating UI instances.
|
||||
Creating UI is complicated, slow, and tedious.
|
||||
|
||||
Vide tries to simplify and speed up this process by providing a declarative and
|
||||
reactive of style programming, which lets you focus more on designing the UI
|
||||
itself and not having to manually update or reparent UI instances.
|
||||
|
||||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
- Concise syntax to reduce verbosity as much as possible.
|
||||
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
|
||||
and semantics.
|
||||
- Being completely typecheckable.
|
||||
- Flexibility with integrating other libraries and allowing users to use their
|
||||
own patterns.
|
||||
- Independence from instance lifetimes.
|
||||
- A powerful reactive system that can update specific properties as a result of
|
||||
state changes, updates are immediate with no diffing needed.
|
||||
- Minimal syntax.
|
||||
- Complete typechecking
|
||||
- Independence from instances.
|
||||
|
||||
## Structure Of A Vide App
|
||||
|
||||
The entry point for all Vide apps is the `mount()` function. This function
|
||||
sets up Vide's reactivity system. It takes and calls a function that should
|
||||
create your entire app, and will apply its result to a target.
|
||||
|
||||
In Vide, your app should be composed of functions, each function creates a
|
||||
specific part of your app, and can be reused if needed. These functions are
|
||||
called *components*.
|
||||
|
||||
```lua
|
||||
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "hi" }
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
As with most declarative libraries, there is an initial learning curve to
|
||||
understand the concepts and usage. This tutorial tries to comprehensively
|
||||
cover these concepts and usage, more so than you need just to use it.
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
# Actions
|
||||
|
||||
Actions in Vide are special callbacks that you can pass along with properties,
|
||||
which will be called when those properties are being processed with the instance
|
||||
being assigned to, allowing you to run custom code.
|
||||
|
||||
```lua
|
||||
local action = vide.action
|
||||
```
|
||||
|
||||
```lua
|
||||
create "TextLabel" {
|
||||
Text = "test",
|
||||
|
||||
action(function(instance)
|
||||
print(instance.Text)
|
||||
end)
|
||||
}
|
||||
|
||||
-- will print "test"
|
||||
```
|
||||
|
||||
Actions can be wrapped with functions to re-use specific behaviors. Below is
|
||||
an example of an action used to listen for property changes:
|
||||
|
||||
```lua
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local output = source ""
|
||||
|
||||
create "TextBox" {
|
||||
changed("Text", output)
|
||||
}
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
changed externally.
|
||||
51
docs/tut/crash-course/10-cleanup.md
Normal file
51
docs/tut/crash-course/10-cleanup.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Cleanup
|
||||
|
||||
Sometimes you may need to do some cleanup when destroying a component or after
|
||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||
is used to queue a cleanup callback for the next time a reactive scope is rerun
|
||||
or destroyed.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. math.floor(count())
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local unmount = mount(Timer)
|
||||
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the reactive scope responsible for creating the timer component is
|
||||
destroyed, such as when it is unmounted.
|
||||
|
||||
::: tip
|
||||
Roblox instances do not need to be explicitly destroyed for their
|
||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
||||
need to use `cleanup()` to destroy instances. However, be wary of connecting
|
||||
a function that references an instance to an event from the same instance,
|
||||
this causes the instance to reference itself and never be freed. In such a case
|
||||
you would need to use `cleanup()` to disconnect this connection or to explicitly
|
||||
destroy the instance.
|
||||
:::
|
||||
202
docs/tut/crash-course/11-control-flow.md
Normal file
202
docs/tut/crash-course/11-control-flow.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Control Flow
|
||||
|
||||
Eventually you may need a way to dynamically create and destroy UI elements
|
||||
resulting from source updates. Vide provides functions to help you do this,
|
||||
known as *control flow* functions.
|
||||
|
||||
These functions return new sources, which hold the instances to be displayed.
|
||||
|
||||
Control flow functions run their components in a new reactive scope, which can
|
||||
be destroyed independently of the reactive scope that called the control flow
|
||||
function. This means parts of your app can be independently created and
|
||||
destroyed.
|
||||
|
||||
## switch()
|
||||
|
||||
`switch()` condtionally displays one instance at a time. It uses a table to map
|
||||
a source value to a component.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
||||
local function Button(props: {
|
||||
Text: string,
|
||||
Activated: () -> ()
|
||||
})
|
||||
local hovered = source(false)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = props.Text,
|
||||
Activated = props.Activated,
|
||||
|
||||
TextColor3 = function()
|
||||
return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7)
|
||||
end,
|
||||
|
||||
MouseEnter = function() hovered(true) end,
|
||||
MouseLeave = function() hovered(false) end
|
||||
}
|
||||
end
|
||||
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Text = "Join",
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
local function LeaveButton()
|
||||
return Button {
|
||||
Text = "Leave"
|
||||
Activated = function() joined(false) end
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
switch(joined) {
|
||||
[true] = LeaveButton,
|
||||
[false] = JoinButton
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root["root scope"]
|
||||
direction LR
|
||||
joined --> switch -.- subroot
|
||||
|
||||
subgraph subroot["switch scope"]
|
||||
direction LR
|
||||
effect["TextColor3 effect"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A `switch()` call creates a new effect and a new scope as seen in the above
|
||||
graph. Whenever `menu` updates, it causes the `switch` effect to run, which
|
||||
will destroy and recreate the switch scope with the new component.
|
||||
|
||||
This will also destroy the internal effect that the button uses to highlight
|
||||
itself when it is hovered, each time the switch is rerun.
|
||||
|
||||
## indexes()
|
||||
|
||||
Often, you will have a table of values with each value displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, `indexes()` allows you to create elements each corresponding to a
|
||||
table index, to display the value at that index.
|
||||
|
||||
```lua
|
||||
local todoList = source {
|
||||
"finish the crash course",
|
||||
"star vide's GitHub"
|
||||
}
|
||||
|
||||
local function TodoList(props: { list: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(todoList, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
TodoList { list = todoList }
|
||||
```
|
||||
|
||||
For each index in the given source table, the given function will be called
|
||||
with:
|
||||
|
||||
1. a source containing the value of the index
|
||||
2. the index itself
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
given source for that index is updated.
|
||||
|
||||
Any time the input source table is updated, the given function will be ran for
|
||||
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||
value), will have its corresponding reactive scope destroyed to clean up that
|
||||
element.
|
||||
|
||||
`indexes()` is said to *map* each table index to a new UI element that can
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root ["root scope"]
|
||||
direction LR
|
||||
todoList --> indexes -.- subroot1 & subroot2
|
||||
|
||||
subgraph subroot1 ["indexes scope 1"]
|
||||
direction LR
|
||||
value1[todo] --> prop1["prop binding"]
|
||||
end
|
||||
|
||||
subgraph subroot2 ["indexes scope 2"]
|
||||
direction LR
|
||||
value2[todo] --> prop2[prop binding]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
One thing to note regarding table sources, is that when you edit a table in a
|
||||
source, you must set that table again to actually update the source.
|
||||
|
||||
```lua
|
||||
local src = source { 1, 2 }
|
||||
local data = src()
|
||||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
||||
Together, these control flow functions cover the majority of cases where you
|
||||
need to dynamically create and destroy parts of your UI.
|
||||
|
||||
If you need to do something that these control flow functions cannot, you can
|
||||
always use `mount()` within an effect to dynamically create and destroy
|
||||
components on your own terms. Just remember to use `cleanup()` to unmount when
|
||||
the effect reruns.
|
||||
|
|
@ -27,7 +27,7 @@ function Menu(props: {
|
|||
Size: UDim2
|
||||
})
|
||||
return Background {
|
||||
Color = props.COlor,
|
||||
Color = props.Color,
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
|
|
@ -77,7 +77,8 @@ be parented.
|
|||
|
||||
```lua
|
||||
type Children = {
|
||||
Children = Array<Instance>
|
||||
-- also can optionally pass a source that returns an array of children too
|
||||
Children = Array<Instance> | () -> Array<Instance>
|
||||
}
|
||||
|
||||
local function List(props: Children & Layout)
|
||||
|
|
@ -107,8 +108,8 @@ properties, this can be used to create overridable default properties.
|
|||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
|
||||
props.Layout,
|
||||
|
||||
-- can be overriden by `props.Layout`
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
53
docs/tut/crash-course/13-actions.md
Normal file
53
docs/tut/crash-course/13-actions.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Actions
|
||||
|
||||
Actions in Vide are special callbacks that you can pass along with properties,
|
||||
to run some code on an instance receiving them.
|
||||
|
||||
```lua
|
||||
local action = vide.action
|
||||
```
|
||||
|
||||
```lua
|
||||
create "TextLabel" {
|
||||
Text = "test",
|
||||
|
||||
action(function(instance)
|
||||
print(instance.Text)
|
||||
end)
|
||||
}
|
||||
|
||||
-- will print "test"
|
||||
```
|
||||
|
||||
Actions can be wrapped with functions for reuse. Below is an example of an
|
||||
action used to listen for property changes:
|
||||
|
||||
```lua
|
||||
local action = vide.action
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function changed(prop: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
-- remember to clean up the connection when the reactive scope the action
|
||||
-- is ran in is destroyed, so the instance can be garbage collected
|
||||
cleanup(connection)
|
||||
end)
|
||||
end
|
||||
|
||||
local output = source ""
|
||||
|
||||
local instance = create "TextBox" {
|
||||
changed("Text", output)
|
||||
}
|
||||
|
||||
instance.Text = "foo"
|
||||
|
||||
print(output()) -- "foo"
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
changed externally.
|
||||
35
docs/tut/crash-course/14-strict-mode.md
Normal file
35
docs/tut/crash-course/14-strict-mode.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Strict Mode
|
||||
|
||||
While developing UI with Vide, you should use Vide's strict mode, which can
|
||||
be set with `vide.strict = true` once when you first require Vide. Strict mode
|
||||
will add extra safety checks and emit better error traces, particularly when
|
||||
errors occur in property bindings.
|
||||
|
||||
Strict mode is automatically enabled when Vide is required in O0 or O1
|
||||
optimization (default studio level). You can `vide.strict = false` if you do not
|
||||
want this.
|
||||
|
||||
Strict mode will run derived sources and effects twice each time they update.
|
||||
This is to help ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
vide.strict = true
|
||||
|
||||
local count = source(0)
|
||||
|
||||
local ran = 0
|
||||
effect(function()
|
||||
ran += 1
|
||||
end)
|
||||
|
||||
print(ran) -- 2
|
||||
count(1)
|
||||
print(ran) -- 4
|
||||
```
|
||||
|
||||
A full list of what strict mode will do can be found
|
||||
[here](../../api/strict-mode).
|
||||
123
docs/tut/crash-course/15-concepts.md
Normal file
123
docs/tut/crash-course/15-concepts.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Concepts Summary
|
||||
|
||||
A summary of all the concepts covered during the crash course.
|
||||
|
||||
## Source
|
||||
|
||||
A source of data.
|
||||
|
||||
Stores a single value that can be updated.
|
||||
|
||||
Created with `source()`.
|
||||
|
||||
# Derived Source
|
||||
|
||||
A new source composed of other sources.
|
||||
|
||||
Created with a plain function or with `derive()`.
|
||||
|
||||
## Effect
|
||||
|
||||
Anything that happens in response to a source update.
|
||||
|
||||
Created with `effect()`.
|
||||
|
||||
## Reactive Scope
|
||||
|
||||
A scope created by certain functions such as:
|
||||
|
||||
- `root()`
|
||||
- `effect()`
|
||||
- `derive()`
|
||||
|
||||
Reactive scopes can:
|
||||
|
||||
- track sources that are read from within.
|
||||
- rerun when a tracked source updates.
|
||||
- track new reactive scopes created from within.
|
||||
|
||||
## Scope Owners
|
||||
|
||||
A reactive scope created within another reactive scope is *owned* by the other
|
||||
reactive scope, with the exception of the reactive scope created by `root()`.
|
||||
|
||||
When a reactive scope is rerun or destroyed, all reactive scopes owned by it are
|
||||
automatically destroyed.
|
||||
|
||||
`root()`, which `mount()` uses internally, creates a reactive scope with no
|
||||
owner, since it must be destroyed manually using a destructor
|
||||
returned.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Arbitrary code to run whenever a reactive scope is rerun or destroyed.
|
||||
|
||||
Queue a function to run using `cleanup()`.
|
||||
|
||||
## Tracking
|
||||
|
||||
Sources read from within a reactive scope will be tracked. This can be disabled
|
||||
using `untrack()`, which will make reactive scopes temporarily ignore sources
|
||||
read.
|
||||
|
||||
The reactive scope created by `root()` is non-tracking by default.
|
||||
|
||||
As a guard against misusage, a reactive scope cannot be created within a
|
||||
reactive scope, unless it is made non-tracking using `untrack()`.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
The combination of reactive scopes can viewed graphically, called a
|
||||
*reactive graph*. This can be a more intuitive way to think of the
|
||||
relationships between effects and the sources they depend on.
|
||||
|
||||
### Code
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local text = derive(function()
|
||||
return "count: " .. text()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
print(text())
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
### Graph resulting from code
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
|
||||
subgraph root
|
||||
text --> effect
|
||||
end
|
||||
|
||||
count --> text
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Since `count` is a source, not an effect, it can exist
|
||||
outside of a root reactive scope.
|
||||
- An update to `count` will cause `text` to rerun, which
|
||||
then causes `effect` to rerun.
|
||||
- When the root reactive scope is destroyed, `text` and
|
||||
`effect` will be destroyed alongside it, since they are
|
||||
owned by it. `count` will be untouched and future updates
|
||||
to `count` will have no effect.
|
||||
|
|
@ -6,14 +6,11 @@ Instances are created using `create()`.
|
|||
properties to assign when creating a new instance for that class.
|
||||
|
||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||
table literals which Vide takes advantage of for brevity.
|
||||
table literals which is recommended to use for brevity.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "Frame" {
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
|
@ -37,22 +34,15 @@ local function App()
|
|||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
Assign a value to a string key to set a property, and assign a value to a
|
||||
number key to set a child. Events can be connected to by assigning a function
|
||||
to a string key.
|
||||
|
||||
You can also use a shorthand to create datatypes instead of explicitly typing
|
||||
out the class name and constructor. The table will be unpacked into the `.new()`
|
||||
constructor of the property's type.
|
||||
|
||||
```lua
|
||||
create "Frame" {
|
||||
AnchorPoint = { 0.5, 1 },
|
||||
UDim2 = { 0.5, 0, 0.5, 0 }
|
||||
}
|
||||
```
|
||||
::: warning
|
||||
When creating an instance with no properties, it is important to not forget to
|
||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||
that class, not an instance of that class.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
# Components
|
||||
|
||||
Components are custom-made reusable pieces of UI made from other pieces of UI.
|
||||
Vide encourages separating different parts of your UI into functions called
|
||||
*components*.
|
||||
|
||||
By using components you can make your application more modular and better
|
||||
organized.
|
||||
A component is a function that creates and returns a piece of UI.
|
||||
|
||||
This is a way to separate your UI into small chunks that you can reuse and put
|
||||
together.
|
||||
|
||||
::: code-group
|
||||
|
||||
```lua [Button.luau]
|
||||
local vide = require(vide)
|
||||
local create = vide.create
|
||||
|
||||
local function Button(props: {
|
||||
|
|
@ -16,11 +20,14 @@ local function Button(props: {
|
|||
})
|
||||
return create "TextButton" {
|
||||
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
Size = UDim2.fromOffset(200, 150),
|
||||
|
||||
Position = props.Position,
|
||||
Text = props.Text,
|
||||
Activated = props.Activated
|
||||
Activated = props.Activated,
|
||||
|
||||
create "UICorner" {}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -28,8 +35,6 @@ return Button
|
|||
```
|
||||
|
||||
```lua [App.luau]
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
|
@ -38,29 +43,35 @@ local function App()
|
|||
return create "ScreenGui" {
|
||||
Button {
|
||||
Position = UDim2.fromOffset(200, 200),
|
||||
Text = "click me!",
|
||||
|
||||
Text = "back",
|
||||
Activated = function()
|
||||
print "clicked"
|
||||
print "go to previous page"
|
||||
end
|
||||
},
|
||||
|
||||
Button {
|
||||
Position = UDim2.fromOffset(400, 200),
|
||||
Text = "next",
|
||||
Activated = function()
|
||||
print "go to next page"
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
App().Parent = game.StarterGui
|
||||
```
|
||||
|
||||
Above is a simple example of a button component with a set color and size,
|
||||
being reused across files.
|
||||
:::
|
||||
|
||||
Above is a simple example of a button component being used across files.
|
||||
|
||||
A single parameter `props` is used to pass properties to the component.
|
||||
|
||||
Components allow you to *encapsulate* behavior. You can only modify the
|
||||
component in ways that you allow in the component.
|
||||
You can only modify the component in ways that you allow in the component,
|
||||
through the `props` parameter.
|
||||
|
||||
This also promotes code reusability. Anytime you want a new button all you do
|
||||
is call `Button {}` instead of creating and setting every property each time.
|
||||
When changing the button in future, any changes to the button file will be
|
||||
reflected anywhere the button is used throughout your app.
|
||||
|
||||
This can be extended to much more complicated UI.
|
||||
To create a new button all you must do is call the `Button` function, passing in
|
||||
values. This saves having to create and set every property each time. Also, when
|
||||
updating the button component in future, any changes to the button file will be
|
||||
seen anywhere the button is used in your app.
|
||||
|
|
|
|||
|
|
@ -1,31 +1,14 @@
|
|||
# Source
|
||||
# Sources
|
||||
|
||||
*Sources* in Vide are special objects that store a single value. They are the
|
||||
core of reactivity in Vide, as updates to a source can automatically update
|
||||
properties or other sources depending on that source.
|
||||
Sources are special objects that store a single value. They are the core of
|
||||
Vide's reactivity. They are called sources because they act as sources of data.
|
||||
|
||||
A source in Vide can be created using `source()`.
|
||||
A source can be created using `source()`.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = count,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
The value passed to `source()` is the initial value of the source.
|
||||
|
|
@ -37,15 +20,22 @@ by calling it with no arguments.
|
|||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Each call of `Counter {}` will create a new counter, each maintaining their
|
||||
own count.
|
||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||
effectively becomes a new source.
|
||||
|
||||
When you assign a function to a non-event property, Vide will immediately run it
|
||||
and check what sources were read from. When updating those sources again after,
|
||||
this function will be re-ran and its return value applied to the property.
|
||||
This is known as *binding* properties.
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
This allows you as the programmer to not need to manually update UI as the state
|
||||
of your program changes. You just define how the data maps to UI, and Vide's
|
||||
reactive system will automatically update any properties depending on sources
|
||||
that are updated.
|
||||
local text = function()
|
||||
return "count: " .. tostring(count())
|
||||
end
|
||||
|
||||
print(text()) -- "count: 0"
|
||||
count(1)
|
||||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
Sources on their own aren't very special, the above can be achieved with plain
|
||||
variables. The real use for sources become apparent when used in combination
|
||||
with *effects*. Similar to a signal and connection, a source and effect allows
|
||||
you to do things like automatically updating UI when a source is updated.
|
||||
|
|
|
|||
|
|
@ -1,45 +1,51 @@
|
|||
# Effect
|
||||
# Effects
|
||||
|
||||
An effect is a function that is run anytime a source updates. They are called
|
||||
effects because they can produce side-effects when reacting to source changes.
|
||||
Effects are functions that are ran in response to source updates. They are
|
||||
called effects because they cause *side-effects* when reacting to source
|
||||
updates.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print("count has updated to: " .. count())
|
||||
print("count: " .. count())
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = count,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
-- "count: 0" printed
|
||||
count(1)
|
||||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
This will print to the terminal anytime the count is changed.
|
||||
The callback given to `effect()` is ran immediately in a *reactive scope*. Any
|
||||
source read from inside a reactive scope will be tracked, so when any of those
|
||||
sources update, the effect will be reran too.
|
||||
|
||||
`effect()` creates an explicit side-effect. There are other side-effects in the
|
||||
above code sample. The setting of `Text = count` creates another side-effect;
|
||||
the updating of the Text property anytime the count is changed.
|
||||
Reactive scopes also track derived sources, it doesn't matter how deeply nested
|
||||
inside a function a source is.
|
||||
|
||||
All observable changes to the user are considered to be side-effects of the
|
||||
reactive system.
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
You should not update other sources using an effect. Improper usage can lead to
|
||||
unecessary updates and infinite loops.
|
||||
local count = source(1)
|
||||
|
||||
local doubled = function()
|
||||
return count() * 2
|
||||
end
|
||||
|
||||
effect(function()
|
||||
print("doubled count: " .. doubled())
|
||||
end)
|
||||
|
||||
-- "doubled count: 2" printed
|
||||
count(2)
|
||||
-- "doubled count: 4" printed
|
||||
```
|
||||
|
||||
If a source is updated with the same value it already had, it will not rerun
|
||||
effects depending on it.
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
# Derived Source
|
||||
|
||||
You can create new sources from existing sources. This is known as *deriving
|
||||
sources*.
|
||||
|
||||
A function that wraps a source effectively becomes a new source. If a source
|
||||
used inside a function is updated, the whole function can be re-ran to recompute
|
||||
its value.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local function doubled()
|
||||
return count() * 2
|
||||
end
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = doubled,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
Now the counter will increment in 2s each time it is clicked.
|
||||
|
||||
Sometimes when using expensive computations to derive state, you only want to
|
||||
recalculate it once when a source state has changed. Although not needed in
|
||||
most cases, you can use `derive()` to create a new source that will cache its
|
||||
value, only recomputing when an input source has changed.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local factorial = derive(function()
|
||||
local n = 1
|
||||
for i = 2, count() do
|
||||
n *= i
|
||||
end
|
||||
return n
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return factorial() + factorial() + factorial()
|
||||
end,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
This can improve performance in cases where a source is read from multiple times
|
||||
between recalculations. In the above example, the factorial is only ever
|
||||
calculated once each time the count changes.
|
||||
80
docs/tut/crash-course/6-root.md
Normal file
80
docs/tut/crash-course/6-root.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Root Reactive Scopes
|
||||
|
||||
Reactive scopes cannot be created on their own - they must be created within
|
||||
another reactive scope so that it can be tracked and later destroyed when it is
|
||||
no longer needed.
|
||||
|
||||
This is the purpose of `mount()`, which creates an initial "root", or
|
||||
"top-level" reactive scope, which all other reactive scopes, such as
|
||||
ones created by `effect()`, can stem from.
|
||||
|
||||
When this root reactive scope is destroyed, it will ensure all other reactive
|
||||
scopes created within it are also destroyed, ensuring everything is cleaned up
|
||||
properly.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function App()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
App() -- will error since effect() was not called within a reactive scope
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
```
|
||||
|
||||
Mounting returns a function that when called will destroy its reactive scope,
|
||||
along with any other reactive scopes created inside it.
|
||||
|
||||
```lua
|
||||
local unmount = mount(App)
|
||||
|
||||
unmount()
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
||||
The reactive graph for the above example looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> effect
|
||||
end
|
||||
```
|
||||
|
||||
When the root reactive scope created by `mount()` is destroyed, the `effect`
|
||||
scope will also be destroyed since it was created within it.
|
||||
|
||||
This is important because you may have an effect that updates the property of a
|
||||
UI instance, meaning the effect is referencing and holding that instance in
|
||||
memory. The effect being destroyed will remove this reference, allowing the
|
||||
instance to be garbage collected.
|
||||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
root reactive scope, since you should be creating all your UI and corresponding
|
||||
effects within a top-level `mount()` call that puts all your UI together. So it
|
||||
is safe to assume that any effect you create will be created under this top
|
||||
level scope. Vide will prevent you from accidently doing otherwise anyways.
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Cleanup
|
||||
|
||||
Sometimes you may need to do some cleanup when destroying a component or after
|
||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||
is used to register a cleanup callback for the next time the reactive scope
|
||||
it is called in re-runs.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. count()
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Timer {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the timer component is destroyed, whether that is from unmounting the app
|
||||
or if it is dynamically created by a control-flow function, which will be
|
||||
covered next.
|
||||
75
docs/tut/crash-course/7-stateful-component.md
Normal file
75
docs/tut/crash-course/7-stateful-component.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Stateful Components
|
||||
|
||||
A stateful component is a component that stores some data internally.
|
||||
|
||||
Stateful components in Vide are created using sources and effects - sources to
|
||||
store the data, and effects to display the data.
|
||||
|
||||
## Internal State
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
end)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
mount(Counter, game.StarterGui)
|
||||
```
|
||||
|
||||
Above is an example of a counter component, that when clicked, will increment
|
||||
its internal count, and automatically update its text to reflect that count.
|
||||
|
||||
Each instance of `Counter()` will maintain its own independent count, since the
|
||||
count source is created inside the component.
|
||||
|
||||
We use `mount()` to create the counter within a reactive scope, which also takes
|
||||
a second argument to parent the counter to another instance.
|
||||
|
||||
## External State
|
||||
|
||||
External sources can also be passed into components for them to use.
|
||||
|
||||
```lua
|
||||
local function Counter(props: { count: () -> number })
|
||||
local count = props.count
|
||||
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
end)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
local count = source(0)
|
||||
|
||||
Counter {
|
||||
count = count
|
||||
}
|
||||
|
||||
count(1) -- the Counter component will update to display this count
|
||||
```
|
||||
|
||||
Sources can be created internally or passed in from externally, there are no
|
||||
restrictions on how they are used as long as the effect using it is created
|
||||
within a reactive scope.
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# Control Flow
|
||||
|
||||
Eventually you will need a way to dynamically create and destroy UI elements
|
||||
resulting from state changes. Vide provides functions to help you do this,
|
||||
known as *control flow* functions.
|
||||
|
||||
These functions return a new source, which holds the instances to be displayed.
|
||||
These sources can be assigned as children, meaning the displayed children
|
||||
will update when the input source updates.
|
||||
|
||||
One of these functions is `switch()`, used to conditionally show one of a set of
|
||||
components.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
||||
local function ToggleButton(p: {
|
||||
Text: string,
|
||||
Toggle: (boolean) -> boolean
|
||||
})
|
||||
return create "TextButton" {
|
||||
Size = UDim2.fromOffset(300, 300),
|
||||
Text = p.Text,
|
||||
Activated = function()
|
||||
p.Toggle(not p.Toggle())
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local loggedIn = source(false)
|
||||
|
||||
local function LoginMenu()
|
||||
return Frame {
|
||||
switch(loggedIn) {
|
||||
[true] = function()
|
||||
return ToggleButton { Text = "Log out", Toggle = loggedIn }
|
||||
end,
|
||||
|
||||
[false] = function()
|
||||
return ToggleButton { Text = "Log in", Toggle = loggedIn }
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { LoginMenu {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
Above is an example of using a switch to create a login menu. Each time
|
||||
`loggedIn` toggles, the current button will be destroyed, and a new button
|
||||
created, which the text to represent the current action, to log in or log out.
|
||||
|
||||
Another control flow function, `indexes()`, is used to create elements from an
|
||||
input table.
|
||||
|
||||
Often, you will have a table of values that will be displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, `indexes()` can autmatically run a transform function for each
|
||||
index and value, generating a UI element.
|
||||
|
||||
```lua
|
||||
local todoList = {
|
||||
"Finish the crash course",
|
||||
"Star vide's GitHub"
|
||||
}
|
||||
|
||||
local elements = indexes(todoList, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
|
||||
mount(function()
|
||||
return create "ScreenGui" {
|
||||
create "UIListLayout" {}, elements
|
||||
}
|
||||
end, game.StarterGui)
|
||||
```
|
||||
|
||||
For each unique index in the passed table, the transform function will be called
|
||||
with 1. a source containing the value of the index, 2. the index itself.
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
given source is updated instead.
|
||||
|
||||
`indexes()` is said to map each *index* in a table to a UI element, each index
|
||||
has a single corresponding element.
|
||||
|
||||
An element is only destroyed if the value of an index is set to `nil`.
|
||||
66
docs/tut/crash-course/8-property-binding.md
Normal file
66
docs/tut/crash-course/8-property-binding.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Property Binding
|
||||
|
||||
Explicitly creating effects to update properties can be tedious. Vide provides a
|
||||
way to *implicitly* create an effect to update properties.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end,
|
||||
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
This example is equivalent to the example seen on the previous page.
|
||||
|
||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||
function will implicitly create an effect to update that property anytime a
|
||||
source used within is updated.
|
||||
|
||||
Just like effects, the function is ran immediately in a reactive scope to set
|
||||
the property initially and determine what sources are being used.
|
||||
|
||||
This allows you as the programmer to not need to manually update UI as the state
|
||||
of your program changes. You just define how data sources map to UI, and Vide's
|
||||
reactive system will automatically update any properties depending on those
|
||||
sources.
|
||||
|
||||
## Children Binding
|
||||
|
||||
Children can also be set in a similar manner. A source passed as a child (passed
|
||||
with a number key instead of string key) can return an instance or an array of
|
||||
instances. Vide will automatically unparent removed instances and parent new
|
||||
instances when that source's stored instances change.
|
||||
|
||||
```lua
|
||||
local items = source {
|
||||
create "TextLabel" { Text = "A" }
|
||||
}
|
||||
|
||||
local function List(props: { children: () -> { Instance } })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
props.children
|
||||
}
|
||||
end
|
||||
|
||||
local list = List { children = items } -- creates a list with a single text label "A"
|
||||
|
||||
items {
|
||||
create "TextLabel" { Text = "B" },
|
||||
create "TextLabel" { Text = "C" }
|
||||
}
|
||||
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C".
|
||||
```
|
||||
92
docs/tut/crash-course/9-derived-source.md
Normal file
92
docs/tut/crash-course/9-derived-source.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Derived Sources
|
||||
|
||||
We have seen the basic way to derive a source:
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
return "count: " .. tostring(count())
|
||||
end
|
||||
|
||||
print(text()) -- "count: 0"
|
||||
count(1)
|
||||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
However, in some cases where this source could be used by multiple effects at
|
||||
the same time, the function wrapping the source will needlessly rerun to convert
|
||||
the count into a string for each effect using it.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
print "ran"
|
||||
return "count: " .. tostring(count())
|
||||
end
|
||||
|
||||
effect(function() text() end)
|
||||
effect(function() text() end)
|
||||
|
||||
source(1) -- prints "ran" x2
|
||||
```
|
||||
|
||||
To avoid this, you can use `derive()` to derive a new source instead. This will
|
||||
run a callback in a new reactive scope only when a dependent source has updated.
|
||||
Reading this derived source multiple times will just return a cached result from
|
||||
when it last updated.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(0)
|
||||
|
||||
local text = derive(function()
|
||||
print "ran"
|
||||
return "count: " .. tostring(count())
|
||||
end)
|
||||
|
||||
effect(function() text() end)
|
||||
effect(function() text() end)
|
||||
|
||||
source(1) -- prints "ran" x1
|
||||
```
|
||||
|
||||
`derive()` must also be called within a reactive scope, just like `effect()`.
|
||||
|
||||
If the recalculated value is the same as the old value, the derived source will
|
||||
not rerun the effects using it.
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> text --> effect1 & effect2
|
||||
end
|
||||
```
|
||||
|
||||
Deriving a source in this manner is similar to creating an effect to update
|
||||
another source. You should never manually do this using an effect however,
|
||||
improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another.
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# Reactive Scoping
|
||||
|
||||
This is a brief document designed to give the user more insight into how Vide's
|
||||
reactive graph works.
|
||||
|
||||
Each time you create and derive sources, a new node representing that source is
|
||||
created and added to the reactive graph. Each node stores a value and a
|
||||
side-effect function. Each node also keeps track of its parents and children,
|
||||
as well as any cleanups registered.
|
||||
|
||||
Any time a node is updated, Vide will traverse and update that node's children,
|
||||
its children's children, etc, until all nodes descending from that node has been
|
||||
updated. Traversal will stop at a node if that node's cached value does not
|
||||
change after an update.
|
||||
|
||||
For every node that is updated, a scope is opened for that node. These scopes
|
||||
are referred to as "reactive scopes". Any source read from within a node's scope
|
||||
will that node as a child. This is similar to cleanups, anytime a cleanup is
|
||||
registered, it is added to the node of the currently active scope.
|
||||
|
||||
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
|
||||
active reactive scope is the node at the top of this stack.
|
||||
|
||||
When destroying a node, its descendents are traversed and also destroyed.
|
||||
When being destroyed, a node's connections (parents and children) are cleared,
|
||||
and any pending cleanup functions are ran.
|
||||
|
||||
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
||||
the root node which will track any node created or derived inside its scope, or
|
||||
any cleanups registered. Without it, nodes could be garbage collected without a
|
||||
chance to run pending cleanups which can cause memory leakage.
|
||||
|
||||
Control flow functions in Vide are special, as they can dynamically create and
|
||||
destroy new root scopes.
|
||||
|
||||
It is the combination of the above which allows us to write components like so:
|
||||
|
||||
```lua
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local connection = stepped:Connect(function() count(count() + 1) end)
|
||||
|
||||
cleanup(function() connection:Disconnect() end)
|
||||
effect(function() print(count()) end)
|
||||
|
||||
return create "TextLabel" { Text = count }
|
||||
end
|
||||
```
|
||||
|
||||
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
|
||||
just sees this as a function that creates nodes in the reactive graph.
|
||||
|
||||
Whenever the reactive scope that calls this function is destroyed, like by a
|
||||
control flow function, the registered cleanup will be called, and the effect
|
||||
(which is just a node on the reactive graph) is destroyed. The returned instance
|
||||
and the bound `count` source is just considered to be a side-effect, and with
|
||||
the reactive scope from which the side-effects stem from destroyed, the instance
|
||||
can be garbage collected - everything is nicely cleaned up.
|
||||
|
||||
> todo: add graphics
|
||||
|
|
@ -10,14 +10,14 @@ local function is_action(v: any)
|
|||
end
|
||||
|
||||
local function action(callback: (Instance) -> (), priority: number?): Action
|
||||
local t = {
|
||||
local a = {
|
||||
priority = priority or 1,
|
||||
callback = callback
|
||||
}
|
||||
|
||||
setmetatable(t :: any, ActionMT)
|
||||
setmetatable(a :: any, ActionMT)
|
||||
|
||||
return table.freeze(t)
|
||||
return table.freeze(a)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
142
src/apply.luau
142
src/apply.luau
|
|
@ -13,41 +13,58 @@ type Node<T> = graph.Node<T>
|
|||
type Array<V> = { V }
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
-- buffer of event -> callback to connect after properties are set
|
||||
local event_buffer = {} :: Map<string, () -> ()>
|
||||
local free_caches: {
|
||||
-- event listeners to connect after properties are set
|
||||
events: Map<
|
||||
string, -- event name
|
||||
() -> () -- listener
|
||||
>,
|
||||
|
||||
-- buffer of priority -> callback to run after events are connected
|
||||
local action_buffers = {} :: Map<number, Array<(Instance) -> ()>>
|
||||
-- actions to run after events are connected
|
||||
actions: Map<
|
||||
number, -- priority
|
||||
Array<(Instance) -> ()> -- action callbacks
|
||||
>,
|
||||
|
||||
-- lazily create buffers on nil index
|
||||
setmetatable(action_buffers :: any, {
|
||||
__index = function(_, i: number)
|
||||
action_buffers[i] = {}
|
||||
return action_buffers[i]
|
||||
end
|
||||
})
|
||||
-- cache to detect duplicate property setting at same nesting depth
|
||||
nested_debug: Map<
|
||||
number, -- depth
|
||||
Map<string, true> -- set of property names
|
||||
>,
|
||||
|
||||
-- cache in strict mode to detect duplicate property set at same nesting level
|
||||
local nested_debug_cache = {} :: Map<number, Map<string, true>>
|
||||
|
||||
setmetatable(nested_debug_cache :: any, {
|
||||
__index = function(_, i: number)
|
||||
nested_debug_cache[i] = {}
|
||||
return nested_debug_cache[i]
|
||||
end
|
||||
})
|
||||
|
||||
-- use stack instead of recursive function to process nested layers one at time
|
||||
-- use stack instead of recursive function to process nesting layers one at time
|
||||
-- deeper-nested properties take precedence over shallower-nested ones
|
||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||
local nested_stack = {} :: { {} | number }
|
||||
nested_stack: { {} | number }
|
||||
}?
|
||||
|
||||
local function borrow_caches(): typeof(assert(free_caches))
|
||||
if free_caches then
|
||||
local caches = free_caches :: typeof(assert(free_caches))
|
||||
free_caches = nil
|
||||
return caches
|
||||
else
|
||||
return {
|
||||
events = {},
|
||||
actions = setmetatable({} :: any, { -- lazy init
|
||||
__index = function(self, i) self[i] = {}; return self[i] end
|
||||
}),
|
||||
nested_debug = setmetatable({} :: any, {
|
||||
__index = function(self, i: number) self[i] = {}; return self[i] end
|
||||
}),
|
||||
nested_stack = {}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function return_caches(caches: typeof(free_caches) )
|
||||
free_caches = caches
|
||||
end
|
||||
|
||||
-- todo: solution without manual updating of this table
|
||||
-- map of datatype names to class default constructor for aggregate init
|
||||
local aggregates = {}
|
||||
|
||||
for i, v in next, {
|
||||
for name, class in {
|
||||
CFrame = CFrame,
|
||||
Color3 = Color3,
|
||||
UDim = UDim,
|
||||
|
|
@ -55,27 +72,39 @@ for i, v in next, {
|
|||
Vector2 = Vector2,
|
||||
Vector3 = Vector3,
|
||||
Rect = Rect
|
||||
} do
|
||||
aggregates[i] = v.new
|
||||
} :: Map<string, { [string]: any }> do
|
||||
aggregates[name] = class.new
|
||||
end
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
if not properties then
|
||||
throw("attempt to call a constructor returned by create() with no properties")
|
||||
end
|
||||
|
||||
-- processes a potentially nested table of values to assign to an instance
|
||||
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
|
||||
local strict = flags.strict
|
||||
|
||||
table.clear(nested_stack)
|
||||
if strict then table.clear(nested_debug_cache) end
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
|
||||
local caches = borrow_caches()
|
||||
local events = caches.events
|
||||
local actions = caches.actions
|
||||
local nested_debug = caches.nested_debug
|
||||
local nested_stack = caches.nested_stack
|
||||
|
||||
-- process all properties
|
||||
local depth = 1
|
||||
|
||||
repeat
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if strict then -- check for duplicate prop assignment at nesting layer
|
||||
if nested_debug_cache[depth][property] then
|
||||
if strict then -- check for duplicate prop assignment at nesting depth
|
||||
if nested_debug[depth][property] then
|
||||
throw(`duplicate property {property} at depth {depth}`)
|
||||
end
|
||||
nested_debug_cache[depth][property] = true
|
||||
nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "table" then -- attempt aggregate init
|
||||
|
|
@ -86,7 +115,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
|
|||
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||
elseif type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
event_buffer[property] = value :: () -> () -- add event to buffer
|
||||
events[property] = value :: () -> () -- add event to buffer
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||
end
|
||||
|
|
@ -98,7 +127,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
|
|||
bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
else
|
||||
table.insert(nested_stack, value :: {})
|
||||
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||
|
|
@ -109,36 +138,17 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
|
|||
end
|
||||
end
|
||||
|
||||
-- pop next nested table off stack
|
||||
depth = table.remove(nested_stack) :: number
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
|
||||
until not properties
|
||||
|
||||
for event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
end
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
if parent then properties.Parent = nil end
|
||||
|
||||
-- reset buffers
|
||||
table.clear(event_buffer)
|
||||
for _, buffer in next, action_buffers do
|
||||
table.clear(buffer)
|
||||
end
|
||||
|
||||
-- process all properties for immediate setting or buffering
|
||||
process_props(instance, properties)
|
||||
|
||||
-- connect buffered events
|
||||
for event, fn in next, event_buffer do
|
||||
(instance :: any)[event]:Connect(fn)
|
||||
end
|
||||
|
||||
-- run buffered actions
|
||||
for _, buffer in next, action_buffers do
|
||||
for _, callback in next, buffer do
|
||||
for _, queued in next, actions do
|
||||
for _, callback in next, queued do
|
||||
callback(instance)
|
||||
end
|
||||
end
|
||||
|
|
@ -152,6 +162,14 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
end
|
||||
end
|
||||
|
||||
-- clear caches
|
||||
table.clear(events)
|
||||
for _, queued in next, actions do table.clear(queued) end
|
||||
if strict then table.clear(nested_debug) end
|
||||
table.clear(nested_stack)
|
||||
|
||||
return_caches(caches)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
|
|
|
|||
25
src/batch.luau
Normal file
25
src/batch.luau
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
local function batch(setter: () -> ())
|
||||
local already_batching = flags.batch
|
||||
|
||||
flags.batch = true
|
||||
|
||||
local ok, err: string? = pcall(setter)
|
||||
|
||||
if not already_batching then
|
||||
flags.batch = false
|
||||
|
||||
if not already_batching then
|
||||
graph.flush_update_queue()
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then throw(`error occured while batching updates: {err}`) end
|
||||
end
|
||||
|
||||
return batch
|
||||
|
|
@ -5,7 +5,7 @@ local flags = require(script.Parent.flags)
|
|||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
|
|
@ -31,8 +31,7 @@ function create_binding<T>(updater: (T) -> T, binding: T)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(binding, updater)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ local function changed<T>(property: string, callback: (T) -> ())
|
|||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
|
||||
callback((instance :: any)[property])
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,43 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local get_scope = graph.get_scope
|
||||
local add_cleanup = graph.add_cleanup
|
||||
|
||||
local function cleanup(callback: () -> ())
|
||||
local function helper(obj: any)
|
||||
return
|
||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||
elseif obj.destroy then function() obj:destroy() end
|
||||
elseif obj.disconnect then function() obj:disconnect() end
|
||||
elseif obj.Destroy then function() obj:Destroy() end
|
||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||
else throw("cannot cleanup given object")
|
||||
end
|
||||
|
||||
local function cleanup(value: unknown)
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
throw "cannot cleanup in a non-reactive scope"
|
||||
end; assert(scope)
|
||||
|
||||
add_cleanup(scope, callback)
|
||||
if type(value) == "function" then
|
||||
add_cleanup(scope, value :: () -> ())
|
||||
else
|
||||
add_cleanup(scope, helper(value))
|
||||
end
|
||||
end
|
||||
|
||||
return cleanup
|
||||
type Destroyable = { destroy: (any) -> () } | { Destroy: (any) -> () }
|
||||
type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
||||
|
||||
return cleanup ::
|
||||
( (callback: () -> ()) -> () ) &
|
||||
( (instance: Destroyable) -> () ) &
|
||||
( (connection: Disconnectable) -> () ) &
|
||||
( (instance: Instance) -> () ) &
|
||||
( (connection: RBXScriptConnection) -> () )
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ local Instance = game and Instance or require "test/mock".Instance :: never
|
|||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local apply = require(script.Parent.apply)
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
local function create_instance(class: string)
|
||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||
|
||||
setmetatable(ctor_cache :: any, {
|
||||
__index = function(self, class)
|
||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
||||
|
||||
|
|
@ -18,29 +20,37 @@ local function create_instance(class: string)
|
|||
end
|
||||
end
|
||||
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
local function ctor(properties: Props): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
end; create_instance = memoize(create_instance) -- always return same constructor for given class
|
||||
|
||||
self[class] = ctor
|
||||
return ctor
|
||||
end
|
||||
})
|
||||
|
||||
local function create_instance(class: string)
|
||||
return ctor_cache[class]
|
||||
end
|
||||
|
||||
local function clone_instance(instance: Instance)
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
return function(properties: Props): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then error("Attempt to clone a non-archivable instance", 3) end
|
||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
local function create(class_or_instance: string|Instance)
|
||||
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
return create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
return clone_instance(class_or_instance)
|
||||
else
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
end
|
||||
return nil :: never
|
||||
end
|
||||
end
|
||||
|
||||
type Props = { [any]: any }
|
||||
return (create :: any) ::
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ local graph = require(script.Parent.graph)
|
|||
local create_node = graph.create_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
local function derive<T>(source: () -> T): () -> T
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(false :: any, source)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ if not game then script = require "test/relative-string" end
|
|||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
local function effect<T>(callback: (T) -> T, initial_value: T)
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(initial_value, callback)
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,7 @@
|
|||
return { strict = false }
|
||||
local function inline_test(): string
|
||||
return debug.info(1, "n")
|
||||
end
|
||||
|
||||
local is_O2 = inline_test() ~= "inline_test"
|
||||
|
||||
return { strict = not is_O2, batch = false }
|
||||
|
|
|
|||
180
src/graph.luau
180
src/graph.luau
|
|
@ -12,47 +12,44 @@ export type Node<T> = {
|
|||
cache: T,
|
||||
effect: ((T) -> T) | false,
|
||||
cleanups: { () -> () } | false,
|
||||
parents: { owner: StartNode<T>?, [number]: StartNode<T> },
|
||||
[number]: Node<T>
|
||||
|
||||
owned: { Node<T> } | false,
|
||||
owner: Node<T> | false,
|
||||
|
||||
parents: { StartNode<T> },
|
||||
[number]: Node<T> -- children
|
||||
}
|
||||
|
||||
-- reactive scope stack
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
|
||||
|
||||
-- runs a given callback in a context that Luau does not allow yielding in
|
||||
local check_for_yield: <T...>(fn: (T...) -> (), T...) -> (boolean, string?) do
|
||||
local t = { __mode = "kv" }
|
||||
setmetatable(t, t)
|
||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||
local thread = coroutine.create(pcall)
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg)
|
||||
|
||||
check_for_yield = function(fn, ...: any)
|
||||
local args = { ... }
|
||||
assert(resume_ok)
|
||||
|
||||
t.__unm = function(_)
|
||||
fn(unpack(args))
|
||||
if coroutine.status(thread) ~= "dead" then
|
||||
return false, "attempt to yield in reactive scope"
|
||||
end
|
||||
|
||||
local ok, err: string? = pcall(function()
|
||||
local _ = -t
|
||||
end)
|
||||
|
||||
return ok, if err == "attempt to yield across metamethod/C-call boundary"
|
||||
or err == "thread is not yieldable" then "yield occured"
|
||||
else err
|
||||
end
|
||||
return run_ok, result
|
||||
end
|
||||
|
||||
local function get_scope(): Node<unknown>?
|
||||
return scopes[scopes.n]
|
||||
end
|
||||
|
||||
local function get_owning_scope(): Node<unknown>
|
||||
local function assert_owning_scope(): Node<unknown>
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
local caller_name = debug.info(2, "n")
|
||||
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
|
||||
return throw(`cannot use {caller_name}() in a non-reactive scope`)
|
||||
elseif scope.effect then
|
||||
throw("owning scope is not stable; are you trying to derive a new source from within a side-effect?")
|
||||
throw("cannot create new reactive scope in a tracking reactive scope")
|
||||
end
|
||||
|
||||
return scope
|
||||
end
|
||||
|
||||
|
|
@ -62,8 +59,12 @@ local function add_child<T>(parent: StartNode<any>, child: Node<any>)
|
|||
end
|
||||
|
||||
local function set_owner(node: Node<any>, owner: Node<any>)
|
||||
node.parents.owner = owner
|
||||
table.insert(owner, node)
|
||||
node.owner = owner
|
||||
if owner.owned then
|
||||
table.insert(owner.owned, node)
|
||||
else
|
||||
owner.owned = { node }
|
||||
end
|
||||
end
|
||||
|
||||
local function open_scope<T>(node: Node<T>)
|
||||
|
|
@ -96,19 +97,18 @@ local function run_cleanups<T>(node: Node<T>)
|
|||
end
|
||||
end
|
||||
|
||||
local function remove_child<T>(parent: StartNode<T>, child: Node<T>)
|
||||
local idx = table.find(parent, child)
|
||||
assert(idx, "child not found")
|
||||
local n = #parent
|
||||
parent[idx] = parent[n]
|
||||
parent[n] = nil
|
||||
local function find_and_swap_pop<T>(t: { T }, v: T)
|
||||
local idx = table.find(t, v) :: number
|
||||
local n = #t
|
||||
t[idx] = t[n]
|
||||
t[n] = nil
|
||||
end
|
||||
|
||||
local function unparent<T>(node: Node<T>)
|
||||
local parents = node.parents
|
||||
|
||||
for i, parent in ipairs(parents) do
|
||||
remove_child(parent, node)
|
||||
for i, parent in next, parents do
|
||||
find_and_swap_pop(parent, node)
|
||||
parents[i] = nil
|
||||
end
|
||||
end
|
||||
|
|
@ -117,83 +117,120 @@ local function destroy<T>(node: Node<T>)
|
|||
run_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
node.effect = false
|
||||
|
||||
if node.parents.owner then
|
||||
remove_child(node.parents.owner, node)
|
||||
node.parents.owner = nil
|
||||
if node.owner then
|
||||
find_and_swap_pop(node.owner.owned :: { Node<T> }, node)
|
||||
node.owner = false
|
||||
end
|
||||
|
||||
while node[1] do destroy(node[1]) end
|
||||
if node.owned then
|
||||
local owned = node.owned
|
||||
while owned[1] do destroy(owned[1]) end
|
||||
end
|
||||
end
|
||||
|
||||
local update_queue = {} :: { Node<any> }
|
||||
local function destroy_owned<T>(node: Node<T>)
|
||||
if node.owned then
|
||||
local owned = node.owned
|
||||
while owned[1] do destroy(owned[1]) end
|
||||
end
|
||||
end
|
||||
|
||||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
||||
|
||||
local function evaluate_node<T>(node: Node<T>)
|
||||
local cur_value = node.cache
|
||||
|
||||
if flags.strict then
|
||||
run_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
open_scope(node)
|
||||
|
||||
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
|
||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then throw(err :: string) end
|
||||
if not ok then throw(new_value :: string) end
|
||||
|
||||
node.cache = new_value :: T
|
||||
end
|
||||
|
||||
run_cleanups(node) -- todo: move in scope?
|
||||
run_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
open_scope(node)
|
||||
|
||||
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
|
||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`side-effect error from source update\n{new_value}`)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
||||
return cur_value ~= new_value -- node has changed value
|
||||
return cur_value ~= new_value
|
||||
end
|
||||
|
||||
-- todo: case where owner is set from an untrack call within an effectful node, children clearing
|
||||
local function update_from<T>(node: StartNode<T>, n0: number)
|
||||
if not node[1] then return end
|
||||
|
||||
local n = n0
|
||||
|
||||
-- unparent all children and queue for eval
|
||||
do
|
||||
local child = node[1]
|
||||
while child do -- todo: case where child in owner context
|
||||
unparent(child)
|
||||
|
||||
n += 1
|
||||
update_queue[n] = child
|
||||
|
||||
child = node[1]
|
||||
local function queue_children<T>(node: StartNode<T>)
|
||||
local i = update_queue.n
|
||||
while node[1] do
|
||||
i += 1
|
||||
update_queue[i] = node[1]
|
||||
unparent(node[1])
|
||||
end
|
||||
update_queue.n = i
|
||||
end
|
||||
|
||||
-- evaluate all queued children
|
||||
for i = n0 + 1, n do
|
||||
local child = update_queue[i] -- todo: error: index boolean
|
||||
if not child.effect then continue end
|
||||
local _flushing = false
|
||||
local function flush_update_queue()
|
||||
assert(not _flushing, "recursive queue flush occured") -- todo
|
||||
_flushing = true
|
||||
|
||||
if evaluate_node(child) then
|
||||
update_from(child, n)
|
||||
local n0 = 0
|
||||
|
||||
local i = n0 + 1
|
||||
while i <= update_queue.n do
|
||||
local node = update_queue[i]
|
||||
--assert(node.effect)
|
||||
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any
|
||||
i += 1
|
||||
end
|
||||
|
||||
update_queue.n = n0
|
||||
|
||||
_flushing = false
|
||||
end
|
||||
|
||||
local function update<T>(root: StartNode<T>)
|
||||
local n0 = update_queue.n
|
||||
queue_children(root)
|
||||
|
||||
if flags.batch then return end
|
||||
|
||||
local i = n0 + 1
|
||||
while i <= update_queue.n do
|
||||
local node = update_queue[i]
|
||||
--assert(node.effect)
|
||||
|
||||
-- check if node is still owned in case destroyed after queued
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
local function update<T>(node: StartNode<T>)
|
||||
update_from(node, 0)
|
||||
update_queue.n = n0
|
||||
end
|
||||
|
||||
local function track<T>(node: StartNode<T>)
|
||||
|
|
@ -208,6 +245,10 @@ local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
|
|||
cache = value,
|
||||
effect = effect,
|
||||
cleanups = false,
|
||||
|
||||
owner = false,
|
||||
owned = false,
|
||||
|
||||
parents = {},
|
||||
}
|
||||
end
|
||||
|
|
@ -225,7 +266,7 @@ return table.freeze {
|
|||
close_scope = close_scope,
|
||||
evaluate_node = evaluate_node,
|
||||
get_scope = get_scope,
|
||||
get_owning_scope = get_owning_scope,
|
||||
assert_owning_scope = assert_owning_scope,
|
||||
add_cleanup = add_cleanup,
|
||||
set_owner = set_owner,
|
||||
destroy = destroy,
|
||||
|
|
@ -236,5 +277,6 @@ return table.freeze {
|
|||
create_node = create_node,
|
||||
create_start_node = create_start_node,
|
||||
get_children = get_children,
|
||||
flush_update_queue = flush_update_queue,
|
||||
scopes = scopes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- vide.luau
|
||||
-- v0.1.0
|
||||
-- v0.2.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
|
@ -11,10 +11,13 @@ local create = require(script.create)
|
|||
local apply = require(script.apply)
|
||||
local source = require(script.source)
|
||||
local effect = require(script.effect)
|
||||
local derive = require(script.derive)
|
||||
local cleanup = require(script.cleanup)
|
||||
local untrack = require(script.untrack)
|
||||
local derive = require(script.derive)
|
||||
local read = require(script.read)
|
||||
local batch = require(script.batch)
|
||||
local switch = require(script.switch)
|
||||
local show = require(script.show)
|
||||
local indexes, values = require(script.maps)()
|
||||
local spring, update_springs = require(script.spring)()
|
||||
local action = require(script.action)()
|
||||
|
|
@ -51,12 +54,15 @@ local vide = {
|
|||
effect = effect,
|
||||
derive = derive,
|
||||
switch = switch,
|
||||
show = show,
|
||||
indexes = indexes,
|
||||
values = values,
|
||||
|
||||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
|
@ -86,9 +92,6 @@ local vide = {
|
|||
end
|
||||
}
|
||||
|
||||
do
|
||||
local set = false
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if index == "strict" then
|
||||
|
|
@ -100,14 +103,11 @@ do
|
|||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if index == "strict" then
|
||||
if set then throw "strict mode has already been set" end
|
||||
set = true
|
||||
flags.strict = value :: boolean
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return vide
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node
|
|||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
|
@ -28,7 +28,7 @@ local function check_primitives(t: {})
|
|||
end
|
||||
|
||||
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
|
@ -112,6 +112,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
local node = create_node(false :: any, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
|
|
@ -122,7 +123,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
end
|
||||
|
||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
|
@ -214,6 +215,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
local node = create_node(false :: any, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
local function memoize<X, Y>(f: (X) -> Y): (X) -> Y
|
||||
local cache: { [X]: Y? } = {}
|
||||
|
||||
return function(x: X): Y
|
||||
local y = cache[x]
|
||||
|
||||
if not y then
|
||||
y = f(x)
|
||||
cache[x] = y
|
||||
end
|
||||
|
||||
return y :: Y
|
||||
end
|
||||
end
|
||||
|
||||
return memoize
|
||||
|
||||
7
src/read.luau
Normal file
7
src/read.luau
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function read<T>(value: T | () -> T): T
|
||||
return if type(value) == "function" then value() else value
|
||||
end
|
||||
|
||||
return read
|
||||
18
src/show.luau
Normal file
18
src/show.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local switch = require(script.Parent.switch)
|
||||
|
||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||
local function truthy()
|
||||
return not not source()
|
||||
end
|
||||
|
||||
return switch(truthy) {
|
||||
[true] = component,
|
||||
[false] = fallback,
|
||||
}
|
||||
end
|
||||
|
||||
return show ::
|
||||
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
|
||||
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)
|
||||
|
|
@ -6,7 +6,7 @@ local create_start_node = graph.create_start_node
|
|||
local track = graph.track
|
||||
local update = graph.update
|
||||
|
||||
export type Source<T> = (() -> T) & ((T) -> T)
|
||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||
|
||||
local function source<T>(initial_value: T): Source<T>
|
||||
local node = create_start_node(initial_value)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type Node<T> = graph.Node<T>
|
|||
type StartNode<T> = graph.StartNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local update = graph.update
|
||||
local set_owner = graph.set_owner
|
||||
|
|
@ -150,7 +150,7 @@ local springs: { [SpringData<any>]: StartNode<any> } = {}
|
|||
setmetatable(springs, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
-- https://en.wikipedia.org/wiki/Damping
|
||||
|
||||
|
|
@ -161,6 +161,12 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
local c_c = 2*w_n
|
||||
local c = z * c_c
|
||||
|
||||
-- todo: is there a solution other than reducing step size?
|
||||
-- todo: this does not catch all solver exploding cases
|
||||
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
||||
throw("spring damping too high, consider reducing damping or increasing period")
|
||||
end
|
||||
|
||||
local data: SpringData<T> = {
|
||||
k = k,
|
||||
c = c,
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ local evaluate_node = graph.evaluate_node
|
|||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local destroy = graph.destroy
|
||||
local get_owning_scope = graph.get_owning_scope
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
|
||||
local owner = get_owning_scope()
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
return function(map)
|
||||
local last_scope: Node<false>?
|
||||
|
|
@ -35,7 +35,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
if component == nil then return nil end
|
||||
|
||||
if type(component) ~= "function" then
|
||||
throw("map must map a value to a function")
|
||||
throw "map must map a value to a function"
|
||||
end
|
||||
|
||||
local new_scope = create_node(false, false)
|
||||
|
|
@ -53,7 +53,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
return result
|
||||
end
|
||||
|
||||
local node = create_node(nil :: any, update)
|
||||
local node = create_node(nil :: U?, update)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local get_scope = graph.get_scope
|
||||
|
||||
local function untrack<T>(source: () -> T): T
|
||||
local scope = get_scope()
|
||||
if not scope then
|
||||
throw("cannot untrack in non-reactive scope")
|
||||
end; assert(scope)
|
||||
|
||||
if scope then
|
||||
-- sources are only tracked if the node in scope has an effect
|
||||
local effect = scope.effect
|
||||
scope.effect = false
|
||||
|
|
@ -22,6 +19,9 @@ local function untrack<T>(source: () -> T): T
|
|||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
else
|
||||
return source()
|
||||
end
|
||||
end
|
||||
|
||||
return untrack
|
||||
|
|
|
|||
|
|
@ -6,26 +6,30 @@ local source = vide.source
|
|||
local derive = vide.derive
|
||||
local indexes = vide.indexes
|
||||
local values = vide.values
|
||||
local batch = vide.batch
|
||||
local cleanup = vide.cleanup
|
||||
local create = vide.create
|
||||
|
||||
assert(not vide.strict)
|
||||
|
||||
local function TITLE(name: string)
|
||||
print()
|
||||
print(testkit.color.white(name))
|
||||
end
|
||||
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
local function WRAP_BENCH(name: string, fn: () -> ())
|
||||
local function ROOT_BENCH(name: string, fn: () -> ())
|
||||
vide.root(function(destroy)
|
||||
BENCH(name, fn)
|
||||
return destroy
|
||||
end)()
|
||||
end
|
||||
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
|
||||
TITLE "sources"
|
||||
|
||||
WRAP_BENCH("create source", function()
|
||||
BENCH("create source", function()
|
||||
local cache = table.create(N)
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -33,7 +37,7 @@ WRAP_BENCH("create source", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("get value", function()
|
||||
BENCH("get value", function()
|
||||
local src = source(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -41,7 +45,7 @@ WRAP_BENCH("get value", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("set value", function()
|
||||
BENCH("set value", function()
|
||||
local src = source(1)
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -49,7 +53,7 @@ WRAP_BENCH("set value", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("derive 1 source", function()
|
||||
ROOT_BENCH("derive 1 source", function()
|
||||
local cache = table.create(N)
|
||||
local src = source(1)
|
||||
|
||||
|
|
@ -60,7 +64,7 @@ WRAP_BENCH("derive 1 source", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("derive 4 sources", function()
|
||||
ROOT_BENCH("derive 4 sources", function()
|
||||
local cache = table.create(N)
|
||||
local src = vide.source(1)
|
||||
local src2 = vide.source(2)
|
||||
|
|
@ -76,7 +80,7 @@ end)
|
|||
|
||||
TITLE "graphs"
|
||||
|
||||
WRAP_BENCH("update 1->1 graph", function()
|
||||
ROOT_BENCH("update 1->1 graph", function()
|
||||
local src = source(1)
|
||||
|
||||
local _derived = derive(function() return src() end)
|
||||
|
|
@ -86,7 +90,7 @@ WRAP_BENCH("update 1->1 graph", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("update 1->1 graph with cleanup", function()
|
||||
ROOT_BENCH("update 1->1 graph with cleanup", function()
|
||||
local src = source(1)
|
||||
|
||||
derive(function()
|
||||
|
|
@ -99,7 +103,7 @@ WRAP_BENCH("update 1->1 graph with cleanup", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("update 1->1000 graph", function()
|
||||
ROOT_BENCH("update 1->1000 graph", function()
|
||||
local src = source(-1)
|
||||
|
||||
for i = 1, 1000 do
|
||||
|
|
@ -113,7 +117,7 @@ WRAP_BENCH("update 1->1000 graph", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("update 1->1->1->1...1000 graph", function()
|
||||
ROOT_BENCH("update 1->1->1->1...1000 graph", function()
|
||||
local src = source(-1)
|
||||
|
||||
local last = src
|
||||
|
|
@ -129,8 +133,28 @@ WRAP_BENCH("update 1->1->1->1...1000 graph", function()
|
|||
end
|
||||
end)
|
||||
|
||||
-- todo: repeat with batching
|
||||
WRAP_BENCH("update 1000->1 graph", function()
|
||||
-- todo: why does it hang at 1k? it didn't before
|
||||
ROOT_BENCH("update 500->1 graph", function()
|
||||
local srcs = {}
|
||||
for i = 1, 500 do
|
||||
srcs[i] = source(0)
|
||||
end
|
||||
|
||||
derive(function()
|
||||
for i = 1, 500 do
|
||||
srcs[i]()
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
for i = 1, START(1) do
|
||||
for idx = 1, 500 do
|
||||
srcs[idx](i)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
ROOT_BENCH("update 1000->1 graph (batched)", function()
|
||||
local srcs = {}
|
||||
for i = 1, 1000 do
|
||||
srcs[i] = source(0)
|
||||
|
|
@ -144,14 +168,16 @@ WRAP_BENCH("update 1000->1 graph", function()
|
|||
end)
|
||||
|
||||
for i = 1, START(1) do
|
||||
batch(function()
|
||||
for idx = 1, 1000 do
|
||||
srcs[idx](i)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
-- todo: optimize, repeat with batching
|
||||
WRAP_BENCH("update 1000x 1->1 common extern. graph", function()
|
||||
-- todo: optimize this case
|
||||
ROOT_BENCH("update 1000 1->1 common extern. graph", function()
|
||||
local ext = source(-1)
|
||||
|
||||
local srcs = {}
|
||||
|
|
@ -171,7 +197,7 @@ end)
|
|||
|
||||
TITLE "property apply"
|
||||
|
||||
WRAP_BENCH("apply 0 properties", function()
|
||||
ROOT_BENCH("apply 0 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
|
|
@ -180,7 +206,7 @@ WRAP_BENCH("apply 0 properties", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("apply 8 properties", function()
|
||||
ROOT_BENCH("apply 8 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
|
|
@ -198,7 +224,7 @@ WRAP_BENCH("apply 8 properties", function()
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("bind property", function()
|
||||
ROOT_BENCH("bind property", function()
|
||||
local apply = require "src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
|
|
@ -213,7 +239,7 @@ WRAP_BENCH("bind property", function()
|
|||
return nil
|
||||
end)
|
||||
|
||||
WRAP_BENCH("update binding", function()
|
||||
ROOT_BENCH("update binding", function()
|
||||
local apply = require "src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
|
|
@ -230,11 +256,29 @@ WRAP_BENCH("update binding", function()
|
|||
return nil
|
||||
end)
|
||||
|
||||
TITLE "switch()"
|
||||
|
||||
ROOT_BENCH("switch()", function()
|
||||
local M = 2^8
|
||||
|
||||
local map = {}
|
||||
for i = 1, M do
|
||||
map[i] = function() return i end
|
||||
end
|
||||
|
||||
local input = source(0)
|
||||
vide.switch(input)(map)
|
||||
|
||||
for i = 1, START(N) do
|
||||
input(bit32.band(i, M - 1) + 1) -- i % m + 1
|
||||
end
|
||||
end)
|
||||
|
||||
TITLE "indexes()"
|
||||
|
||||
N /= 1024
|
||||
|
||||
WRAP_BENCH("indexes() all new", function()
|
||||
ROOT_BENCH("indexes() all new", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -252,7 +296,7 @@ WRAP_BENCH("indexes() all new", function()
|
|||
return nil
|
||||
end)
|
||||
|
||||
WRAP_BENCH("indexes() no change", function()
|
||||
ROOT_BENCH("indexes() no change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -272,7 +316,7 @@ WRAP_BENCH("indexes() no change", function()
|
|||
return nil
|
||||
end)
|
||||
|
||||
WRAP_BENCH("indexes() all change", function()
|
||||
ROOT_BENCH("indexes() all change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -296,7 +340,7 @@ WRAP_BENCH("indexes() all change", function()
|
|||
src(data)
|
||||
end)
|
||||
|
||||
WRAP_BENCH("indexes() all remove", function()
|
||||
ROOT_BENCH("indexes() all remove", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -320,7 +364,7 @@ end)
|
|||
|
||||
TITLE "values()"
|
||||
|
||||
WRAP_BENCH("values() all new", function()
|
||||
ROOT_BENCH("values() all new", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -338,7 +382,7 @@ WRAP_BENCH("values() all new", function()
|
|||
return nil
|
||||
end)
|
||||
|
||||
WRAP_BENCH("values() no change", function()
|
||||
ROOT_BENCH("values() no change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -358,7 +402,7 @@ WRAP_BENCH("values() no change", function()
|
|||
src(data)
|
||||
end)
|
||||
|
||||
WRAP_BENCH("values() all change", function()
|
||||
ROOT_BENCH("values() all change", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -383,7 +427,7 @@ WRAP_BENCH("values() all change", function()
|
|||
src(data)
|
||||
end)
|
||||
|
||||
WRAP_BENCH("values() all remove", function()
|
||||
ROOT_BENCH("values() all remove", function()
|
||||
local data = {}
|
||||
|
||||
for i = 1, N do
|
||||
|
|
@ -407,7 +451,7 @@ N *= 1024
|
|||
|
||||
TITLE "cleanup"
|
||||
|
||||
WRAP_BENCH("register new cleanup", function()
|
||||
ROOT_BENCH("register new cleanup", function()
|
||||
local cleanup = cleanup
|
||||
|
||||
local cleaner = function() end
|
||||
|
|
@ -431,7 +475,7 @@ TITLE "aggregate"
|
|||
do
|
||||
-- the purpose of the two following benchmarks is to measure the overhead of
|
||||
-- aggregate construction
|
||||
WRAP_BENCH("set explicit mock vector2", function()
|
||||
ROOT_BENCH("set explicit mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
|
||||
|
|
@ -446,7 +490,7 @@ do
|
|||
end
|
||||
end)
|
||||
|
||||
WRAP_BENCH("set aggregate mock vector2", function()
|
||||
ROOT_BENCH("set aggregate mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
|
||||
|
|
@ -465,7 +509,7 @@ end
|
|||
-- innacurate due to no Vector3 in vanilla Luau
|
||||
-- mock vector is 200x slower than native vector
|
||||
|
||||
-- WRAP_BENCH("spring update", function()
|
||||
-- ROOT_BENCH("spring update", function()
|
||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
-- local src = source(0)
|
||||
|
|
@ -485,7 +529,7 @@ end
|
|||
|
||||
-- N /= 1024
|
||||
|
||||
-- WRAP_BENCH("spring step", function()
|
||||
-- ROOT_BENCH("spring step", function()
|
||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
-- local src = source(0)
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@ local function main()
|
|||
local reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||
local offset = string.rep("\n", MAX - fv + OFFSET)
|
||||
local bar = testkit.color.gray(remainder_to_block(v - fv) .. "\n" .. string.rep(BLOCK .. "\n", fv))
|
||||
print(reset .. offset .. bar)
|
||||
print(reset .. offset .. bar .. "\n" .. v)
|
||||
end)
|
||||
|
||||
local elapsed = 0
|
||||
local T = 3
|
||||
local elapsed = T/1.2
|
||||
repeat local dt = step()
|
||||
vide.step(dt)
|
||||
|
||||
local T = 3
|
||||
elapsed += dt
|
||||
while elapsed >= T do
|
||||
elapsed -= T
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- testkit.luau
|
||||
-- v0.7.1
|
||||
-- v0.7.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local color = {
|
||||
|
|
@ -323,7 +323,7 @@ end
|
|||
|
||||
local function print2(v: unknown)
|
||||
type Buffer = { n: number, [number]: string }
|
||||
type Cyclic = { [{}]: true }
|
||||
type Cyclic = { n: number, [{}]: number }
|
||||
|
||||
-- overkill concatenationless string buffer
|
||||
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
|
||||
|
|
@ -347,16 +347,19 @@ local function print2(v: unknown)
|
|||
else -- is table
|
||||
local tabbed_indent = indent .. TAB
|
||||
|
||||
str.n += 1
|
||||
|
||||
if cyclic[value] then
|
||||
str[str.n] = color.gray "*cyclic reference*"
|
||||
str.n += 1
|
||||
str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
|
||||
return
|
||||
else
|
||||
cyclic[value] = true
|
||||
cyclic.n += 1
|
||||
cyclic[value] = cyclic.n
|
||||
end
|
||||
|
||||
str[str.n] = "{\n"
|
||||
str.n += 3
|
||||
str[str.n - 2] = "{ "
|
||||
str[str.n - 1] = color.gray(tostring(cyclic[value]))
|
||||
str[str.n - 0] = "\n"
|
||||
|
||||
local i, v = next(value, nil)
|
||||
while v ~= nil do
|
||||
|
|
@ -393,7 +396,7 @@ local function print2(v: unknown)
|
|||
end
|
||||
|
||||
local str = { n = 0 }
|
||||
local cyclic = {}
|
||||
local cyclic = { n = 0 }
|
||||
tos(v, 0, str, cyclic)
|
||||
print(table.concat(str))
|
||||
end
|
||||
|
|
@ -455,7 +458,7 @@ return {
|
|||
return BENCH, START
|
||||
end,
|
||||
|
||||
print2 = print2,
|
||||
print = print2,
|
||||
|
||||
seq = shallow_eq,
|
||||
deq = deep_eq,
|
||||
|
|
|
|||
685
test/tests.luau
685
test/tests.luau
|
|
@ -31,6 +31,8 @@ end
|
|||
|
||||
local NIL = nil :: any
|
||||
|
||||
vide.strict = false
|
||||
|
||||
TEST("graph", function()
|
||||
local create_node = graph.create_node
|
||||
local track = graph.track
|
||||
|
|
@ -39,6 +41,7 @@ TEST("graph", function()
|
|||
local get_scope = graph.get_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local set_owner = graph.set_owner
|
||||
local get_children = graph.get_children
|
||||
local add_cleanup = graph.add_cleanup
|
||||
local destroy = graph.destroy
|
||||
|
|
@ -73,10 +76,14 @@ TEST("graph", function()
|
|||
end
|
||||
|
||||
do CASE "rerun linked nodes"
|
||||
local root = node()
|
||||
local a = node()
|
||||
local b = node()
|
||||
local c = node()
|
||||
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
|
||||
local count = 0
|
||||
|
||||
local function effect(x)
|
||||
|
|
@ -102,8 +109,15 @@ TEST("graph", function()
|
|||
end
|
||||
|
||||
do CASE "diamond graph"
|
||||
-- a -> b -> d
|
||||
-- -> c
|
||||
local root = node()
|
||||
local a, b, c, d = node(), node(), node(), node()
|
||||
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
set_owner(d, root)
|
||||
|
||||
local b_cnt, c_cnt, d_cnt = 0, 0, 0
|
||||
function b.effect(x) b_cnt += 1; return not x end
|
||||
function c.effect(x) c_cnt += 1; return not x end
|
||||
|
|
@ -121,15 +135,20 @@ TEST("graph", function()
|
|||
end
|
||||
|
||||
do CASE "duplicate child on rerun"
|
||||
local root = node()
|
||||
local a, b, c = node(), node(), node()
|
||||
|
||||
set_owner(a, root)
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
|
||||
function c.effect(x)
|
||||
track(a)
|
||||
track(b)
|
||||
return not x
|
||||
end
|
||||
|
||||
open_scope(c); assert(c.effect)(NIL); close_scope()
|
||||
open_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); close_scope()
|
||||
|
||||
update(a)
|
||||
|
||||
|
|
@ -166,28 +185,28 @@ TEST("graph", function()
|
|||
items_updated = node()
|
||||
track(items_updated) -- should not
|
||||
|
||||
add_child(root, items_updated)
|
||||
set_owner(items_updated, root)
|
||||
do open_scope(items_updated)
|
||||
track(items)
|
||||
|
||||
do open_scope(root)
|
||||
add_child(root, scope1)
|
||||
set_owner(scope1, root)
|
||||
do open_scope(scope1)
|
||||
clean "scope1"
|
||||
bind1 = node()
|
||||
|
||||
add_child(scope1, bind1)
|
||||
set_owner(bind1, scope1)
|
||||
do open_scope(bind1)
|
||||
clean "bind1"
|
||||
track(selected)
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
|
||||
add_child(root, scope2)
|
||||
set_owner(scope2, root)
|
||||
do open_scope(scope2)
|
||||
clean "scope2"
|
||||
bind2 = node()
|
||||
add_child(scope2, bind2)
|
||||
set_owner(bind2, scope2)
|
||||
do open_scope(bind2)
|
||||
clean "bind2"
|
||||
track(selected)
|
||||
|
|
@ -207,10 +226,10 @@ TEST("graph", function()
|
|||
|
||||
do
|
||||
local c = get_children(root)
|
||||
CHECK(#c == 3)
|
||||
CHECK(table.find(c, items_updated))
|
||||
CHECK(table.find(c, scope1 :: Node<any>))
|
||||
CHECK(table.find(c, scope2 :: Node<any>))
|
||||
CHECK(#c == 0)
|
||||
-- CHECK(table.find(c, items_updated))
|
||||
-- CHECK(table.find(c, scope1 :: Node<any>))
|
||||
-- CHECK(table.find(c, scope2 :: Node<any>))
|
||||
end
|
||||
|
||||
do
|
||||
|
|
@ -222,19 +241,19 @@ TEST("graph", function()
|
|||
|
||||
do
|
||||
local c = get_children(scope1)
|
||||
CHECK(#c == 1)
|
||||
CHECK(table.find(c, bind1))
|
||||
CHECK(#c == 0)
|
||||
--CHECK(table.find(c, bind1))
|
||||
end
|
||||
|
||||
do
|
||||
local c = get_children(scope2)
|
||||
CHECK(#c == 1)
|
||||
CHECK(table.find(c, bind2))
|
||||
CHECK(#c == 0)
|
||||
--CHECK(table.find(c, bind2))
|
||||
end
|
||||
|
||||
-- destroy
|
||||
|
||||
CHECK(table.find(get_children(root), scope1 :: Node<any>))
|
||||
--CHECK(table.find(get_children(root), scope1 :: Node<any>))
|
||||
|
||||
destroy(scope1)
|
||||
CHECK(cleaned.scope1)
|
||||
|
|
@ -243,7 +262,7 @@ TEST("graph", function()
|
|||
bind1 = NIL
|
||||
bind2 = NIL
|
||||
gc()
|
||||
CHECK(#get_children(root) == 2)
|
||||
CHECK(#get_children(root) == 0)
|
||||
CHECK(#get_children(selected) == 1)
|
||||
end
|
||||
|
||||
|
|
@ -253,6 +272,55 @@ TEST("graph", function()
|
|||
gc()
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do CASE "recursive update"
|
||||
--[[
|
||||
|
||||
A -> B + C
|
||||
D -> E + F
|
||||
|
||||
B updates D
|
||||
|
||||
depth=1
|
||||
B, C
|
||||
^
|
||||
|
||||
depth=2
|
||||
E, F
|
||||
^
|
||||
|
||||
depth=2
|
||||
_, F
|
||||
^
|
||||
|
||||
depth=1
|
||||
_, _ <- attempt to update nothing
|
||||
^
|
||||
|
||||
]]
|
||||
|
||||
local a, b, c, d, e, f = node(), node(), node(), node(), node(), node()
|
||||
|
||||
local root = node()
|
||||
set_owner(a, root)
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
set_owner(d, root)
|
||||
set_owner(e, root)
|
||||
set_owner(f, root)
|
||||
|
||||
function b.effect(x)
|
||||
update(d)
|
||||
return not x
|
||||
end
|
||||
|
||||
add_child(a, b); add_child(a, c)
|
||||
add_child(d, e); add_child(d, f)
|
||||
|
||||
update(a)
|
||||
|
||||
CHECK(true)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("mount()", function()
|
||||
|
|
@ -480,6 +548,44 @@ TEST("derive()", wrap_root(function()
|
|||
CHECK(count == 2)
|
||||
end
|
||||
|
||||
-- do CASE "behavior of effect within an effect"
|
||||
-- local num = source(1)
|
||||
|
||||
-- local ran = table.create(100, 0)
|
||||
-- local cleaned = table.create(100, 0)
|
||||
|
||||
-- local destroy = vide.mount(function()
|
||||
-- local owner = derive(function()
|
||||
-- local i = num()
|
||||
|
||||
-- return untrack(function()
|
||||
-- return derive(function()
|
||||
-- ran[i] += 1
|
||||
-- cleanup(function()
|
||||
-- cleaned[i] += 1
|
||||
-- end)
|
||||
-- return i
|
||||
-- end)
|
||||
-- end)
|
||||
-- end)
|
||||
|
||||
-- local child1 = owner()
|
||||
-- num(2)
|
||||
-- CHECK(cleaned[1] == 1)
|
||||
-- local child2 = owner()
|
||||
|
||||
-- CHECK(child1() == 1)
|
||||
-- CHECK(child2() == 2)
|
||||
-- end)
|
||||
|
||||
-- destroy()
|
||||
|
||||
-- CHECK(ran[1] == 1)
|
||||
-- CHECK(ran[2] == 1)
|
||||
-- CHECK(cleaned[1] == 1)
|
||||
-- CHECK(cleaned[2] == 1)
|
||||
-- end
|
||||
|
||||
do CASE "garbage collection"
|
||||
-- check that `b` does not allow gc of `a`
|
||||
local a = source(1)
|
||||
|
|
@ -554,6 +660,7 @@ TEST("effect()", wrap_root(function()
|
|||
end))
|
||||
|
||||
TEST("cleanup()", wrap_root(function()
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
|
@ -612,6 +719,25 @@ TEST("cleanup()", wrap_root(function()
|
|||
src(3)
|
||||
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
|
||||
end
|
||||
|
||||
do CASE "cleanup objects"
|
||||
local ran = {}
|
||||
|
||||
root(function(destroy)
|
||||
effect(function()
|
||||
cleanup { disconnect = function() ran.disconnect = true end }
|
||||
cleanup { Disconnect = function() ran.Disconnect = true end }
|
||||
cleanup { destroy = function() ran.destroy = true end }
|
||||
cleanup { Destroy = function() ran.Destroy = true end }
|
||||
destroy()
|
||||
end)
|
||||
end)
|
||||
|
||||
CHECK(ran.disconnect)
|
||||
CHECK(ran.Disconnect)
|
||||
CHECK(ran.destroy)
|
||||
CHECK(ran.Destroy)
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("create()", wrap_root(function()
|
||||
|
|
@ -816,6 +942,33 @@ TEST("create()", wrap_root(function()
|
|||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do CASE "recursive create"
|
||||
local set_test_to_true = vide.action(function(self) (self :: any).test = true end)
|
||||
|
||||
local f2
|
||||
|
||||
local to_apply = {
|
||||
{ a = 1 },
|
||||
set_test_to_true,
|
||||
b = function() f2 = create "Frame" { a = 2 } end,
|
||||
} :: { [number|string]: unknown }
|
||||
|
||||
-- do -- confirm iteration order
|
||||
-- local t = {}
|
||||
-- for i in to_apply do
|
||||
-- table.insert(t, i)
|
||||
-- end
|
||||
-- assert(t[1] == "a")
|
||||
-- end
|
||||
|
||||
local f = create "Frame" (to_apply)
|
||||
|
||||
CHECK((f :: any).a == 1)
|
||||
CHECK((f :: any).test == true )
|
||||
|
||||
CHECK((f2 :: any).a == 2)
|
||||
end
|
||||
|
||||
do CASE "garbage collection test"
|
||||
local wref
|
||||
|
||||
|
|
@ -843,6 +996,76 @@ TEST("create()", wrap_root(function()
|
|||
end
|
||||
end))
|
||||
|
||||
TEST("show()", wrap_root(function()
|
||||
local untrack = vide.untrack
|
||||
local cleanup = vide.cleanup
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local show = vide.show
|
||||
local root = vide.root
|
||||
|
||||
do CASE "main"
|
||||
-- uses switch() internally, more extensive testing of scoping not needed
|
||||
local value = source("truey" :: unknown)
|
||||
local function one() return 1 end
|
||||
local function two() return 2 end
|
||||
|
||||
local output = show(value, one, two)
|
||||
|
||||
CHECK(output() == 1)
|
||||
value(nil)
|
||||
CHECK(output() == 2)
|
||||
end
|
||||
|
||||
do CASE "alt"
|
||||
local visible = vide.source(true)
|
||||
local count = vide.source(0)
|
||||
|
||||
local outer = 0
|
||||
local inner = 0
|
||||
local destroyed = 0
|
||||
|
||||
root(function()
|
||||
effect(function()
|
||||
visible()
|
||||
outer += 1
|
||||
|
||||
untrack(function()
|
||||
effect(function()
|
||||
count()
|
||||
|
||||
inner += 1
|
||||
|
||||
cleanup(function()
|
||||
destroyed += 1
|
||||
end)
|
||||
end)
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
CHECK(outer == 1)
|
||||
CHECK(inner == 1)
|
||||
CHECK(destroyed == 0)
|
||||
|
||||
count(count() + 1)
|
||||
CHECK(outer == 1)
|
||||
CHECK(inner == 2)
|
||||
CHECK(destroyed == 1)
|
||||
|
||||
visible(false)
|
||||
CHECK(outer == 2)
|
||||
CHECK(inner == 3)
|
||||
CHECK(destroyed == 2)
|
||||
|
||||
count(count() + 1)
|
||||
CHECK(outer == 2)
|
||||
CHECK(inner == 4)
|
||||
CHECK(destroyed == 3)
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("switch()", wrap_root(function()
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
|
@ -951,11 +1174,28 @@ TEST("switch()", wrap_root(function()
|
|||
|
||||
CHECK(n0 == n1)
|
||||
end
|
||||
|
||||
do CASE "strict"
|
||||
vide.strict = true
|
||||
|
||||
local input = source(0)
|
||||
local output = switch(input) {
|
||||
[0] = function() return 0 end,
|
||||
[1] = function() return 1 end,
|
||||
}
|
||||
|
||||
CHECK(output() == 0)
|
||||
input(1)
|
||||
CHECK(output() == 1)
|
||||
|
||||
vide.strict = false
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("indexes()", wrap_root(function()
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local indexes = vide.indexes
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
|
|
@ -976,11 +1216,15 @@ TEST("indexes()", wrap_root(function()
|
|||
|
||||
local count = table.create(3, 0)
|
||||
|
||||
local output = vide.root(function()
|
||||
local output = indexes(input, function(v, i)
|
||||
count[i] += 1
|
||||
return v
|
||||
end)
|
||||
|
||||
return output
|
||||
end)
|
||||
|
||||
input { 1, 2, 4 }
|
||||
|
||||
CHECK(output()[1]() == 1)
|
||||
|
|
@ -1091,6 +1335,59 @@ TEST("indexes()", wrap_root(function()
|
|||
|
||||
CHECK(n0 == n1)
|
||||
end
|
||||
|
||||
-- practical example based on the graph - recursive update test
|
||||
do CASE "recursive update"
|
||||
local items = source { 1 }
|
||||
|
||||
local updated = table.create(100, 0)
|
||||
|
||||
indexes(items, function(item)
|
||||
effect(function()
|
||||
item()
|
||||
updated[1] += 1
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
item()
|
||||
updated[2] += 1
|
||||
end)
|
||||
|
||||
return {}
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
items()
|
||||
updated[3] += 1
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
items()
|
||||
updated[4] += 1
|
||||
end)
|
||||
|
||||
items { 2 }
|
||||
|
||||
CHECK(updated[1] == 2)
|
||||
CHECK(updated[2] == 2)
|
||||
CHECK(updated[3] == 2)
|
||||
CHECK(updated[4] == 2)
|
||||
end
|
||||
|
||||
do CASE "strict"
|
||||
vide.strict = true
|
||||
|
||||
local input = source{1}
|
||||
local output = indexes(input, function(v)
|
||||
return { v }
|
||||
end)
|
||||
|
||||
CHECK(output()[1][1]() == 1)
|
||||
input{2}
|
||||
CHECK(output()[1][1]() == 2)
|
||||
|
||||
vide.strict = false
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("values()", wrap_root(function()
|
||||
|
|
@ -1321,12 +1618,12 @@ TEST("spring()", wrap_root(function()
|
|||
end))
|
||||
|
||||
TEST("untrack()", wrap_root(function()
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
local derive = vide.derive
|
||||
local untrack = vide.untrack
|
||||
local cleanup = vide.cleanup
|
||||
local root = vide.root
|
||||
|
||||
do CASE "does not register dependency"
|
||||
local a = source(0)
|
||||
|
|
@ -1505,7 +1802,7 @@ TEST("changed()", wrap_root(function()
|
|||
changed("Text", output)
|
||||
}
|
||||
|
||||
--CHECK(output() == "a")
|
||||
CHECK(output() == "a")
|
||||
text.Text = "b"
|
||||
CHECK(output() == "b")
|
||||
end
|
||||
|
|
@ -1530,6 +1827,338 @@ TEST("changed()", wrap_root(function()
|
|||
end
|
||||
end))
|
||||
|
||||
TEST("batch()", wrap_root(function()
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local batch = vide.batch
|
||||
|
||||
do CASE "evaluation deferred"
|
||||
local a = source(0)
|
||||
|
||||
local count = { b = 0, b2 = 0, c = 0 }
|
||||
|
||||
local b = derive(function()
|
||||
count.b += 1
|
||||
return a() + 1
|
||||
end)
|
||||
|
||||
local b2 = derive(function()
|
||||
count.b2 += 1
|
||||
return a() + 2
|
||||
end)
|
||||
|
||||
local c = derive(function()
|
||||
count.c += 1
|
||||
return b() + b2()
|
||||
end)
|
||||
|
||||
batch(function()
|
||||
a(1)
|
||||
CHECK(count.b == 1)
|
||||
CHECK(count.b2 == 1)
|
||||
CHECK(count.c == 1)
|
||||
end)
|
||||
|
||||
CHECK(count.b == 2)
|
||||
CHECK(count.b2 == 2)
|
||||
CHECK(count.c == 2)
|
||||
|
||||
CHECK(b() == 2)
|
||||
CHECK(c() == 5)
|
||||
end
|
||||
|
||||
do CASE "recursive call"
|
||||
local a1 = source(0)
|
||||
local a2 = source(0)
|
||||
local a3 = source(0)
|
||||
|
||||
local count = { b1 = 0, b2 = 0, b3 = 0 }
|
||||
|
||||
local b1 = derive(function()
|
||||
count.b1 += 1
|
||||
return a1() + 1
|
||||
end)
|
||||
|
||||
local b2 = derive(function()
|
||||
count.b2 += 1
|
||||
return a2() + 1
|
||||
end)
|
||||
|
||||
local b3 = derive(function()
|
||||
count.b3 += 1
|
||||
return a3() + 1
|
||||
end)
|
||||
|
||||
batch(function()
|
||||
a1(1)
|
||||
batch(function()
|
||||
a2(2)
|
||||
end)
|
||||
a3(3)
|
||||
CHECK(count.b1 == 1)
|
||||
CHECK(count.b2 == 1)
|
||||
CHECK(count.b3 == 1)
|
||||
end)
|
||||
|
||||
CHECK(count.b1 == 2)
|
||||
CHECK(count.b2 == 2)
|
||||
CHECK(count.b3 == 2)
|
||||
|
||||
CHECK(b1() == 2)
|
||||
CHECK(b2() == 3)
|
||||
CHECK(b3() == 4)
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("read()", wrap_root(function()
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local read = vide.read :: any -- todo
|
||||
|
||||
do CASE "read primitive"
|
||||
CHECK(read(1) == 1)
|
||||
end
|
||||
|
||||
do CASE "read source"
|
||||
local src = source(1) :: () -> number
|
||||
CHECK(read(src) == 1)
|
||||
end
|
||||
|
||||
do CASE "track source"
|
||||
local src = source(0)
|
||||
|
||||
local count = 0
|
||||
effect(function()
|
||||
read(src)
|
||||
count += 1
|
||||
end)
|
||||
|
||||
src(1)
|
||||
CHECK(count == 2)
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("nested effects cases", function()
|
||||
local vide = require "src/init"
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
local cleanup = vide.cleanup
|
||||
local root = vide.root
|
||||
|
||||
local ran = 0
|
||||
local cleaned = 0
|
||||
|
||||
local function Count()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
count()
|
||||
ran += 1
|
||||
cleanup(function() cleaned += 1 end)
|
||||
end)
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function App(destroy)
|
||||
local name = source "a"
|
||||
|
||||
effect(function()
|
||||
name()
|
||||
untrack(Count)
|
||||
end)
|
||||
|
||||
CHECK(ran == 1)
|
||||
CHECK(cleaned == 0)
|
||||
|
||||
name "b"
|
||||
|
||||
CHECK(ran == 2)
|
||||
CHECK(cleaned == 1)
|
||||
|
||||
destroy()
|
||||
|
||||
CHECK(ran == 2)
|
||||
CHECK(cleaned == 2)
|
||||
end
|
||||
|
||||
root(App)
|
||||
end)
|
||||
|
||||
TEST("graph edge cases", wrap_root(function()
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local root = vide.root
|
||||
|
||||
do CASE "diamond A,B,C,D"
|
||||
--[[
|
||||
|
||||
a > b > d
|
||||
> c >
|
||||
|
||||
]]
|
||||
|
||||
local a = source(0)
|
||||
|
||||
local b = derive(function() return (a() % 2 == 0) and 1 or 0 end)
|
||||
local c = derive(function() return a() * 2 end)
|
||||
local d = derive(function() return b() + c() end)
|
||||
|
||||
local count = { b = 0, c = 0, d = 0 }
|
||||
effect(function() b(); count.b += 1 end)
|
||||
effect(function() c(); count.c += 1 end)
|
||||
effect(function() d(); count.d += 1 end)
|
||||
|
||||
a(1)
|
||||
CHECK(count.b == 2)
|
||||
CHECK(count.c == 2)
|
||||
CHECK(count.d == 2)
|
||||
CHECK(d() == 2)
|
||||
|
||||
a(3)
|
||||
CHECK(count.b == 2)
|
||||
CHECK(count.c == 3)
|
||||
CHECK(count.d == 3)
|
||||
CHECK(d() == 6)
|
||||
end
|
||||
|
||||
do CASE "diamond A,B,C,D,E"
|
||||
--[[
|
||||
|
||||
a > b > e
|
||||
> c > d >
|
||||
|
||||
]]
|
||||
|
||||
local a = source(0)
|
||||
|
||||
local b = derive(function() return (a() % 2 == 0) and 1 or 0 end)
|
||||
local c = derive(function() return a() * 2 end)
|
||||
local d = derive(function() return c() * 2 end)
|
||||
local e = derive(function() return b() + d() end)
|
||||
|
||||
local count = { b = 0, c = 0, d = 0, e = 0 }
|
||||
effect(function() b(); count.b += 1 end)
|
||||
effect(function() c(); count.c += 1 end)
|
||||
effect(function() d(); count.d += 1 end)
|
||||
effect(function() e(); count.e += 1 end)
|
||||
|
||||
CHECK(e() == 1)
|
||||
|
||||
a(1)
|
||||
|
||||
CHECK(count.b == 2)
|
||||
CHECK(count.c == 2)
|
||||
CHECK(count.d == 2)
|
||||
CHECK(count.e == 3) -- todo: redundant re-eval
|
||||
CHECK(e() == 4)
|
||||
|
||||
a(3)
|
||||
CHECK(count.b == 2)
|
||||
CHECK(count.c == 3)
|
||||
CHECK(count.d == 3)
|
||||
CHECK(count.e == 4)
|
||||
CHECK(e() == 12)
|
||||
end
|
||||
|
||||
do CASE "repeated read"
|
||||
local a = source(0)
|
||||
local b = derive(function() return a() + a() end)
|
||||
|
||||
local count = 0
|
||||
effect(function() b(); count += 1 end)
|
||||
|
||||
a(1)
|
||||
CHECK(b() == 2)
|
||||
CHECK(count == 2)
|
||||
end
|
||||
|
||||
do CASE "do not destroy children"
|
||||
local parent = source(0)
|
||||
|
||||
local
|
||||
destroy,
|
||||
parent_to_destroy,
|
||||
update_parent_to_destroy
|
||||
= root(function(destroy)
|
||||
local src = source(0)
|
||||
return
|
||||
destroy,
|
||||
derive(function() return src() end),
|
||||
src
|
||||
end)
|
||||
|
||||
local count = 0
|
||||
|
||||
effect(function()
|
||||
count += 1
|
||||
parent()
|
||||
parent_to_destroy()
|
||||
end)
|
||||
|
||||
parent(parent() + 1)
|
||||
CHECK(count == 2)
|
||||
update_parent_to_destroy(1)
|
||||
CHECK(count == 3)
|
||||
|
||||
destroy()
|
||||
|
||||
update_parent_to_destroy(2)
|
||||
CHECK(count == 3)
|
||||
|
||||
parent(parent() + 1)
|
||||
CHECK(count == 4)
|
||||
end
|
||||
|
||||
do CASE "double destroy"
|
||||
-- issue:
|
||||
-- parent evaluates
|
||||
-- child A queued
|
||||
-- child B queued
|
||||
-- child A destroys child B
|
||||
-- child B reevaluates due to already being queued
|
||||
-- parent destroys, destroys child B - uh oh
|
||||
|
||||
local
|
||||
destroy_parent,
|
||||
parent,
|
||||
update_parent
|
||||
= root(function(destroy)
|
||||
local src = source(0)
|
||||
return
|
||||
destroy,
|
||||
derive(function() return src() end),
|
||||
src
|
||||
end)
|
||||
|
||||
local destroy_child, _child_B = function() end, nil
|
||||
|
||||
local count_A = 0
|
||||
|
||||
-- child_A
|
||||
effect(function()
|
||||
count_A += 1
|
||||
parent()
|
||||
destroy_child()
|
||||
end)
|
||||
|
||||
local count_B = 0
|
||||
destroy_child, _child_B = root(function(destroy)
|
||||
return
|
||||
destroy,
|
||||
derive(function() count_B += 1; return parent() end)
|
||||
end)
|
||||
|
||||
update_parent(parent() + 1)
|
||||
CHECK(count_A == 2)
|
||||
CHECK(count_B == 1) -- child B should not run again
|
||||
destroy_parent() -- should not error
|
||||
CHECK(true)
|
||||
end
|
||||
end))
|
||||
|
||||
TEST("strict", wrap_root(function()
|
||||
vide.strict = true
|
||||
|
||||
|
|
@ -1640,6 +2269,22 @@ TEST("strict", wrap_root(function()
|
|||
|
||||
CHECK(ok)
|
||||
end
|
||||
|
||||
do CASE "effect counter"
|
||||
local src = source(true)
|
||||
|
||||
local count = 0
|
||||
|
||||
effect(function(x: number)
|
||||
src()
|
||||
count = x + 1
|
||||
return count
|
||||
end, count)
|
||||
|
||||
CHECK(count == 2)
|
||||
src(not src())
|
||||
CHECK(count == 4)
|
||||
end
|
||||
end))
|
||||
|
||||
local ok = FINISH()
|
||||
|
|
|
|||
14
todo.md
14
todo.md
|
|
@ -1,14 +1,4 @@
|
|||
# todo
|
||||
|
||||
- auto-enable of strict mode depending on compiler optimizaton level
|
||||
- property binding optimization
|
||||
- would no longer allow `cleanup()` usage in binding scopes
|
||||
- solution to nested reactivity, see: SolidJS stores
|
||||
- investigate performance of wide graphs
|
||||
- optimize child removal
|
||||
- implement from solid:
|
||||
- Portal
|
||||
- batch
|
||||
- optimize `indexes()` double-diffing
|
||||
- define behavior of deriving a source within a derived source
|
||||
- review destruction of node under a root that has a child in another root
|
||||
- improve error traces
|
||||
- prevent redundant re-eval of nodes in a complex diamond graph
|
||||
|
|
|
|||
19
wally.toml
Normal file
19
wally.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "centau/vide"
|
||||
description = "A reactive Luau library for creating UI. "
|
||||
license = "MIT"
|
||||
version = "0.2.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
include = ["default.project.json", "LICENSE", "src"]
|
||||
exclude = [
|
||||
".github",
|
||||
"docs",
|
||||
"test",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".luaurc",
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
"todo.md"
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue