Compare commits

..

No commits in common. "main" and "0.3.0" have entirely different histories.
main ... 0.3.0

84 changed files with 2207 additions and 3442 deletions

View file

@ -1,33 +0,0 @@
name: release
on:
release:
types: [published]
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: setup rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: build
run: rojo build default.project.json -o build.rbxm
- name: release
run: gh release upload ${{github.event.release.tag_name}} build.rbxm

View file

@ -1,6 +1,9 @@
name: site-deploy
on:
push:
branches: [main] # todo: remove later
workflow_dispatch:
permissions:
@ -21,21 +24,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@v3
- name: Install dependencies
run: npm install
- name: Build with VitePress
run: npm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v2
with:
path: docs/.vitepress/dist
@ -49,4 +52,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v2

View file

@ -8,13 +8,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install Luau zip
uses: robinraju/release-downloader@v1.9
uses: robinraju/release-downloader@v1.6
with:
repository: Roblox/luau
tag: "0.679"
tag: "0.620"
fileName: luau-ubuntu.zip
out-file-path: bin

View file

@ -1,54 +0,0 @@
name: publish to wally and pesde
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
jobs:
wally:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: setup rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: login
run: wally login --token "${{ secrets.WALLY_TOKEN }}"
- name: publish
run: wally publish
pesde:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: setup rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: setup pesde
run: pesde install
- name: login
run: pesde auth login --token "${{ secrets.PESDE_TOKEN }}"
- name: publish
run: pesde publish --yes

8
.gitignore vendored
View file

@ -1,16 +1,10 @@
.vscode
_local
aftman.toml
sourcemap.json
docs/.vitepress/dist
docs/.vitepress/cache
docs/node_modules
docs/package-lock.json
luau_packages/
lune_packages/
.pesde/
wally.lock
pesde.lock

View file

@ -6,66 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
--------------------------------------------------------------------------------
## [0.4.1] - 2026-07-11
### Changed
- `branch()` is now allowed to be used within a reactive scope.
### Deprecated
- `create()` overloads. Supported is now only `create(class)(props)`.
### Fixed
- `create()` types in the new solver should now work without `::`.
--------------------------------------------------------------------------------
## [0.4.0] - 2026-01-17
### Added
- `create("ClassName", { props })` and `create(Instance, { props })` syntax.
- `cleanup()` now accepts `thread` types.
- Implicit effects to set children can now recursively create more implicit
effects to set children.
- `spring()` returns a second value, a setter to set position, velocity and
impulse.
- Improved `spring()` updating and unscheduling.
- `show()` now receives a source to its callback returning the current value
of the condition.
- Ignore `false` passed as a child.
- Flag `vide.defaults` to disable the setting of default properties.
- Delayed scope destruction for control flow functions: `show()` `switch()` `indexes()` `values()`.
- Better `create()` types for the new type solver.
### Changed
- A scope can no longer be destroyed while it is active. Strict mode will check
for this.
- Implicit effects to set children now unparent all children when the effect is
destroyed.
- Error reporting should be improved with better formatting when effects invoke
other effects and no more loss of stack traces.
- Nesting parent properties now work, and they are now also checked for
duplicates like other properties.
### Removed
- Aggregate construction when setting properties with `create()`.
--------------------------------------------------------------------------------
## [0.3.1] - 2024-10-09
### Added
- Context functions now also return results.
- `version` table with current version.
--------------------------------------------------------------------------------
## [0.3.0] - 2024-10-06
### Added

View file

@ -18,7 +18,7 @@ for a quick introduction to the library.
## Code sample
```luau
```lua
local create = vide.create
local source = vide.source

View file

@ -1,83 +1,70 @@
//import { defineConfig } from "vitepress"
import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config
export default withMermaid({
title: "Vide",
titleTemplate: ":title - A reactive UI library for Luau",
description: "A reactive UI library for Luau.",
base: "/vide/",
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
themeConfig: {
logo: "/logo.svg",
search: {
provider: "local"
},
footer: {
message: 'Released under the MIT License.',
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
{ text: "API", link: "/api/reactivity-core"},
],
sidebar: {
"/api/": [
{
text: "API",
items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
{ text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" },
]
}
],
"/tut/": [
{
text: "Crash Course",
items: [
{ 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: "Sources", link: "/tut/crash-course/4-source" },
{ text: "Effects", link: "/tut/crash-course/5-effect" },
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
{ text: "Actions", link: "/tut/crash-course/12-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
]
},
{
text: "Advanced",
items: [
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
]
},
{
text: "Design Patterns",
items: [
]
}
],
},
socialLinks: [
{ icon: "github", link: "https://github.com/centau/vide" }
]
}
})
//import { defineConfig } from "vitepress"
import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config
export default withMermaid({
title: "Vide",
titleTemplate: ":title - A reactive UI library for Luau",
description: "A reactive UI library for Luau.",
base: "/vide/",
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
themeConfig: {
logo: "/logo.svg",
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
{ text: "API", link: "/api/reactivity-core"},
],
sidebar: {
"/api/": [
{
text: "API",
items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
{ text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" },
]
}
],
"/tut/": [
{
text: "Crash Course",
items: [
{ 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: "Sources", link: "/tut/crash-course/4-source" },
{ text: "Effects", link: "/tut/crash-course/5-effect" },
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
{ 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: "Actions", link: "/tut/crash-course/12-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
]
},
{
text: "Advanced Reactivity",
items: [
{ text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
]
}
],
},
socialLinks: [
{ icon: "github", link: "https://github.com/centau/vide" }
]
}
})

View file

@ -1,25 +0,0 @@
.home > * > .VPNavBar:not(.top) {
backdrop-filter: blur(0.5rem);
background-color: transparent !important;
}
.home > * > .VPNavBar > .divider {
opacity: 0;
}
.home > .VPContent {
display: flex;
justify-content: center;
flex-direction: column;
}
.VPHome {
margin-top: auto !important;
margin-bottom: auto !important;
}
.VPBadge a {
text-decoration: none;
color: inherit
}

View file

@ -1,8 +1,4 @@
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './vars.css'
import './index.css'
export default {
extends: DefaultTheme,
}
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './vars.css'
export default DefaultTheme

View file

@ -1,52 +1,3 @@
/* Colors */
:root {
--vp-c-brand-1: #3086ff;
--vp-c-brand-2: #75aeff;
--vp-button-brand-bg: #3661a2;
--vp-button-brand-hover-bg: #24447f;
--vp-button-brand-press-bg: #4896f3;
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#3661a2,
#4896f3
);
--vp-home-hero-logo-background: -webkit-linear-gradient(
120deg,
#3661a2,
#4896f3
);
--vp-home-hero-image-filter: blur(96px);
--vp-c-bg: #f2f5f8;
--vp-c-bg-alt: #dfe8f5;
--vp-c-bg-elv: #dde7f4;
--vp-c-bg-soft: #e8f1fe;
--vp-c-border: #c0c3c6;
--vp-c-divider: #dfe2e6;
--vp-c-gutter: #dfe2e6;
--vp-plugin-tabs-tab-bg: var(--vp-c-bg);
--vp-badge-info-bg: #122d26;
--vp-badge-info-text: #6bdbbd;
--vp-badge-tip-bg: #132741;
--vp-badge-tip-text: #70abfa;
}
.dark {
--vp-c-brand-2: #234782;
--vp-c-bg: #0d131b;
--vp-c-bg-alt: #111720;
--vp-c-bg-elv: #182231;
--vp-c-bg-soft: #182231;
--vp-c-border: #111720;
--vp-c-divider: #1d273c;
--vp-c-gutter: #181d27;
}
:root {
--vp-c-brand-1: #3086ff;
}

View file

@ -1,30 +1,25 @@
# Animation
# Animation API
## spring() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
## spring()
Returns a new source with a value always moving torwards the input source value.
- **Type**
```luau
```lua
function spring<T>(
source: () -> T & Animatable,
period: number = 1,
damping_ratio: number = 1
): (() -> T, SpringControl<T>)
): () -> T
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
type SpringControl<T> = ({
position: T?,
velocity: T?,
impulse: T?
}) -> ()
```
- **Details**
Creates a reactive scope internally to detect source updates.
An effect is created to update the new source every frame based on the input
source value.
The movement is physically simulated according to a
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
@ -44,7 +39,3 @@ Returns a new source with a value always moving torwards the input source value.
You can change when the solver runs by calling `vide.step(dt)`, which will
advance the simulation time by `dt` seconds and automatically stop the
solver running in heartbeat.
::: warning
Large periods or damping ratios can break the spring.
:::

View file

@ -1,4 +1,39 @@
# Element Creation
# Element Creation API
<br/>
## mount()
Runs a function in a new stable scope and optionally applies its result to a
target instance.
- **Type**
```lua
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
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 stable scope, just like
[root()](reactivity-core.md#root).
Returns a function that when called will destroy the stable scope.
- **Example**
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
mount(App, game.StarterGui)
```
## create()
@ -6,11 +41,11 @@ Creates a new UI element, applying any given properties.
- **Type**
```luau
```lua
function create(class: string): (Properties) -> Instance
function create(instance: Instance): (Properties) -> Instance
type Properties = Map<string|number, unknown>
type Properties = Map<string|number, any>
```
- **Details**
@ -41,53 +76,73 @@ Creates a new UI element, applying any given properties.
Basic element creation.
```luau
local frame = create "TextButton" {
Name = "Button",
Size = UDim2.fromOffset(200, 160),
Activated = function()
print "clicked"
end,
create "UICorner" {}
```lua
local frame = create "Frame" {
Name = "NewFrame",
Position = UDim2.fromScale(1, 0)
}
```
A component using property nesting.
```lua
type Layout = {
Layout = {
Position: UDim2?,
Size: UDim2?,
AnchorPoint: Vector2?
}
}
type Children = {
Children = Array<Instance>
}
function Background(props: Layout & Children & {
Color: Color3
})
return create "Frame" {
BackgroundColor3 = props.Color,
props.Layout,
props.Children
}
end
```
## action()
Creates a special object that can be passed to `create()` to invoke custom
actions on instances.
Creates a callback that can be passed to `create()` to invoke custom actions on
instances.
- **Type**
```luau
```lua
function action((Instance) -> (), priority: number = 1): Action
```
- **Details**
When passed to `create()`, the function is called with the instance being
created as the only argument. Actions take precedence over property and
child assignments.
When passed to `create()`, the given callback is called with the instance
being created as the only argument. Actions take precedence over property
and child assignments.
A priority can be optionally specified to ensure certain actions run after
other actions. Lower priority values are ran first.
other actions. Higher priority numbers are ran after lower priority numbers.
- **Example**
An action to listen to changed properties:
```luau
local function changed(property: string, fn: (new) -> ())
```lua
local function changed(property: string, callback: (new) -> ())
return action(function(instance)
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
fn(instance[property])
local con - instance:GetPropertyChangedSignal(property):Connect(function()
callback(instance[property])
end)
-- disconnect on scope destruction to allow gc of instance
-- disconnect on reactive scope destruction to allow gc of instance
cleanup(function()
cn:Disconnect()
con:Disconnect()
end)
end)
end
@ -95,7 +150,7 @@ actions on instances.
local output = source ""
create "TextBox" {
-- will update the output source anytime the text property is changed
-- will update the `output` source anytime the text property is changed
changed("Text", output)
}
```
@ -106,47 +161,16 @@ A wrapper for `action()` to listen for property changes.
- **Type**
```luau
function changed(property: string, fn: (unknown) -> ()): Action
```lua
function changed(property: string, callback: (...unknown) -> ()): Action
```
- **Details**
Will run the given function immediately and whenever the property updates.
Will run the given callback any time the property is changed, as well as
when the action is initially run.
The function is called with the updated property value.
The changed connection is disconnected when the scope the action is ran in
is destroyed.
Runs with an action priority of 1.
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
Runs a function in a new stable scope and optionally applies its result to a
target instance.
- **Type**
```luau
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
This is a utility for `root()` when parenting a component to an existing
instance.
The result of the function is applied to a target in the same way
properties are using `create()`.
Returns a function that when called will destroy the stable scope.
- **Example**
```luau
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
local destroy = mount(App, game.StarterGui)
```

View file

@ -1,44 +1,36 @@
# Reactivity: Core
# Reactivity API: Core
## Scopes
Vide code can run in one of two scopes: <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge> or <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>.
- Reactive scopes rerun if a source read within updates.
- Stable scopes never rerun.
- Reactive scopes cannot be created directly within another reactive scope.
- When a scope is destroyed, all scopes created within are also destroyed.
Different functions in Vide's API will run code in different scopes.
<br/>
:::warning
Yielding is not allowed in any stable or reactive scope. Strict mode will check
for this.
:::
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
## root()
Runs a function in a new stable scope.
Creates and runs a function in a new stable scope.
- **Type**
```luau
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
type Destructor = () -> ()
```lua
function root<T...>(fn: (() -> ()) -> T...): (() -> (), T...)
```
- **Details**
Returns a destructor and any values returned by the callback.
Returns a function to destroy the root scope. Also passes this function as
the first argument into its callback.
All values returned by the callback are also returned following the destructor.
## source()
Creates a new source.
Creates a new source with the given value.
- **Type**
```luau
```lua
function source<T>(value: T): Source<T>
type Source<T> =
@ -48,108 +40,83 @@ Creates a new source.
- **Details**
Call the returned source with no argument to read its value.
Call the returned source with an argument to set its value.
Calling the returned source with no argument will return its stored value,
calling with an argument will set a new value.
- **Example**
```luau
```lua
local count = source(0)
print(count())-- 0
count(count() + 1)
print(count()) -- 1
count() -- 0
count(count() + 1) -- 1
```
## effect() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
## effect()
Runs a function in a new reactive scope.
Runs a side-effect in a new reactive scope on source update.
- **Type**
```luau
function effect(fn: () -> ())
```lua
function effect(callback: () -> ())
```
- **Details**
The function is ran once immediately.
Any time a source referenced in the callback is updated, the callback will
be reran.
The callback is ran once immediately.
- **Example**
```luau
local count = source(1)
```lua
local num = source(1)
effect(function()
print(count())
print(num())
end)
-- prints 1
count(2)
num(num() + 1)
-- prints 2
```
## derive() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
## derive()
Runs a function in a new reactive scope to compute a value for new source.
Derives a new source in a new reactive scope from existing sources.
- **Type**
```luau
function derive<T>(fn: () -> T): () -> T
```lua
function derive<T>(source: () -> T): () -> T
```
- **Details**
Anytime the reactive scope reruns, the output source value is set to what is
returned.
The derived source will have its value recalculated when any source source
it derives from is updated.
The function is ran once immediately.
Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
The callback is ran once immediately.
- **Example**
```luau
```lua
local count = source(0)
local text = derive(function() return `count: {count()}` end)
print(text()) -- "count: 0"
text() -- "count: 0"
count(1)
print(text()) -- "count: 1"
text() -- "count: 1"
```
A `derive()` should be used instead of a pure function when you expect it to
be read multiple times between updates, because `derive()` will cache the
result to prevent recomputing it on every read.
::: code-group
```luau [Pure Function]
local count = source(0)
local text = function()
print "ran"
return `count: {count()}`
end
count(1)
print(text()) -- prints "ran" followed by "count: 1"
print(text()) -- prints "ran" followed by "count: 1"
```
```luau [Derived Source]
local count = source(0)
local text = derive(function() -- [!code highlight]
print "ran"
return `count: {count()}`
end) -- [!code highlight]
count(1) -- prints "ran"
print(text()) -- prints "count: 1"
print(text()) -- prints "count: 1"
```
:::
--------------------------------------------------------------------------------

View file

@ -1,229 +0,0 @@
# Reactivity: Dynamic Scopes
Dynamic scopes are scopes that are created or destroyed in response to
source updates. Vide provides functions for some common use-cases for dynamic scopes.
## show() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component if the source is truthy. Optionally shows a fallback component
if the source is falsey.
- **Type**
```luau
function show<T>(source: () -> unknown, component: Constructor<T>): () -> T?
function show<T, U>(source: () -> unknown, component: Constructor<T>, fallback: () -> U): () -> T | U
type Constructor<T> = () -> (T, number?)
```
- **Details**
Creates a reactive scope internally to detect source updates.
The component is run in a stable scope when truthy, otherwise the stable
scope is destroyed.
Returns a source holding an instance of the currently shown component or
`nil` if no component is currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
## switch() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows one of a set of components depending on a source and a mapping table.
- **Type**
```luau
function switch<K, V>(source: () -> K): (map: Map<K, Constructor<V>>): () -> V?
type Constructor<T> = () -> (T, number?)
```
- **Details**
Creates a reactive scope internally to detect source updates.
When the source updates, its value is inputted into a map to get a component
constructor. This component is then run in a stable scope. The previous
stable scope is destroyed.
Returns a source holding an instance of the currently shown component or
`nil` if no component is currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
- **Example**
```luau
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component for each index in a table.
- **Type**
```luau
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
constructor: (value: () -> VI, index: KI) -> (VO, number?)
): Array<VO>
```
- **Details**
Creates a reactive scope internally to detect source updates.
When the source table updates, a component is generated for each index in
the table.
- For any added index, the `constructor` function is run in a new stable
scope to produce an instance that is cached.
- For any removed index, the stable scope for that index is destroyed.
The `constructor` function is called with:
1. A *source containing the index's value*.
2. The *index itself*.
Anytime an existing index's value changes, the `constructor` function is not
rerun, instead, that index's corresponding source is updated with the new
value.
Returns a source holding an array of instances currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
- **Example**
```luau
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return i .. ": " .. item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
}
end)
```
## values() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
Shows a component for each value in a table.
- **Type**
```luau
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
constructor: (value: VI, index: () -> KI) -> (VO, number?)
): Array<VO>
- **Details**
Operates with the same idea as `indexes()`, but applied to values instead of
indexes.
Creates a reactive scope internally to detect source updates.
When the source table updates, a component is generated for each value in
the table.
- For any added value, the `constructor` function is run in a new stable scope
to produce an instance that is cached.
- For any removed value, the stable scope for that value is destroyed.
The `constructor` function is called with:
1. The *value itself*.
2. A *source containing the value's index*.
Anytime an existing value's index changes, the `constructor` function is not
rerun, instead, that value's corresponding source is updated with the new
index.
Returns a source holding an array of instances currently shown.
Destruction of the scope can be delayed by returning the number of seconds
to delay by, after the component.
::: warning
Having the same values appear multiple times in the input source table can
cause unexpected behavior. Strict mode has checks for this.
:::
- **Example**
```luau
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = function()
return i() .. ": " .. item.Name
end
Image = "rbxassetid://" .. item.icon,
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive values. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will produce the same observed result.
The main difference is performance, picking the right function to use can
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 source index can be used to animate a change in position for the UI
element.
--------------------------------------------------------------------------------

218
docs/api/reactivity-flow.md Normal file
View file

@ -0,0 +1,218 @@
# Reactivity API: Control Flow
<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 stable scope. If it changes from a
truthy to falsey value, the stable 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 stable scope, and destroyed
when the input source switches back to truthy.
## switch()
Shows one of a set of components depending on an input source and a mapping table.
- **Type**
```lua
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
```
- **Details**
Returns a source holding an instance of the currently shown component.
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 stable
scope. If the input source changes, the stable scope the component was
created in will be destroyed, and a new component created under a new
stable scope. If no component is found for an input value, the switch will
output `nil`.
- **Example**
```lua
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **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 stable
scope to produce a new instance.
- For any removed index, the stable 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.
1. First argument is a *source containing the index's value*.
2. Second argument is the *index itself*.
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.
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return i .. ": " .. item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
Returns a source holding an array of instances currently shown.
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 stable
scope to produce a new instance.
- For any removed value, the stable 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.
::: warning
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**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = function()
return i() .. ": " .. item.Name
end
Image = "rbxassetid://" .. item.icon,
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive values. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
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. 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 be used to animate a change in position for the UI element.
--------------------------------------------------------------------------------

View file

@ -1,47 +1,41 @@
# Reactivity: Utility
# Reactivity API: Utility
## cleanup()
Queues a callback to run when a scope is reran or destroyed.
Runs a callback anytime a scope is reran or destroyed.
- **Type**
```luau
function cleanup(v: Function | Disconnectable | Destroyable | thread)
```lua
function cleanup(callback: () -> ())
function cleanup(obj: Destroyable)
function cleanup(obj: Disconnectable)
type Function = () -> ()
type Destroyable = { destroy: () -> () }
type Disconnectable = { disconnect: () -> () }
```
- **Example**
```luau
local count = source(0)
```lua
local data = source(1)
local destroy = root(function()
effect(function()
count()
effect(function()
local label = create "TextLabel" { Text = data() }
cleanup(function()
print "cleaned"
end)
cleanup(function()
label:Destroy()
end)
end
-- nothing printed yet
count(1) -- prints "cleaned"
count(2) -- prints "cleaned"
destroy() -- prints "cleaned"
end)
```
## untrack() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
## untrack()
Runs a function in a new stable scope.
Runs a given function in a new stable scope.
- **Type**
```luau
```lua
function untrack<T>(source: () -> T): T
```
@ -52,7 +46,7 @@ Runs a function in a new stable scope.
- **Example**
```luau
```lua
local a = source(0)
local b = source(0)
@ -61,84 +55,65 @@ Runs a function in a new stable scope.
end)
print(sum()) -- 0
b(1) -- untracked so reactive scope created by derive() does not rerun
b(1)
print(sum()) -- 0
a(1) -- reactive scope created by derive() reruns
a(1)
print(sum()) -- 2
```
## read()
Utility used to read a value that is either a primitive or a source.
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**
```luau
```lua
function read<T>(value: T | () -> T): T
```
## batch()
Runs a function where any source updates made within the function do not
trigger effects until after the function ends.
Runs a given function where any source updates made within the function do not
trigger effects until after the function finishes running.
- **Type**
```luau
```lua
function batch(fn: () -> ())
```
- **Details**
Improves performance when an effect depends on multiple sources, and those
sources need to be updated.
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.
- **Example**
```luau
local a = source(0)
local b = source(0)
effect(function()
print(a() + b())
end)
-- prints "0"
batch(function()
a(1) -- no print
b(2) -- no print
end)
-- prints "3"
```
## context() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
## context()
Creates a new context.
- **Type**
```luau
```lua
function context<T>(default: T): Context<T>
type Context<T> =
() -> T -- get
& <U>(T, () -> U) -> U -- set
& (T, () -> ()) -> () -- set
```
- **Details**
Calling `context()` returns a new context function.
Call this function with no arguments to get the context value.
Call this function with a value and a function to create a new context with
the given value.
The new context is run under a stable scope.
Call this function with a value and a callback to set a new context with the
given value.
- **Example**
```luau
```lua
local theme = context()
local function Button()
@ -156,3 +131,4 @@ Creates a new context.
end)
```
--------------------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
Strict mode is library-wide and can get set by doing:
```luau
```lua
vide.strict = true
```
@ -14,24 +14,25 @@ and identifying improper usage.
Currently, strict mode will:
1. Run reactive scopes twice when a source updates.
2. Throw an error if yields occur where they are not allowed.
3. Checks for `indexes()` and `values()` outputting primitive values.
4. Checks for `values()` input having duplicate values.
5. Checks for duplicate nested properties at same depth.
6. Checks for destruction of an active scope.
7. Better error reporting and stack traces.
1. Run derived sources twice a source updates.
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 `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 reactive scopes twice each time they update, it helps ensure that
computations are pure, and that any cleanup is done 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 will catch.
As well as additional safety checks, Vide will dedicate extra resources to
recording and better emitting stack traces where errors occur, particularly
when implicit effects are created for instance property updating.
when binding properties to sources.
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 do not
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.

View file

@ -1,16 +1,14 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
pageClass: home
next:
text: 'Introduction'
link: '/tut/crash-course/1-introduction'
hero:
name: "Vide"
name: Vide
text: ""
tagline: A reactive UI library for Luau.
image:
src: /logo.svg
alt: Vide
actions:
- theme: brand
text: Tutorials
@ -19,4 +17,8 @@ hero:
text: API Reference
link: /api/reactivity-core
features:
- title: In Development
details: Not recommended for production use.
---

View file

@ -1,12 +1,14 @@
{
"type": "module",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.4.5",
"vitepress-plugin-mermaid": "2.0.17"
}
}
{
"type": "module",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "1.0.0-rc.25",
"vitepress-plugin-mermaid": "2.0.14"
}
}

View file

@ -1,4 +1,4 @@
<svg width="375" height="220" viewBox="15 7 115 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<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)"/>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

@ -1,31 +1,37 @@
<svg width="200" height="200" viewBox="0 0 152 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_ii_282_2)">
<path d="M63.659 130.625C69.1438 140.125 82.8561 140.125 88.341 130.625L144.659 33.0785C148.573 26.2993 143.068 17.9607 135.296 18.8969L106.185 22.4029C99.2887 23.2337 93.3954 27.7609 90.8157 34.21L76 71.25L57 23.75L16.7038 18.8969C8.93178 17.9607 3.42658 26.2993 7.34067 33.0785L63.659 130.625Z" fill="url(#paint0_radial_282_2)"/>
<path d="M57 23.75L76 71.25L95 119.091C90.9207 126.157 80.6246 125.862 76.9557 118.575L27.4208 20.1875L57 23.75Z" fill="url(#paint1_radial_282_2)"/>
<path d="M144.659 33.0785L88.3409 130.625C82.8561 140.125 69.1438 140.125 63.659 130.625L57 119.091L38 86.1821C45.7268 99.4265 65.3087 97.9783 71.0034 83.7416L76 71.25L95 23.75L135.296 18.8969C143.068 17.9607 148.573 26.2993 144.659 33.0785Z" fill="#4896F3"/>
<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_ii_282_2" x="6.04761" y="18.8246" width="163.905" height="166.925" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<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"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<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="24" dy="48"/>
<feGaussianBlur stdDeviation="96"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<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="shape" result="effect1_innerShadow_282_2"/>
<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="12" dy="24"/>
<feGaussianBlur stdDeviation="48"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<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_innerShadow_282_2" result="effect2_innerShadow_282_2"/>
<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_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(76 149.625) rotate(-90) scale(133 153.575)">
<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_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(61.75 19) rotate(74.3578) scale(123.317 72.2423)">
<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>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

@ -1,148 +0,0 @@
# Dynamic Scopes
Dynamic scopes are scopes that are created and destroyed in response to
source updates. This is needed for conditionally rendering parts of your UI,
such as opening and closing menus.
While Vide provides functions for common ways to do this, this section will
show how you can implement them yourself so you are not limited by only what is
provided.
## Recreating [`show()`](/api/reactivity-dynamic#show-reactive)
The most basic one, `show()`, can be
implemented yourself like so:
```luau
local function show(toggle: () -> unknown, component: () -> Instance)
return derive(function()
return if toggle() then untrack(component) else nil
end)
end
```
The main thing to note here is the use of `untrack()`. This function runs its
callback in a new stable scope. Without this, if the component were to create
a reactive scope, an error would occur since a reactive scope cannot be created
within a reactive scope.
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#444455",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#444455"
}
}}%%
graph
subgraph derive ["derive (reactive)"]
subgraph untrack ["untrack (stable)"]
subgraph effect ["effect (reactive)"]
end
end
end
```
You can see from the above graph how the effect would not be created directly
inside the derive, there is a stable scope between them. This requirement exists
as a guard against unintentional rerendering of UI.
## Recreating [`switch()`](/api/reactivity-dynamic#switch-reactive)
```luau
local function switch(key)
return function(map)
return derive(function()
local component = map[key()]
return if component then untrack(component) else nil
end)
end
end
```
## Recreating [`indexes()`](/api/reactivity-dynamic#indexes-reactive)
This is a more complicated function because it manages multiple scopes at the
same time, unlike the previous functions. Because some scopes may persist
between reruns, we cannot use `untrack()` anymore which automatically destroys
on rerun; we must use `root()` where the lifetime of each scope is managed
manually and independently.
```luau
local function indexes<I, VI, VO>(
input: () -> Map<I, VI>,
transform: (value: () -> VI, index: I) -> VO
)
local index_caches = {} :: Map<I, {
previous_input: VI,
output: VO,
source: (VI) -> VI,
destroy: () -> ()
}?>
-- destroy all scopes if the parent scope is destroyed
cleanup(function()
for _, cache in index_caches do
assert(cache).destroy()
end
end)
return derive(function()
local new_input = input()
-- destroy scopes of removed indexes
for i, cache in index_caches do
if new_input[i] == nil then
assert(cache).destroy()
index_caches[i] = nil
end
end
-- create scopes or update sources of added or changed index values
for i, v in new_input do
local cache = index_caches[i]
if cache == nil then -- no scope created for this index, create one
local src = source(v)
local destroy, result = root(function()
return transform(src, i)
end)
index_caches[i] = {
destroy = destroy,
source = src,
output = result,
previous_input = v
}
elseif cache.previous_input ~= v then -- scope exists, update source
cache.previous_input = v
cache.source(v)
else -- scope exists and value has not changed; do nothing
end
end
-- return the cached output values as an array
local array = table.create(#index_caches)
for _, cache in index_caches do
table.insert(array, assert(cache).output)
end
return array
end)
end
```
--------------------------------------------------------------------------------
Though the above functions are already provided to you by Vide, this serves as
an example for how you may create your own dynamic scope functions.

View file

@ -0,0 +1,192 @@
# Nested Scopes
Nesting 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
root(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 scopes 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 = root(function()
Counter()
end)
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 `root()` within the effect
to manually create and destroy a new stable scope whenever the effect reruns.
The reason for creating a stable scope is to prevent the effect from tracking
any sources that may be read inside the `Counter()` call. Otherwise, the effect
may be rerun needlessly and recreate the counter.
Alternatively, instead of using `root()`:
```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. `untrack()` creates a
stable scope inside the reactive scope, and we can create another reactive scope
inside that stable scope. 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.

View file

View file

@ -0,0 +1 @@
# show()

View file

@ -0,0 +1 @@
# switch()

View file

@ -0,0 +1 @@
# indexes()

View file

@ -0,0 +1 @@
# values()

View file

@ -6,16 +6,10 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/).
## Why Vide?
Vide's reactive and declarative API aims to let you program UI as simply as
possible, with a strong focus on how data flows through your application.
Vide provides a reactive and declarative API to simplify managing UI.
Some of Vide's main design choices:
Some of the main focuses behind Vide's design choices:
- Syntax minimal.
- Data oriented.
- Typechecking compatible.
- Instance independent.
Vide's reactivity operates with the concept
of scopes which carries a learning curve, though is what makes Vide's minimal
syntax possible. The crash course will introduce these concepts gradually.
- Minimal syntax
- Complete typechecking
- Independence from instances

View file

@ -5,14 +5,14 @@ a side-effect from a source update. Vide provides a function `cleanup()` which
is used to queue a callback for the next time a reactive scope is rerun or
destroyed, or when a stable scope is destroyed.
```luau
```lua
local root = vide.root
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local count = source(0)
local destroy = root(function()
effect(function()
local x = count()

View file

@ -0,0 +1,98 @@
# 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.
The new sources can be used in `create()` to update the children of a container
instance.
## indexes()
`indexes()` *maps* 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.
```lua
local list = source {
"finish the crash course",
"star Vide's GitHub"
}
local function TodoList(props: { list: () -> Array<string> })
return create "Frame" {
create "UIListLayout" {},
indexes(list, function(todo, i)
return create "TextLabel" {
Text = function()
return i .. ": " .. todo()
end,
LayoutOrder = i
}
end)
}
end
TodoList { list = list }
```
For each index in the given source table, the given function to `indexes()` will
be run in a new stable scope with:
1. a source containing the value at 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 stable scope destroyed.
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
```
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
```

View file

@ -1,211 +0,0 @@
# Dynamic Scopes
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 *dynamic scope* functions.
These functions create and destroy scopes for you in response to source
updates. They return a source containing the created component. This source can
be parented as a child which will update the shown children whenever the source
updates.
The simplest example is using `show()`.
```luau
local source = vide.source
local create = vide.create
local show = vide.show
local root = vide.root
function Button(props: { Text: string, Activated: () -> () })
return create "TextButton" {
Text = props.Text,
Activated = props.Activated
}
end
function Menu()
return create "TextLabel" {
Text = "This is a menu"
}
end
function App()
local toggled = source(false)
return create "ScreenGui" {
Button {
Text = "Toggle Menu",
Activated = function()
toggled(not toggled())
end
},
show(toggled, Menu) -- [!code highlight]
}
end
root(function()
App().Parent = game.StarterGui
end)
```
This is a complete example of rendering UI which has a single button that
toggles the opening of a menu.
--------------------------------------------------------------------------------
Another common function is `indexes()`. This function creates a component for
each index in a table.
Each component created is done so in a new and independent stable scope. The
indexes of the table are checked each source update to prevent redunant
destruction and recreation of UI elements.
```luau
local source = vide.source
local create = vide.create
local indexes = vide.indexes
local root = vide.root
local function Todo(props: {
Text: () -> string,
Position: number,
Activated: () -> ()
})
return create "TextButton" {
Text = function() return props.Position .. ": " .. props.Text() end,
LayoutOrder = props.Position,
Activated = Activated
}
end
local function TodoList(props: { List: () -> Array<string> })
return create "Frame" {
create "UIListLayout" {},
indexes(props.List, function(text, i) -- [!code highlight]
return Todo {
Text = text,
Position = i,
Activated = function() -- remove the todo when clicked
local list = props.List()
table.remove(list, i)
props.List(list)
end
}
end)
}
end
function App()
local list = source {
"finish the crash course",
"star Vide's GitHub"
}
return create "ScreenGui" {
TodoList { List = list },
}
end
root(function()
App().Parent = game.StarterGui
end)
```
The reactive graph for the above example:
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
graph
subgraph root ["root"]
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
```
When you edit a table in a source, you must set that table again to actually
update the source.
```luau
local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run
```
--------------------------------------------------------------------------------
All dynamic scope functions also support delaying the destruction of the scope.
This is useful for playing any sort of animation or effect before the UI
instance is removed.
If you have the following code, for example:
```lua
local function Menu()
return create "Frame" {}
end
local toggled = source(true)
create "ScreenGui" {
show(toggled, function()
return Menu {}
end)
}
toggled(false) -- menu will disappear immediately
```
```lua
local function Menu(props: { Visible: () -> boolean })
local transparency = spring(function()
return if p.Visible then 0 else 1
end
return create "Frame" {
BackgroundTransparency = transparency
}
end
local toggled = source(true)
create "ScreenGui" {
show(toggled, function(_, present)
return Menu { p.Visible = present }, 3 -- give a generous 3 seconds for the spring to complete before destroying
end)
}
toggled(false)
-- `present` will go `false` immediately
-- transparency will begin being sprung
-- after 3 seconds the scope is destroyed, giving the spring enough time to complete
```
If `toggled` goes from truthy to falsey, beginning the timer, but then back
to truthy before the timer finishes, the timer is cancelled and the scope is
not destroyed.

View file

@ -1,11 +1,13 @@
# Actions
Actions are special callbacks that you can pass along with properties,
Actions in Vide are special callbacks that you can pass along with properties,
to run some code on an instance receiving them.
```luau
```lua
local action = vide.action
```
```lua
create "TextLabel" {
Text = "test",
@ -20,15 +22,14 @@ create "TextLabel" {
Actions can be wrapped with functions for reuse. Below is an example of an
action used to listen for property changes:
```luau
```lua
local action = vide.action
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local function changed(property: string, callback: (new) -> ())
local function changed(prop: string, callback: (new) -> ())
return action(function(instance)
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
callback(instance[property])
end)
@ -48,7 +49,7 @@ effect(function()
print(output())
end)
instance.Text = "foo" -- "foo" will be printed by the effect
instance.Text = "foo" -- "foo" will be printed from the effect
```
The source `output` will be updated with the new property value any time it is

View file

@ -13,7 +13,7 @@ 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 properly.
```luau
```lua
local source = vide.source
local effect = vide.effect
@ -23,7 +23,6 @@ local count = source(0)
local ran = 0
effect(function()
count()
ran += 1
end)

View file

@ -30,7 +30,7 @@ Created by:
- `root()`
- `untrack()`
- `show()`
- `switch()`
- `indexes()`
Stable scopes do not track sources and never rerun.
@ -47,14 +47,23 @@ Created by:
Reactive scopes do track sources and will rerun when those sources update.
Reactive scopes cannot be created within a reactive scope, but stable scopes
can be created within a reactive scope.
can.
## Scope Cleanup
## Scope Owners
When a scope is rerun or destroyed, all scopes created within it are
automatically destroyed.
A scope created within another scope is *owned* by the other scope, with the
exception of the scope created by `root()`.
Any functions queued by `cleanup()` are also ran.
When a scope is rerun or destroyed, all scopes owned by it are automatically
destroyed.
`root()` creates a stable scope with no owner, instead it is destroyed manually.
## Cleanup
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
Queue a function to run using `cleanup()`.
## Reactive Graph
@ -64,12 +73,12 @@ relationships between effects and the sources they depend on.
### Code
```luau
```lua
local count = source(0)
root(function()
local text = derive(function()
return "count: " .. count()
return "count: " .. text()
end)
effect(function()
@ -84,12 +93,12 @@ end)
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryColor": "#1B1B1F",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#202530"
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#1C1C1F"
}
}}%%
@ -109,5 +118,6 @@ Notes:
- An update to `count` will cause `text` to rerun, which
then causes `effect` to rerun.
- When the root scope is destroyed, `text` and
`effect` will be destroyed alongside it, since they were created within it.
`count` will be untouched and future updates to `count` will have no effect.
`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.

View file

@ -5,7 +5,7 @@ Instances are created using `create()`.
Parentheses `()` can be omitted when calling functions with string or
table literals for brevity.
```luau
```lua
local create = vide.create
return create "ScreenGui" {

View file

@ -10,7 +10,7 @@ together.
::: code-group
```luau [Button.luau]
```lua [Button.luau]
local create = vide.create
local function Button(props: {
@ -34,7 +34,7 @@ end
return Button
```
```luau [Menu.luau]
```lua [Menu.luau]
local create = vide.create
local Button = require(Button)

View file

@ -5,7 +5,7 @@ Vide's reactivity.
A source can be created using `source()`.
```luau
```lua
local source = vide.source
local count = source(0)
@ -16,13 +16,13 @@ The value passed to `source()` is the initial value of the source.
The value of a source can be set by calling it with an argument, and can be read
by calling it with no arguments.
```luau
```lua
count(count() + 1) -- increment count by 1
```
Sources can be *derived* by wrapping them in functions.
```luau
```lua
local count = source(0)
local text = function()

View file

@ -1,11 +1,11 @@
# Effects
Effects are functions that are ran in response to source updates.
Effects are functions that are ran in response to source updates. They are
A source and effect is analogous to a signal and connection.
Effects are created using `effect()`.
```luau
```lua
local source = vide.source
local effect = vide.effect
@ -23,13 +23,10 @@ count(1)
Any source read inside an effect is tracked and will rerun the effect when
that source is updated.
The effect runs its callback once immediately to initially figure out what
sources are being read.
Derived sources are also tracked, it does not matter how deeply nested
Derived sources are also tracked, it doesn't matter how deeply nested
inside a function a source is.
```luau
```lua
local source = vide.source
local effect = vide.effect
@ -50,22 +47,3 @@ count(2)
If a source is updated with the same value it already had, it will not rerun
effects depending on it.
You can also read from a source within an effect without the effect tracking it.
```luau
local source = vide.source
local effect = vide.effect
local untrack = vide.untrack
local a = source(0)
local b = source(0)
effect(function()
print(`a: {a()} b: {untrack(b)}`)
end)
a(1) -- prints "a: 1 b: 0"
b(1) -- prints nothing
a(2) -- prints "a: 2 b: 1"
```

View file

@ -7,46 +7,62 @@ But the disconnecting of many signals and connections is tedious and verbose.
Vide instead operates on the concept of scopes which provides a much cleaner
API, given that you follow a few rules.
There are two types of scopes: stable and reactive.
Scopes come in two flavors; stable and reactive.
- A scope must be created within another scope.
- Stable scopes never rerun.
- Reactive scopes can rerun.
- A reactive scope cannot be created within another reactive scope, only within
a stable scope.
- All scopes must be created within another scope with the exception of `root()`
- Stable scopes never rerun
- Reactive scopes can rerun
- A reactive scope cannot be created within another reactive scope
An exception to the first rule is `root()`, which creates the initial scope that
you destroy manually with a destructor function it returns.
`root()` creates a stable scope.
`effect()` creates a reactive scope.
`root()` creates a stable scope.
Whenever a scope is destroyed, any scope created within that scope is also
destroyed, and so on.
destroyed, and so on. This is why all scopes must be created within another
scope, except `root()` which is used to create the initial scope that you can
manually destroy.
```luau
```lua
local root = vide.root
local source = vide.source
local effect = vide.effect
local count = source(0)
local function setup()
local count = source(0)
effect(function()
print(count())
end)
return count
end
setup() -- error, effect() tried to create a reactive scope with no stable scope
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
local destroy = root(setup) -- ok since effect() was called in a stable scope
local count = root(setup) -- ok since effect() was called within a stable scope
count(1) -- prints "1"
```
The scope created by `root()` can be destroyed.
```lua
local function setup()
local count = source(0)
effect(function()
print(count())
end)
return count
end
local destroy, count = root(setup)
count(1) -- prints "1"
count(2) -- prints "2"
destroy()
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
count(2) -- effect is destroyed; no longer prints
```
Vide's reactivity can be represented graphically, as a *reactive graph*.
@ -57,12 +73,12 @@ The reactive graph for the above example looks like so:
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryColor": "#1B1B1F",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#161618"
}
}}%%
@ -74,7 +90,7 @@ subgraph root
end
```
When the stable `root()` scope is destroyed, the reactive `effect()`
When the stable `root()` is destroyed, the reactive `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

View file

@ -1,9 +1,11 @@
# Reactive Components
# Stateful Components
Reactive components in Vide are created using sources and effects - sources to
Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
```luau
## Internal State
```lua
local create = vide.create
local source = vide.source
local effect = vide.effect
@ -31,13 +33,19 @@ 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.
## External State
External sources can also be passed into components for them to use.
```luau
local function CountDisplay(props: { count: () -> number })
```lua
local function Counter(props: { count: () -> number })
local count = props.count
local instance = create "TextLabel" {}
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
effect(function()
instance.Text = "count: " .. count()
@ -48,11 +56,11 @@ end
local count = source(0)
CountDisplay {
Counter {
count = count
}
count(1) -- the CountDisplay component will update to display this 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

View file

@ -3,9 +3,7 @@
Explicitly creating effects to update properties is tedious. You can
*implicitly* create an effect to update properties instead.
::: code-group
```luau [Implicit Effect]
```lua
local create = vide.create
local source = vide.source
@ -24,30 +22,6 @@ local function Counter()
end
```
```luau [Explicit Effect]
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
```
:::
This example is equivalent to the example seen on the previous page.
Instead of explicitly creating an effect, assigning a (non-event) property a
@ -60,7 +34,7 @@ with a number key instead of string key) can return an instance or an array of
instances. An effect is automatically created to unparent removed instances and
parent new instances on source update.
```luau
```lua
local items = source {
create "TextLabel" { Text = "A" }
}
@ -72,12 +46,12 @@ local function List(props: { children: () -> { Instance } })
}
end
local list = List { children = items } -- creates a list with text label "A"
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 text label "A", and parent labels "B" and "C"
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
```

View file

@ -2,7 +2,7 @@
We have seen the basic way to derive a source:
```luau
```lua
local count = source(0)
local text = function()
@ -18,7 +18,7 @@ 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.
```luau
```lua
local source = vide.source
local effect = vide.effect
@ -39,7 +39,7 @@ To avoid this, you can use `derive()` to derive a new source instead. This will
run a function in a reactive scope only when a source used inside updated.
Reading this derived source multiple times will just return a cached result.
```luau
```lua
local source = vide.source
local effect = vide.effect
local derive = vide.derive
@ -57,8 +57,7 @@ effect(function() text() end)
count(1) -- prints "ran" x1
```
Because `derive()` creates a reactive scope, it must be called within a stable
scope, just like `effect()`.
`derive()` must also be called within a stable 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.
@ -69,12 +68,12 @@ The reactive graph for the above example:
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryColor": "#1B1B1F",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#161618"
}
}}%%
@ -87,7 +86,7 @@ end
```
Deriving a source in this manner is similar to creating an effect to update
another source. You should avoid doing this using an effect however.
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
source.

View file

@ -1,10 +0,0 @@
local vide = require "@self/src/lib"
export type source<T> = vide.source<T>
export type Source<T> = vide.Source<T>
export type context<T> = vide.context<T>
export type Context<T> = vide.Context<T>
export type Instances = vide.Instances
export type Properties<T> = vide.Properties<T>
return vide

View file

@ -1,15 +0,0 @@
name = "centau/vide"
version = "0.4.1"
description = "A reactive Luau library for creating UI."
authors = ["centau"]
repository = "https://github.com/centau/vide"
license = "MIT"
includes = ["src/*", "README.md", "pesde.toml"]
[target]
environment = "roblox"
build_files = ["src"]
lib = "src/init.luau"
[indices]
default = "https://github.com/pesde-pkg/index"

View file

@ -1,9 +0,0 @@
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
# For more information, see https://github.com/rojo-rbx/rokit
# New tools can be added by running `rokit add <tool>` in a terminal.
[tools]
pesde = "pesde-pkg/pesde@0.6.2+registry.0.2.2"
wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"

View file

@ -1,20 +1,24 @@
local typeof = game and typeof or require "../test/mock".typeof :: never
if not game then script = require "test/relative-string" end
local typeof = game and typeof or require "test/mock".typeof :: never
local Vector2 = game and Vector2 or require "test/mock".Vector2 :: never
local UDim2 = game and UDim2 or require "test/mock".UDim2 :: never
local flags = require "./flags"
local implicit_effect = require "./implicit_effect"
local _, is_action = require "./action"()
local graph = require "./graph"
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local _, is_action = require(script.Parent.action)()
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
type Array<V> = { V }
type ArrayOrV<V> = {ArrayOrV<V>} | V
type Map<K, V> = { [K]: V }
type Cache = {
local free_caches: {
-- event listeners to connect after properties are set
events: Array<
| string -- 1. event name
| () -> () -- 2. listener
events: Map<
string, -- event name
() -> () -- listener
>,
-- actions to run after events are connected
@ -23,34 +27,30 @@ type Cache = {
Array<(Instance) -> ()> -- action callbacks
>,
-- what to parent the instance to after running actions
parent: unknown,
-- cache to detect duplicate property setting at same nesting depth
nested_debug: Map<
number, -- depth
Map<string, true> -- set of property names
>,
-- 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 }
nested_stack: { {} | number }
}
}?
local free_cache: Cache?
local function borrow_cache(): Cache
if free_cache then
local cache = free_cache
free_cache = nil
return cache
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
}),
parent = nil,
nested_debug = setmetatable({} :: any, {
__index = function(self, i: number) self[i] = {}; return self[i] end
}),
@ -59,61 +59,36 @@ local function borrow_cache(): Cache
end
end
local function return_cache(cache: Cache )
free_cache = cache
local function return_caches(caches: typeof(free_caches) )
free_caches = caches
end
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
for property, value in properties do
if type(property) == "string" then
if flags.strict then -- check for duplicate property assignment at nesting depth
if cache.nested_debug[depth][property] then
error(`duplicate property {property} at depth {depth}`, 0)
end
cache.nested_debug[depth][property] = true
end
if property == "Parent" then
cache.parent = value
continue
end
if type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
table.insert(cache.events, property) -- add event name to buffer
table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
else
implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property
end
else
(instance :: any)[property] = value -- set property
end
elseif type(property) == "number" then
if type(value) == "function" then
implicit_effect.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
elseif type(value) == "table" then
if is_action(value) then
table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
elseif flags.defer_nested_properties then
table.insert(cache.nested_stack, value :: {})
table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
else
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
end
elseif type(value) == "userdata" then
(value :: Instance).Parent = instance -- parent child
end
end
end
-- map of datatype names to class default constructor for aggregate init
local aggregates = {}
for name, class in {
CFrame = CFrame,
Color3 = Color3,
UDim = UDim,
UDim2 = UDim2,
Vector2 = Vector2,
Vector3 = Vector3,
Rect = Rect
} :: 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
error "attempt to call a constructor returned by create() with no properties"
throw("attempt to call a constructor returned by create() with no properties")
end
local caches = borrow_cache()
local strict = flags.strict
-- 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
@ -122,39 +97,79 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
-- process all properties
local depth = 1
repeat
process_properties(properties, instance, caches, depth)
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 depth
if nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`)
end
nested_debug[depth][property] = true
end
if type(value) == "table" then -- attempt aggregate init
local ctor = aggregates[typeof((instance :: any)[property])]
if ctor == nil then
throw(`cannot aggregate type {typeof(value)} for property {property}`)
end
(instance :: any)[property] = ctor(unpack(value :: {}))
elseif type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
events[property] = value :: () -> () -- add event to buffer
else
bind.property(instance, property, value :: () -> ()) -- bind property
end
else
(instance :: any)[property] = value -- set property
end
elseif type(property) == "number" then
if type(value) == "function" then
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
elseif type(value) == "table" then
if is_action(value) then
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
end
else
(value :: Instance).Parent = instance -- parent child
end
end
end
depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {}
until not properties
for i = 1, #events, 2 do
local event_name = events[i]
local event_listener = events[i + 1]
;(instance :: any)[event_name]:Connect(event_listener)
for event, listener in next, events do
(instance :: any)[event]:Connect(listener)
end
for _, queued in actions do
for _, callback in queued do
for _, queued in next, actions do
for _, callback in next, queued do
callback(instance)
end
end
local parent = caches.parent
-- finally set parent if any
if parent then
if type(parent) == "function" then
implicit_effect.parent(instance, parent :: () -> Instance)
bind.parent(instance, parent :: () -> Instance)
else
instance.Parent = parent :: Instance
end
end
-- clear caches
table.clear(events)
for _, queued in actions do table.clear(queued) end
caches.parent = nil
if flags.strict then table.clear(nested_debug) end
for _, queued in next, actions do table.clear(queued) end
if strict then table.clear(nested_debug) end
table.clear(nested_stack)
return_cache(caches)
return_caches(caches)
return instance
end

View file

@ -1,5 +1,8 @@
local flags = require "./flags"
local graph = require "./graph"
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
@ -10,14 +13,14 @@ local function batch(setter: () -> ())
from = graph.get_update_queue_length()
end
local ok, err: string? = xpcall(setter, debug.traceback)
local ok, err: string? = pcall(setter)
if not already_batching then
flags.batch = false
graph.flush_update_queue(from)
end
if not ok then error(`error occured while batching updates: {err}`, 0) end
if not ok then throw(`error occured while batching updates: {err}`) end
end
return batch

105
src/bind.luau Normal file
View file

@ -0,0 +1,105 @@
if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node
function create_implicit_effect<T>(updater: (T) -> T, binding: T)
evaluate_node(create_node(assert_stable_scope(), updater, binding))
end
type PropertyBinding = {
instance: Instance,
property: string,
source: () -> unknown
}
local function update_property_effect(p: PropertyBinding)
(p.instance :: any)[p.property] = p.source()
return p
end
type ParentBinding = {
instance: Instance,
parent: () -> Instance
}
local function update_parent_effect(p: ParentBinding)
p.instance.Parent = p.parent()
return p
end
type ChildrenBinding = {
instance: Instance,
cur_children_set: { [Instance]: true },
new_children_set: { [Instance]: true },
children: () -> Instance | { Instance }
}
type ArrayOrV<V> = V | { V }
local function update_children_effect(p: ChildrenBinding)
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
local new_children = p.children() -- all (and only) children that should be parented after this update
if type(new_children) ~= "table" then
new_children = { new_children }
end
local function process_child(child: ArrayOrV<Instance>)
if type(child) == "table" then
for _, child in next, child do
process_child(child)
end
else
if new_child_set[child] then return end -- stops redundant reparenting
new_child_set[child] = true -- record child set from this update
if not cur_children_set[child] then
child.Parent = p.instance -- if child wasn't already parented then parent it
else
cur_children_set[child] = nil -- remove child from cache if it was already in cache
end
end
end
process_child(new_children)
for child in next, cur_children_set do
child.Parent = nil -- unparent all children that weren't in the new children set
end
table.clear(cur_children_set) -- clear cache, preserve capacity
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
return p
end
return {
property = function(instance, property, source)
return create_implicit_effect(update_property_effect, {
instance = instance,
property = property,
source = source
})
end,
parent = function(instance, parent)
return create_implicit_effect(update_parent_effect, {
instance = instance,
parent = parent
})
end,
children = function(instance, children)
return create_implicit_effect(update_children_effect, {
instance = instance,
cur_children_set = {},
new_children_set = {},
children = children
})
end
}

View file

@ -1,40 +0,0 @@
local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local destroy = graph.destroy
local get_scope = graph.get_scope
local function branch<T>(fn: () -> T): (() -> (), T)
local current = get_scope()
if not current then
error(`cannot use branch() outside a stable or reactive scope`, 0)
end
local parent = current.owner
if not parent then
error(`current scope is not owned by a scope`, 0)
end
local node = create_node(parent, false, false)
local destroy = function()
destroy(node)
end
push_scope(node)
local ok, result = xpcall(fn, debug.traceback)
pop_scope()
if not ok then
destroy()
error(`error while running branch():\n\n{result}`, 0)
end
return destroy, result
end
return branch

View file

@ -1,5 +1,7 @@
local action = require "./action"()
local cleanup = require "./cleanup"
if not game then script = require "test/relative-string" end
local action = require(script.Parent.action)()
local cleanup = require(script.Parent.cleanup)
local function changed<T>(property: string, callback: (T) -> ())
return action(function(instance)

View file

@ -1,26 +1,27 @@
local typeof = game and typeof or require "../test/mock".typeof :: never
if not game then script = require "test/relative-string" end
local typeof = game and typeof or require "test/mock".typeof :: never
local graph = require "./graph"
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
local get_scope = graph.get_scope
local push_cleanup = graph.push_cleanup
local function helper(obj: any)
return
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
elseif type(obj) == "thread" then function() task.cancel(obj) 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 error "cannot cleanup given object"
else throw("cannot cleanup given object")
end
local function cleanup(value: unknown)
local scope = get_scope()
if not scope then
error "cannot cleanup outside a stable or reactive scope"
throw "cannot cleanup outside a stable or reactive scope"
end; assert(scope)
if type(value) == "function" then
@ -35,7 +36,6 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
return cleanup ::
( (callback: () -> ()) -> () ) &
( (thread: thread) -> () ) &
( (instance: Destroyable) -> () ) &
( (connection: Disconnectable) -> () ) &
( (instance: Instance) -> () ) &

View file

@ -1,4 +1,7 @@
local graph = require "./graph"
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 create_node = graph.create_node
local get_scope = graph.get_scope
@ -6,7 +9,7 @@ local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local set_context = graph.set_context
export type Context<T> = (() -> T) & (<U>(T, () -> U) -> U)
export type Context<T> = (() -> T) & ((T, () -> ()) -> ())
local nil_symbol = newproxy()
local count = 0
@ -18,7 +21,7 @@ local function context<T>(...: T): Context<T>
local has_default = select("#", ...) > 0
local default_value = ...
return function<T>(...): any -- todo: fix type error
return function(...)
local scope: Node<unknown>? | false = get_scope()
if select("#", ...) == 0 then -- get
@ -43,10 +46,10 @@ local function context<T>(...: T): Context<T>
if has_default ~= nil then
return default_value
else
error("attempt to get context when no context is set and no default context is set", 0)
throw("attempt to get context when no context is set and no default context is set")
end
else -- set
if not scope then return error("attempt to set context outside of a vide scope", 0) end
if not scope then return throw("attempt to set context outside of a vide scope") end
local value, component = ...
@ -61,10 +64,8 @@ local function context<T>(...: T): Context<T>
pop_scope()
if not ok then
error(`error while running context:\n\n{result}`, 0)
throw(`error while running context:\n\n{result}`)
end
return result
end
return nil :: any

View file

@ -1,119 +1,84 @@
local typeof = game and typeof or require "../test/mock".typeof :: never
local Instance = game and Instance or require "../test/mock".Instance :: never
if not game then script = require "test/relative-string" end
local typeof = game and typeof or require "test/mock".typeof:: never
local Instance = game and Instance or require "test/mock".Instance :: never
local defaults = require "./defaults"
local apply = require "./apply"
local flags = require "./flags"
local throw = require(script.Parent.throw)
local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply)
local function create_constructor_for_class(class: string): ({ [unknown]: unknown }) -> Instance
local function constructor(properties: { [unknown]: unknown }): Instance
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 error(`invalid class name {class}`, 0) end
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
if flags.defaults then
local default: { [string]: unknown }? = defaults[class]
if default then
for i, v in default do
(instance :: any)[i] = v
end
local default: { [string]: unknown }? = defaults[class]
if default then
for i, v in next, default do
(instance :: any)[i] = v
end
end
return apply(instance, properties)
end
local function ctor(properties: Props): Instance
return apply(instance:Clone(), properties)
end
return constructor
self[class] = ctor
return ctor
end
})
local function create_instance(class: string)
return ctor_cache[class]
end
local constructor_cache = {} :: { [string]: ({ [unknown]: unknown }) -> Instance }
-- todo: remove support for different overloads
local function create(class_or_instance: string|Instance, properties: { [unknown]: unknown }?): unknown
if type(class_or_instance) ~= "string" and typeof(class_or_instance) ~= "Instance" then
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
local function clone_instance(instance: Instance)
return function(properties: Props): Instance
local clone = instance:Clone()
if not clone then throw "attempt to clone a non-archivable instance" end
return apply(clone, properties)
end
end
local constructor: ({ [unknown]: unknown }) -> Instance
local function create(class_or_instance: string|Instance): (Props) -> Instance
if type(class_or_instance) == "string" then
constructor = constructor_cache[class_or_instance]
if not constructor then
constructor = create_constructor_for_class(class_or_instance)
constructor_cache[class_or_instance] = constructor
end
return create_instance(class_or_instance)
elseif typeof(class_or_instance) == "Instance" then
return clone_instance(class_or_instance)
else
constructor = function(props)
local clone = assert(class_or_instance:Clone(), "attempt to clone a non-archivable instance")
return apply(clone, props)
end
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
return nil :: never
end
return if properties
then constructor(properties)
else constructor
end
export type Instances = {
Folder: Folder,
BillboardGui: BillboardGui,
CanvasGroup: CanvasGroup,
Frame: Frame,
ImageButton: ImageButton,
ImageLabel: ImageLabel,
ScreenGui: ScreenGui,
ScrollingFrame: ScrollingFrame,
SurfaceGui: SurfaceGui,
TextBox: TextBox,
TextButton: TextButton,
TextLabel: TextLabel,
UIAspectRatioConstraint: UIAspectRatioConstraint,
UICorner: UICorner,
UIGradient: UIGradient,
UIGridLayout: UIGridLayout,
UIListLayout: UIListLayout,
UISizeConstraint: UISizeConstraint,
UITextSizeConstraint: UITextSizeConstraint,
UIScale: UIScale,
UIPadding: UIPadding,
UIStroke: UIStroke,
UIFlexItem: UIFlexItem,
UIPageLayout: UIPageLayout,
UITableLayout: UITableLayout,
VideoFrame: VideoFrame,
ViewportFrame: ViewportFrame,
ProximityPrompt: ProximityPrompt,
UIDragDetector: UIDragDetector,
Camera: Camera,
WorldModel: WorldModel,
}
export type function Properties(instance: type?)
local properties = types.newtable()
while instance do
for i, v in instance:properties() do
local connector = v.read and v.read.tag == "table" and v.read:readproperty(types.singleton("Connect"))
if connector then
local params = connector:parameters().head
if not params then continue end
local listener = params[2]
if not listener then continue end
properties:setproperty(i, types.optional(listener))
elseif v.write then
properties:setproperty(i, types.optional(types.unionof(
v.write,
types.newfunction({}, { head = { v.write } })
)))
end
end
instance = instance:readparent()
end
properties:setindexer(types.number, types.any)
return properties
end
type Create = <Name>(Name|keyof<Instances>|"") -> (Properties<index<Instances, Name>>) -> index<Instances, Name>
return create :: Create
type Props = { [any]: any }
return (create :: any) ::
( <T>(T & Instance) -> (Props) -> T ) &
( ("Folder") -> (Props) -> Folder ) &
( ("BillboardGui") -> (Props) -> BillboardGui ) &
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
( ("Frame") -> (Props) -> Frame ) &
( ("ImageButton") -> (Props) -> ImageButton ) &
( ("ImageLabel") -> (Props) -> ImageLabel ) &
( ("ScreenGui") -> (Props) -> ScreenGui ) &
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
( ("TextBox") -> (Props) -> TextBox ) &
( ("TextButton") -> (Props) -> TextButton ) &
( ("TextLabel") -> (Props) -> TextLabel ) &
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
( ("UICorner") -> (Props) -> UICorner ) &
( ("UIGradient") -> (Props) -> UIGradient ) &
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
( ("UIListLayout") -> (Props) -> UIListLayout ) &
( ("UIPadding") -> (Props) -> UIPadding ) &
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
( ("UIScale") -> (Props) -> UIScale ) &
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
( ("UIStroke") -> (Props) -> UIStroke ) &
( ("UITableLayout") -> (Props) -> UITableLayout ) &
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
( ("VideoFrame") -> (Props) -> VideoFrame ) &
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
( (string) -> (Props) -> Instance )

View file

@ -1,10 +1,11 @@
local Enum = game and Enum or require "../test/mock".Enum :: never
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
local Enum = game and Enum or require "test/mock".Enum :: never
local Color3 = game and Color3 or require "test/mock".Color3 :: never
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
return {
Part = {
Material = Enum.Material.SmoothPlastic,
Size = vector.create(1, 1, 1),
Size = Vector3.new(1, 1, 1),
Anchored = true
},
@ -110,4 +111,4 @@ return {
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
}
} :: { [string]: { [string]: unknown} }
}

View file

@ -1,6 +1,8 @@
local graph = require "./graph"
if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
local create_node = graph.create_node
local push_scope_as_child_of = graph.push_scope_as_child_of
local push_child_to_scope = graph.push_child_to_scope
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node
@ -10,7 +12,7 @@ local function derive<T>(source: () -> T): () -> T
evaluate_node(node)
return function()
push_scope_as_child_of(node)
push_child_to_scope(node)
return node.cache
end
end

View file

@ -1,4 +1,6 @@
local graph = require "./graph"
if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node

View file

@ -4,9 +4,4 @@ end
local is_O2 = inline_test() ~= "inline_test"
return {
strict = not is_O2,
defaults = true,
defer_nested_properties = true,
batch = false,
}
return { strict = not is_O2, batch = false }

View file

@ -1,4 +1,7 @@
local flags = require "./flags"
if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
export type SourceNode<T> = {
cache: T,
@ -21,23 +24,9 @@ export type Node<T> = {
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
local function efn(err: string)
local trace = debug.traceback(err, 2)
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
return "\n "
end)
end
trace ..= "\nsource update stacktrace:"
return trace
end
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
local thread = coroutine.create(xpcall)
--local function efn(err: string) return debug.traceback(err, 3) end
local function efn(err: string) return debug.traceback(err, 3) end
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
assert(resume_ok)
@ -58,9 +47,9 @@ local function assert_stable_scope(): Node<unknown>
if not scope then
local caller_name = debug.info(2, "n")
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
elseif scope.effect then
error("cannot create a new reactive scope inside another reactive scope", 0)
throw("cannot create a new reactive scope inside another reactive scope")
end
return scope
@ -93,9 +82,9 @@ end
local function flush_cleanups<T>(node: Node<T>)
if node.cleanups then
for _, fn in node.cleanups do
local ok, err: string? = xpcall(fn, debug.traceback)
if not ok then error(`cleanup error: {err}`, 0) end
for _, fn in next, node.cleanups do
local ok, err: string? = pcall(fn)
if not ok then throw(`cleanup error: {err}`) end
end
table.clear(node.cleanups)
@ -119,10 +108,6 @@ local function unparent<T>(node: Node<T>)
end
local function destroy<T>(node: Node<T>)
if flags.strict and table.find(scopes, node) then
error("attempt to destroy an active scope", 0)
end
flush_cleanups(node)
unparent(node)
@ -148,10 +133,6 @@ local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
local function evaluate_node<T>(node: Node<T>)
if flags.strict then
if table.find(scopes, node) then
error("a scope, that should rerun due to the update of a source, is already active", 0)
end
local initial_value = node.cache
for i = 1, 2 do
@ -167,7 +148,7 @@ local function evaluate_node<T>(node: Node<T>)
if not ok then
table.clear(update_queue)
update_queue.n = 0
error(`effect error stacktrace\n{new_value :: string}`, 0)
throw(`effect stacktrace:\n{new_value :: string}`)
end
node.cache = new_value :: T
@ -187,7 +168,7 @@ local function evaluate_node<T>(node: Node<T>)
if not ok then
table.clear(update_queue)
update_queue.n = 0
error(`effect error:\n{new_value}\n`, 0)
throw(`effect stacktrace:\n{new_value}\n`)
end
node.cache = new_value
@ -249,7 +230,7 @@ local function update_descendants<T>(root: SourceNode<T>)
update_queue.n = n0
end
local function push_scope_as_child_of<T>(node: SourceNode<T>)
local function push_child_to_scope<T>(node: SourceNode<T>)
local scope = get_scope()
if scope and scope.effect then -- do not track nodes with no effect
push_child(node, scope)
@ -306,7 +287,7 @@ return table.freeze {
push_cleanup = push_cleanup,
destroy = destroy,
flush_cleanups = flush_cleanups,
push_scope_as_child_of = push_scope_as_child_of,
push_child_to_scope = push_child_to_scope,
update_descendants = update_descendants,
push_child = push_child,
create_node = create_node,
@ -315,7 +296,5 @@ return table.freeze {
flush_update_queue = flush_update_queue,
get_update_queue_length = get_update_queue_length,
set_context = set_context,
scopes = scopes,
q = update_queue
scopes = scopes
}

View file

@ -1,125 +0,0 @@
local graph = require "./graph"
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope
local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node
local push_cleanup = graph.push_cleanup
local function update_property_effect(p: {
instance: Instance,
property: string,
source: () -> unknown
})
(p.instance :: any)[p.property] = p.source()
return p
end
local function update_parent_effect(p: {
instance: Instance,
source: () -> Instance
})
p.instance.Parent = p.source()
return p
end
-- todo: investigate if "count" method used in indexes() and values() can improve performance here
local function update_children_effect(p: {
instance: Instance,
cur_children_set: { [Instance]: true },
new_children_set: { [Instance]: true },
source: () -> Instance | { Instance }
})
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
local new_children_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
local new_children = p.source() -- all (and only) children that should be parented after this update
local function process_child(child: Instance | { Instance })
if type(child) == "userdata" then
if new_children_set[child] then return end -- stops redundant reparenting
new_children_set[child] = true -- record child set from this update
if not cur_children_set[child] then
child.Parent = p.instance -- if child wasn't already parented then parent it
else
cur_children_set[child] = nil -- remove child from cache if it was already in cache
end
elseif type(child) == "table" then
for _, child in child do
process_child(child)
end
elseif type(child) == "function" then
local node = create_node(assert(get_scope()), update_children_effect, {
instance = p.instance,
cur_children_set = {},
new_children_set = {},
source = child
})
evaluate_node(node)
push_cleanup(assert(get_scope()), function()
for child in node.cache.cur_children_set do
child.Parent = nil
end
end)
end
end
process_child(new_children)
for child in cur_children_set do
child.Parent = nil -- unparent all children that weren't in the new children set
end
table.clear(cur_children_set) -- clear cache, preserve capacity
p.cur_children_set, p.new_children_set = new_children_set, cur_children_set
return p
end
return {
property = function(instance, property, source)
local node = create_node(assert_stable_scope(), update_property_effect, {
instance = instance,
property = property,
source = source
})
evaluate_node(node)
return node
end,
parent = function(instance, parent)
local node = create_node(assert_stable_scope(), update_parent_effect, {
instance = instance,
source = parent
})
evaluate_node(node)
return node
end,
children = function(instance, children)
local node = create_node(assert_stable_scope(), update_children_effect, {
instance = instance,
cur_children_set = {},
new_children_set = {},
source = children
})
evaluate_node(node)
push_cleanup(assert_stable_scope(), function()
for child in node.cache.cur_children_set do
child.Parent = nil
end
end)
return node
end
}

View file

@ -1,121 +0,0 @@
local flags = require "./flags"
local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()
type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T
local function indexes<K, V, Obj>(
input: Source<Map<K, V>>,
component: (Source<V>, K, Source<boolean>) -> (Obj, ...number)
): Source<Array<Obj>>
local update_count = 0
local scopes = {} :: Map<K, {
destroy: () -> (),
object: Obj,
value: V?,
value_source: (V?) -> V,
count: number,
delay: number,
present: (boolean?) -> boolean,
timeout: { cancel: boolean }?,
}>
local output = source({} :: Array<Obj>)
local function update_output()
local objects = table.create(4)
for _, scope in scopes do
table.insert(objects, scope.object)
end
output(objects)
end
effect(function()
local data = input()
local count = update_count
update_count += 1
local children_need_update = false -- set to true if a scope is created or destroyed
-- create or update scopes
for i, v in data do
local scope = scopes[i]
if scope == nil then -- create new scope and create component
local value_source = source(v)
local present = source(false)
local delay = nil :: number?
local destroy, object = branch(function()
local object, t = component(value_source, i, present)
delay = t
return object
end)
present(true)
children_need_update = true
scopes[i] = {
destroy = destroy,
object = object,
value = v,
value_source = value_source,
count = count,
delay = delay or 0,
present = present,
timeout = nil,
}
else -- update scope
scope.count = count
if scope.value ~= v then
if scope.timeout then -- index is in input table again; cancel destruction
scope.timeout.cancel = true
scope.timeout = nil
scope.present(true)
end
scope.value = v
scope.value_source(v)
end
end
end
-- destroy scopes
for i, scope in scopes do
if scope.count < count then -- if count is not latest then index is no longer in the input table
scope.present(false)
if scope.delay == 0 then
scope.destroy()
scopes[i] = nil
children_need_update = true
else
scope.value = nil -- set to nil for the `scope.value ~= v` check
if scope.timeout == nil then
scope.timeout = timeout(scope.delay, function() -- todo: possible redundant updates
scope.destroy()
scopes[i] = nil
update_output()
end)
end
end
end
end
if children_need_update then
update_output()
end
end)
return output
end
return indexes

View file

@ -1,12 +1,118 @@
assert(game, "when using vide outside of Roblox, require lib.luau instead")
--------------------------------------------------------------------------------
-- vide.luau
-- v0.3.0
--------------------------------------------------------------------------------
local vide = require("@self/lib")
if not game then script = require "test/relative-string" end
export type source<T> = vide.source<T>
export type Source<T> = vide.Source<T>
export type context<T> = vide.context<T>
export type Context<T> = vide.Context<T>
export type Instances = vide.Instances
export type Properties<T> = vide.Properties<T>
local root = require(script.root)
local mount = require(script.mount)
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 read = require(script.read)
local batch = require(script.batch)
local context = require(script.context)
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)()
local changed = require(script.changed)
local throw = require(script.throw)
local flags = require(script.flags)
export type Source<T> = source.Source<T>
export type source<T> = Source<T>
export type Context<T> = context.Context<T>
export type context<T> = Context<T>
local function step(dt: number)
if game then
debug.profilebegin("VIDE STEP")
debug.profilebegin("VIDE SPRING")
end
update_springs(dt)
if game then
debug.profileend()
debug.profileend()
end
end
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
task.defer(step, dt)
end)
local vide = {
-- core
root = root,
mount = mount,
create = create,
source = source,
effect = effect,
derive = derive,
switch = switch,
show = show,
indexes = indexes,
values = values,
-- util
cleanup = cleanup,
untrack = untrack,
read = read,
batch = batch,
context = context,
-- animations
spring = spring,
-- actions
action = action,
changed = changed,
-- flags
strict = (nil :: any) :: boolean,
-- temporary
apply = function(instance: Instance)
return function(props: { [any]: any })
apply(instance, props)
return instance
end
end,
-- runtime
step = function(dt: number)
if stepped then
stepped:Disconnect()
stepped = nil
end
step(dt)
end
}
setmetatable(vide :: any, {
__index = function(_, index: unknown): ()
if index == "strict" then
return flags.strict
else
throw(`{tostring(index)} is not a valid member of vide`)
end
end,
__newindex = function(_, index: unknown, value: unknown)
if index == "strict" then
flags.strict = value :: boolean
else
throw(`{tostring(index)} is not a valid member of vide`)
end
end
})
return vide

View file

@ -1,122 +0,0 @@
local version = { major = 0, minor = 4, patch = 1 }
local root = require "./root"
local branch = require "./branch"
local mount = require "./mount"
local create = require "./create"
local apply = require "./apply"
local source = require "./source"
local effect = require "./effect"
local derive = require "./derive"
local cleanup = require "./cleanup"
local untrack = require "./untrack"
local read = require "./read"
local batch = require "./batch"
local context = require "./context"
local switch = require "./switch"
local show = require "./show"
local indexes = require "./indexes"
local values = require "./values"
local spring, update_springs = require "./spring"()
local action = require "./action"()
local changed = require "./changed"
local timeout, update_timeouts = require "./timeout"()
local flags = require "./flags"
export type Source<T> = source.Source<T>
export type source<T> = Source<T>
export type Context<T> = context.Context<T>
export type context<T> = Context<T>
export type Instances = create.Instances
export type Properties<T> = create.Properties<T>
local function step(dt: number)
if game then debug.profilebegin("VIDE STEP") end
if game then debug.profilebegin("VIDE SPRING") end
update_springs(dt)
if game then debug.profileend() end
if game then debug.profilebegin("VIDE SCHEDULER") end
update_timeouts(dt)
if game then debug.profileend() end
if game then debug.profileend() end
end
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
task.defer(step, dt)
end)
local vide = {
version = version,
-- core
root = root,
--branch = branch,
mount = mount,
create = create,
source = source,
effect = effect,
derive = derive,
switch = switch,
show = show,
indexes = indexes,
values = values,
-- util
cleanup = cleanup,
untrack = untrack,
read = read,
batch = batch,
context = context,
-- animations
spring = spring,
-- actions
action = action,
changed = changed,
-- flags
strict = (nil :: any) :: boolean,
defaults = (nil :: any) :: boolean,
defer_nested_properties = (nil :: any) :: boolean,
-- temporary
apply = function(instance: Instance)
return function(props: { [any]: any })
apply(instance, props)
return instance
end
end,
-- runtime
step = function(dt: number)
if stepped then
stepped:Disconnect()
stepped = nil
end
step(dt)
end
}
setmetatable(vide :: any, {
__index = function(_, index: unknown): ()
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide`, 0)
else
return flags[index]
end
end,
__newindex = function(_, index: unknown, value: unknown)
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide, 0`)
else
flags[index] = value
end
end
})
return vide

219
src/maps.luau Normal file
View file

@ -0,0 +1,219 @@
if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
type SourceNode<T> = graph.SourceNode<T>
local create_node = graph.create_node
local create_source_node = graph.create_source_node
local push_child_to_scope = graph.push_child_to_scope
local update_descendants = graph.update_descendants
local assert_stable_scope = graph.assert_stable_scope
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local evaluate_node = graph.evaluate_node
local destroy = graph.destroy
type Map<K, V> = { [K]: V }
local function check_primitives(t: {})
if not flags.strict then return end
for _, v in next, t do
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
throw("table source map cannot return primitives")
end
end
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = assert_stable_scope()
local subowner = create_node(owner, false, false)
local input_cache = {} :: Map<K, VI>
local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, SourceNode<VI>>
local remove_queue = {} :: { K }
local scopes = {} :: Map<K, Node<unknown>>
local function update_children(data)
-- queue removed values
for i in next, input_cache do
if data[i] == nil then
table.insert(remove_queue, i)
end
end
-- remove queued values
for _, i in next, remove_queue do
destroy(scopes[i])
input_cache[i] = nil
output_cache[i] = nil
input_nodes[i] = nil
scopes[i] = nil
end
table.clear(remove_queue)
push_scope(subowner)
-- process new or changed values
for i, v in next, data do
local cv = input_cache[i]
if cv ~= v then
if cv == nil then -- create new scope and run transform
local scope = create_node(subowner, false, false)
scopes[i] = scope :: Node<any>
local node = create_source_node(v)
push_scope(scope)
local ok, result = pcall(transform, function()
push_child_to_scope(node)
return node.cache
end, i)
pop_scope()
if not ok then
pop_scope() -- subowner scope
error(result, 0)
end
input_nodes[i] = node
output_cache[i] = result
else -- update source
input_nodes[i].cache = v
update_descendants(input_nodes[i])
end
input_cache[i] = v
end
end
pop_scope()
local output_array = table.create(#scopes)
for _, v in next, output_cache do
table.insert(output_array, v)
end
check_primitives(output_array)
return output_array
end
local node = create_node(owner, function()
return update_children(input())
end, false :: any)
evaluate_node(node)
return function()
push_child_to_scope(node)
return node.cache
end
end
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = assert_stable_scope()
local subowner = create_node(owner, false, false)
local cur_input_cache_up = {} :: Map<VI, K>
local new_input_cache_up = {} :: Map<VI, K>
local output_cache = {} :: Map<VI, VO>
local input_nodes = {} :: Map<VI, SourceNode<K>>
local scopes = {} :: Map<VI, Node<unknown>>
local function update_children(data: Map<K, VI>)
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
if flags.strict then
local cache = {}
for _, v in next, data do
if cache[v] ~= nil then
throw "duplicate table value detected"
end
cache[v] = true
end
end
push_scope(subowner)
-- process data
for i, v in next, data do
new_input_cache[v] = i
local cv = cur_input_cache[v]
if cv == nil then -- create new scope and run transform
local scope = create_node(subowner, false, false)
scopes[v] = scope :: Node<any>
local node = create_source_node(i)
push_scope(scope)
local ok, result = pcall(transform, v, function()
push_child_to_scope(node)
return node.cache
end)
pop_scope()
if not ok then
pop_scope() -- subowner scope
error(result, 0)
end
input_nodes[v] = node
output_cache[v] = result
else -- update source
if cv ~= i then
input_nodes[v].cache = i
update_descendants(input_nodes[v])
end
cur_input_cache[v] = nil
end
end
pop_scope()
-- remove old values
for v in next, cur_input_cache do
destroy(scopes[v])
output_cache[v] = nil
input_nodes[v] = nil
scopes[v] = nil
end
-- update buffer cache
table.clear(cur_input_cache)
cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache
local output_array = table.create(#scopes)
for _, v in next, output_cache do
table.insert(output_array, v)
end
check_primitives(output_array)
return output_array
end
local node = create_node(owner, function()
return update_children(input())
end, false :: any)
evaluate_node(node)
return function()
push_child_to_scope(node)
return node.cache
end
end
return function() return indexes, values end

View file

@ -1,5 +1,7 @@
local root = require "./root"
local apply = require "./apply"
if not game then script = require "test/relative-string" end
local root = require(script.Parent.root)
local apply = require(script.Parent.apply)
local function mount<T>(component: () -> T, target: Instance?): () -> ()
return root(function()

View file

@ -1,3 +1,5 @@
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

View file

@ -1,4 +1,7 @@
local graph = require "./graph"
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 create_node = graph.create_node
local push_scope = graph.push_scope
@ -13,20 +16,21 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
refs[node] = true -- prevent gc of root node
local destroy = function()
if not refs[node] then error "root already destroyed" end
if not refs[node] then throw "root already destroyed" end
refs[node] = nil
destroy(node)
end
push_scope(node)
local result = { xpcall(fn, debug.traceback, destroy) }
local function efn(err: string) return debug.traceback(err, 3) end
local result = { xpcall(fn, efn, destroy) }
pop_scope()
if not result[1] then
destroy()
error(`error while running root():\n\n{result[2]}`, 0)
throw(`error while running root():\n\n{result[2]}`)
end
return destroy, unpack(result :: any, 2)

View file

@ -1,36 +1,18 @@
local source = require "./source"
local derive = require "./derive"
local effect = require "./effect"
local untrack = require "./untrack"
local switch = require "./switch"
if not game then script = require "test/relative-string" end
type Array<T> = { T }
type Source<T> = () -> T
local switch = require(script.Parent.switch)
local function show<T, Obj>(
input: Source<T?>,
component: (Source<T>, Source<boolean>) -> (Obj, ...number),
fallback: ((Source<boolean>) -> (Obj, ...number))?
): Source<nil | Obj | Array<Obj>>
local filtered_input = source()
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
local function truthy()
return not not source()
end
effect(function()
local v = input()
if v then
filtered_input(v)
end
end)
local input_is_truthy = derive(function()
return not not input()
end)
return switch(input_is_truthy) {
[true] = function(present)
return component(filtered_input, present)
end,
[false] = fallback
return switch(truthy) {
[true] = component,
[false] = fallback,
}
end
return show
return show ::
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)

View file

@ -1,7 +1,9 @@
local graph = require "./graph"
if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create_source_node = graph.create_source_node
local push_scope_as_child_of = graph.push_scope_as_child_of
local push_child_to_scope = graph.push_child_to_scope
local update_descendants = graph.update_descendants
export type Source<T> = (() -> T) & ((value: T) -> T)
@ -9,9 +11,9 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
local function source<T>(initial_value: T): Source<T>
local node = create_source_node(initial_value)
local function update_source(...): T
return function(...): T
if select("#", ...) == 0 then -- no args were given
push_scope_as_child_of(node)
push_child_to_scope(node)
return node.cache
end
@ -24,8 +26,6 @@ local function source<T>(initial_value: T): Source<T>
update_descendants(node)
return v
end
return update_source
end
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)

View file

@ -1,4 +1,28 @@
local graph = require "./graph"
if not game then script = require "test/relative-string" end
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
--[[
Supported datatypes:
- number
- CFrame
- Color3
- UDim
- UDim2
- Vector2
- Vector3
- Rect
Unsupported datatypes:
- bool
- Vector2int16
- Vector3int16
- EnumItem
]]
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
type SourceNode<T> = graph.SourceNode<T>
local create_node = graph.create_node
@ -6,79 +30,74 @@ local create_source_node = graph.create_source_node
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node
local update_descendants = graph.update_descendants
local push_scope_as_child_of = graph.push_scope_as_child_of
local push_child_to_scope = graph.push_child_to_scope
local UPDATE_RATE = 120
local TOLERANCE_FACTOR = 10_000
local TOLERANCE = 0.0001
type Vec3 = Vector3
local function Vec3(x: number?, y: number?, z: number?)
return Vector3.new(x, y, z)
end
local ZERO = Vec3(0, 0, 0)
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
--[[
Unsupported datatypes:
- bool
- Vector2int16
- Vector3int16
- EnumItem
]]
type SpringState<T> = {
type SpringData<T> = {
k: number, -- spring constant
c: number, -- damping coeff
x0_123: vector, x0_456: vector, -- initial position
x_123: vector, x_456: vector, -- current position
x1_123: vector, x1_456: vector, -- target position
v_123: vector, v_456: vector, -- current velocity
-- dimensions 1-3
x0_123: Vec3,
x1_123: Vec3,
v_123: Vec3,
-- dimensions 4-6
x0_456: Vec3,
x1_456: Vec3,
v_456: Vec3,
source_value: T -- current value of spring input source
}
type SpringSettings<T> = ({
position: T?,
velocity: T?,
impulse: T?
}) -> ()
type TypeToVec6<T> = (T) -> (vector, vector)
type Vec6ToType<T> = (vector, vector) -> T
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
type Vec6ToType<T> = (Vec3, Vec3) -> T
local type_to_vec6 = {
number = function(v)
return vector.create(v, 0, 0), vector.zero
return Vec3(v, 0, 0), ZERO
end :: TypeToVec6<number>,
CFrame = function(v)
return v.Position, vector.create(v:ToEulerAnglesXYZ())
return v.Position, Vec3(v:ToEulerAnglesXYZ())
end :: TypeToVec6<CFrame>,
Color3 = function(v)
-- todo: hsv, oklab?
return vector.create(v.R, v.G, v.B), vector.zero
return Vec3(v.R, v.G, v.B), ZERO
end :: TypeToVec6<Color3>,
UDim = function(v)
return vector.create(v.Scale, v.Offset, 0), vector.zero
return Vec3(v.Scale, v.Offset, 0), ZERO
end :: TypeToVec6<UDim>,
UDim2 = function(v)
return vector.create(v.X.Scale, v.X.Offset, v.Y.Scale), vector.create(v.Y.Offset, 0, 0)
return Vec3(v.X.Scale, v.X.Offset, v.Y.Scale), Vec3(v.Y.Offset, 0, 0)
end :: TypeToVec6<UDim2>,
Vector2 = function(v)
return vector.create(v.X, v.Y, 0), vector.zero
return Vec3(v.X, v.Y, 0), ZERO
end :: TypeToVec6<Vector2>,
Vector3 = function(v)
return v, vector.zero
return v, ZERO
end :: TypeToVec6<Vector3>,
Rect = function(v)
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
end :: TypeToVec6<Rect>,
table = function(v)
return vector.create(v[1] or 0, v[2] or 0, v[3] or 0), vector.create(v[4] or 0, 0, 0)
end :: TypeToVec6<{ number }>
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
end :: TypeToVec6<Rect>
}
local vec6_to_type = {
@ -112,16 +131,12 @@ local vec6_to_type = {
Rect = function(a, b)
return Rect.new(a.X, a.Y, a.Z, b.X)
end :: Vec6ToType<Rect>,
table = function(a, b)
return { a.X, a.Y, a.Z, b.X }
end :: Vec6ToType<{ number }>
end :: Vec6ToType<Rect>
}
local invalid_type = {
__index = function(_, t: string)
error(`cannot spring type {t}`, 0)
throw(`cannot spring type {t}`)
end
}
@ -130,15 +145,17 @@ setmetatable(vec6_to_type, invalid_type)
-- maps spring data to its corresponding output node
-- lifetime of spring data is tied to output node
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
setmetatable(springs :: any, { __mode = "v" })
local springs: { [SpringData<any>]: SourceNode<any> } = {}
setmetatable(springs, { __mode = "v" })
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
local owner = assert_stable_scope()
-- https://en.wikipedia.org/wiki/Damping
local w_n = 2*math.pi / (period or 1)
local z = damping_ratio or 1
local k = w_n^2
local c_c = 2*w_n
local c = z * c_c
@ -146,22 +163,20 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
-- 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
error("spring damping too high, consider reducing damping or increasing period", 0)
throw("spring damping too high, consider reducing damping or increasing period")
end
local data: SpringState<T> = {
local data: SpringData<T> = {
k = k,
c = c,
x0_123 = vector.zero,
x_123 = vector.zero,
x1_123 = vector.zero,
v_123 = vector.zero,
x0_123 = ZERO,
x1_123 = ZERO,
v_123 = ZERO,
x0_456 = vector.zero,
x_456 = vector.zero,
x1_456 = vector.zero,
v_456 = vector.zero,
x0_456 = ZERO,
x1_456 = ZERO,
v_456 = ZERO,
source_value = false :: any,
}
@ -172,57 +187,33 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
local value = source()
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
data.source_value = value
springs[data] = output
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
return value
end
local updater = create_node(owner, updater_effect, false :: any)
evaluate_node(updater)
-- set initial position to goal
data.x_123, data.x_456 = data.x1_123, data.x1_456
data.x0_123, data.x0_456 = data.x1_123, data.x1_456
-- set output to goal
output.cache = data.source_value
local config = function(p)
local x = p.position
local v = p.velocity
local dv = p.impulse
if x then
local x_123, x_456 = type_to_vec6[typeof(x)](x)
data.x_123, data.x_456 = x_123, x_456
data.x0_123, data.x0_456 = x_123, x_456
end
if v then
data.v_123, data.v_456 = type_to_vec6[typeof(v)](v)
end
if dv then
local dv_123, dv_456 = type_to_vec6[typeof(dv)](dv)
data.v_123 += dv_123
data.v_456 += dv_456
end
-- schedule spring
springs[data] = output
end :: SpringSettings<T>
return function(...)
if select("#", ...) == 0 then -- no args were given
push_scope_as_child_of(output)
push_child_to_scope(output)
return output.cache
end
-- set current position to value
local v = ... :: T
data.x_123, data.x_456 = type_to_vec6[typeof(v)](v)
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
-- reset velocity
data.v_123 = vector.zero
data.v_456 = vector.zero
data.v_123 = ZERO
data.v_456 = ZERO
-- schedule spring
springs[data] = output
@ -231,32 +222,21 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
output.cache = v
return v
end, config
end
-- calculates a float tolerance, based on the magnitude of the float
local function get_min_step(x: number): number
return x/TOLERANCE_FACTOR
end
local function get_min_vector_step(direction: vector): vector
return vector.create(
get_min_step(direction.x),
get_min_step(direction.y),
get_min_step(direction.z)
)
end
end
local function step_springs(dt: number)
for s in springs do
local k = s.k
local c = s.c
local x_123, x_456 = s.x_123, s.x_456
local x1_123, x1_456 = s.x1_123, s.x1_456
local u_123, u_456 = s.v_123, s.v_456
for data in next, springs do
local k, c,
x0_123, x1_123, u_123,
x0_456, x1_456, u_456 =
data.k, data.c,
data.x0_123, data.x1_123, data.v_123,
data.x0_456, data.x1_456, data.v_456
-- calculate displacement from target
local dx_123 = x_123 - x1_123
local dx_456 = x_456 - x1_456
local dx_123 = x0_123 - x1_123
local dx_456 = x0_456 - x1_456
-- calculate spring force
local fs_123 = dx_123*-k
@ -266,50 +246,53 @@ local function step_springs(dt: number)
local ff_123 = u_123*-c
local ff_456 = u_456*-c
-- calculate acceleration
local a_123 = (fs_123 + ff_123)
local a_456 = (fs_456 + ff_456)
-- calculate acceleration step
local dv_123 = (fs_123 + ff_123)*dt
local dv_456 = (fs_456 + ff_456)*dt
-- step acceleration
local v_123 = u_123 + a_123*dt
local v_456 = u_456 + a_456*dt
-- apply acceleration step
local v_123 = u_123 + dv_123
local v_456 = u_456 + dv_456
-- step velocity
local y_123 = x_123 + v_123*dt
local y_456 = x_456 + v_456*dt
-- calculate new position
local x_123 = x0_123 + v_123*dt
local x_456 = x0_456 + v_456*dt
s.x_123, s.x_456 = y_123, y_456
s.v_123, s.v_456 = v_123, v_456
data.x0_123, data.x0_456 = x_123, x_456
data.v_123, data.v_456 = v_123, v_456
end
end
local remove_queue = {}
local function update_spring_sources()
for data, output in springs do
local x0_123, x0_456 = data.x0_123, data.x0_456
local x_123, x_456 = data.x_123, data.x_456
local x1_123, x1_456 = data.x1_123, data.x1_456
local v_123, v_456 = data.v_123, data.v_456
for data, output in next, springs do
local x0_123, x1_123, v_123,
x0_456, x1_456, v_456 =
data.x0_123, data.x1_123, data.v_123,
data.x0_456, data.x1_456, data.v_456
local dx_123, dx_456 =
x0_123 - x1_123,
x0_456 - x1_456
local tol_123 = vector.abs(get_min_vector_step(x0_123 - x1_123))
local tol_456 = vector.abs(get_min_vector_step(x0_456 - x1_456))
if
-- position is at goal (within tolerance)
vector.max(vector.abs(x_123 - x1_123), tol_123) == tol_123
and vector.max(vector.abs(x_456 - x1_456), tol_456) == tol_456
-- velocity is at 0 (within tolerance)
and vector.max(vector.abs(v_123/10), tol_123) == tol_123
and vector.max(vector.abs(v_456/10), tol_456) == tol_456
then
springs[data] = nil
-- todo: can this false positive?
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
-- close enough to target, unshedule spring and set value to target
table.insert(remove_queue, data)
output.cache = data.source_value
else
output.cache = vec6_to_type[typeof(data.source_value)](x_123, x_456)
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
end
update_descendants(output)
end
for _, data in next, remove_queue do
springs[data] = nil
end
table.clear(remove_queue)
end
return function()

View file

@ -1,113 +1,64 @@
local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()
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>
type SourceNode<T> = graph.SourceNode<T>
local create_node = graph.create_node
local evaluate_node = graph.evaluate_node
local push_child_to_scope = graph.push_child_to_scope
local destroy = graph.destroy
local assert_stable_scope = graph.assert_stable_scope
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T
type Component<T> = (Source<boolean>) -> (T, ...number)
local function switch_map<K, Obj>(
input: Source<K>,
map: Map<K, Component<Obj>>
): Source<nil | Obj | Array<Obj>>
local scopes = {} :: Map<K, {
destroy: () -> (),
object: Obj,
delay: number,
present: (boolean?) -> boolean,
timeout: { cancel: boolean }?
}>
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
local owner = assert_stable_scope()
local output = source(nil :: nil | Obj | Array<Obj>)
local function update_output()
local objects = {}
for _, scope in scopes do
table.insert(objects, scope.object)
end
output(
if objects[2] then objects
elseif objects[1] then objects[1]
else nil
)
end
effect(function()
local key: K? = input()
-- destroy (or queue destroy) all scopes not associated with the input key
for k, scope in scopes do
if k == key then continue end
scope.present(false)
if scope.delay == 0 then
scope.destroy()
scopes[k] = nil
else
if scope.timeout == nil then
scope.timeout = timeout(scope.delay, function()
scope.destroy()
scopes[k] = nil
update_output()
end)
end
end
end
-- create new scope or abort destruction of existing scope if key exists
if key ~= nil then
local scope = scopes[key]
if scope then
scope.present(true)
if scope.timeout then
scope.timeout.cancel = true
scope.timeout = nil
end
else
local component = map[key]
if component ~= nil then
if type(component) ~= "function" then
error("map must map a value to a function", 0)
end
local present = source(false)
local delay = nil :: number?
local destroy, object = branch(function()
local object, t = component(present)
delay = t
return object
end)
present(true)
scopes[key] = {
destroy = destroy,
object = object,
delay = delay or 0,
present = present,
timeout = nil
}
end
end
end
update_output()
end)
return output
end
local function switch<K, Obj>(input: Source<K>): (map: Map<K, Component<Obj>>) -> Source<nil | Obj | Array<Obj>>
return function(map)
return switch_map(input, map)
local last_scope: Node<false>?
local last_component: (() -> U)?
local function update(cached): U?
local component = map[source()]
if component == last_component then return cached end
last_component = component
if last_scope then
destroy(last_scope :: Node<any>)
last_scope = nil
end
if component == nil then return nil end
if type(component) ~= "function" then
throw "map must map a value to a function"
end
local new_scope = create_node(owner, false, false)
last_scope = new_scope :: Node<any>
push_scope(new_scope)
local ok, result = pcall(component)
pop_scope()
if not ok then error(result, 0) end
return result
end
local node = create_node(owner, update, nil)
evaluate_node(node)
return function()
push_child_to_scope(node)
return node.cache
end
end
end

7
src/throw.luau Normal file
View file

@ -0,0 +1,7 @@
if not game then script = require "test/relative-string" end
local function VIDE_ASSERT(msg): any
error(msg, 0)
end
return VIDE_ASSERT

View file

@ -1,27 +0,0 @@
local queue = {} :: {
{ t: number, fn: () -> (), cancel: boolean }
}
local function timeout(t: number, fn: () -> ())
local handle = { t = t, fn = fn, cancel = false }
table.insert(queue, handle)
return handle
end
local function update_timeouts(dt: number)
for i = #queue, 1, -1 do
local handle = queue[i]
handle.t -= dt
if handle.cancel or handle.t <= 0 then
queue[i] = queue[#queue]
queue[#queue] = nil
if not handle.cancel then
handle.fn()
end
end
end
end
return function() return timeout, update_timeouts end

View file

@ -1,4 +1,6 @@
local graph = require "./graph"
if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local get_scope = graph.get_scope
@ -10,16 +12,16 @@ local function untrack<T>(source: () -> T): T
local effect = scope.effect
scope.effect = false
local ok, result = xpcall(source, debug.traceback)
local ok, result = pcall(source)
scope.effect = effect :: () -> ()
if not ok then error(result, 0) end
return result :: T
return result
else
return source()
end
end
return untrack :: ( <T>(fn: () -> T) -> T ) & ( (fn: () -> ()) -> () )
return untrack

View file

@ -1,131 +0,0 @@
local flags = require "./flags"
local branch = require "./branch"
local source = require "./source"
local effect = require "./effect"
local timeout = require "./timeout" ()
type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Source<T> = () -> T
local function values<K, V, Obj>(
input: Source<Map<K, V>>,
component: (V, Source<K>, Source<boolean>) -> (Obj, ...number)
): Source<Array<Obj>>
local update_count = 0
local scopes = {} :: Map<V, {
destroy: () -> (),
object: Obj,
index: K?,
index_source: (K?) -> K,
count: number,
delay: number,
present: (boolean?) -> boolean,
timeout: { cancel: boolean }?,
}>
local output = source({} :: Array<Obj>)
local function update_output()
local objects = table.create(4)
for _, scope in scopes do
table.insert(objects, scope.object)
end
output(objects)
end
effect(function()
local data = input()
local count = update_count
update_count += 1
local children_need_update = false -- set to true if a scope is created or destroyed
if flags.strict then -- check for duplicate values
local map = {}
for _, v in data do
if map[v] then
error("table source passed to `values()` contains duplicate values", 0)
end
map[v] = true
end
end
-- create or update scopes
for i, v in data do
local scope = scopes[v]
if scope == nil then -- create new scope and create component
local index_source = source(i)
local present = source(false)
local delay = nil :: number?
local destroy, object = branch(function()
local object, t = component(v, index_source, present)
delay = t
return object
end)
present(true)
children_need_update = true
scopes[v] = {
destroy = destroy,
object = object,
index = i,
index_source = index_source,
count = count,
delay = delay or 0,
present = present,
timeout = nil,
}
else -- update scope
scope.count = count
if scope.index ~= i then
if scope.timeout then -- value is in input table again; cancel destruction
scope.timeout.cancel = true
scope.timeout = nil
scope.present(true)
end
scope.index = i
scope.index_source(i)
end
end
end
-- destroy scopes
for v, scope in scopes do
if scope.count < count then -- if count is not latest then value is no longer in the input table
scope.present(false)
if scope.delay == 0 then
scope.destroy()
scopes[v] = nil
children_need_update = true
else
scope.index = nil -- set to nil for the `scope.index ~= i` check
if scope.timeout == nil then
scope.timeout = timeout(scope.delay, function() -- todo: possible redundant updates
scope.destroy()
scopes[v] = nil
update_output()
end)
end
end
end
end
if children_need_update then
update_output()
end
end)
return output
end
return values

View file

@ -1,7 +1,7 @@
local testkit = require("./testkit")
local testkit = require("test/testkit")
local BENCH, START = testkit.benchmark()
local vide = require "../../vide"
local vide = require "src/init"
local source = vide.source
local derive = vide.derive
local effect = vide.effect
@ -27,7 +27,7 @@ local function ROOT_BENCH(name: string, fn: () -> ())
end)()
end
local N = 2^20
local N = 2^18 -- 262144
TITLE "sources"
@ -200,7 +200,7 @@ end)
TITLE "property apply"
ROOT_BENCH("apply 0 properties", function()
local apply = require "../src/apply"
local apply = require "src/apply"
local instance = create("Frame") {}
for i = 1, START(N) do
@ -209,7 +209,7 @@ ROOT_BENCH("apply 0 properties", function()
end)
ROOT_BENCH("apply 8 properties", function()
local apply = require "../src/apply"
local apply = require "src/apply"
local instance = create("Frame") {}
for i = 1, START(N) do
@ -227,7 +227,7 @@ ROOT_BENCH("apply 8 properties", function()
end)
ROOT_BENCH("bind property", function()
local apply = require "../src/apply"
local apply = require "src/apply"
local instance = create("Frame") {}
local src = source(1)
@ -242,7 +242,7 @@ ROOT_BENCH("bind property", function()
end)
ROOT_BENCH("update binding", function()
local apply = require "../src/apply"
local apply = require "src/apply"
local instance = create("Frame") {}
local src = source(1)
@ -504,44 +504,85 @@ ROOT_BENCH(`get context (depth={depth})`, function()
end)
end)
TITLE "spring()"
N *= 1024
ROOT_BENCH("spring update", function()
local root, source, spring = vide.root, vide.source, vide.spring
TITLE "aggregate"
local src = source(0)
do
-- the purpose of the two following benchmarks is to measure the overhead of
-- aggregate construction
ROOT_BENCH("set explicit mock vector2", function()
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
root(function()
for i = 1, N do
spring(src)
local label = create "TextLabel" {
AnchorPoint = Vector2.new(1, 1)
}
for i = 1, START(N) do
apply(label, {
AnchorPoint = Vector2.new(i, i)
})
end
START(N)
src(1)
return nil
end)
end)
ROOT_BENCH("spring step", function()
local root, source, spring = vide.root, vide.source, vide.spring
ROOT_BENCH("set aggregate mock vector2", function()
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
local src = source(0)
local label = create "TextLabel" {
AnchorPoint = Vector2.new(1, 1)
}
root(function()
for i = 1, N do
spring(src)
for i = 1, START(N) do
apply(label, {
AnchorPoint = { i, i }
})
end
src(1)
START(N)
vide.step(1/60)
return nil
end)
end)
end
-- innacurate due to no Vector3 in vanilla Luau
-- mock vector is 200x slower than native vector
-- ROOT_BENCH("spring update", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- START(N)
-- src(1)
-- return nil
-- end)
-- end)
-- N /= 1024
-- ROOT_BENCH("spring step", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- src(1)
-- START(N)
-- vide.step(1/60)
-- return nil
-- end)
-- end)
return nil

View file

@ -1,27 +0,0 @@
local vide = require "../src/"
local count = vide.source(0)
vide.create("TextButton") {
BackgroundTransparency = 1,
AnchorPoint = "bad value", -- should error
InvalidProperty = true, -- should error
Text = function()
return "count: " .. count()
end,
Size = function() -- should error
return "bad value"
end,
MouseEnter = function(x, y)
end,
Activated = "bad value", -- should error
vide.create "TextLabel" {},
function() end,
}

View file

@ -86,9 +86,6 @@ local Instance = {} :: any do
local proxies = {} :: { [Data]: userdata? }
setmetatable(proxies :: any, { __mode = "v" })
-- allocate variables for the metamethods as __index and get_proxy cross refrences each other
local __index, __newindex
local function get_data(userdata: userdata): Data
local function f(userdata: userdata): ProxyMT
return getmetatable(userdata :: any)
@ -97,19 +94,6 @@ local Instance = {} :: any do
return f(userdata).data
end
local function get_proxy(data: Data): userdata
return proxies[data] or (function()
local userdata = newproxy(true)
local proxy = getmetatable(userdata)
proxy.proxy = userdata
proxy.data = data
proxy.__index = __index
proxy.__newindex = __newindex
proxies[data] = userdata
return userdata
end)()
end
local function is_instance(value: unknown): boolean
local mt = getmetatable(value :: any)
return mt and mt.data and mt.data.type == "Instance"
@ -117,16 +101,16 @@ local Instance = {} :: any do
local methods = {}
__index = function(userdata: userdata, property: string): ()
local function __index(userdata: userdata, property: string): ()
local data = get_data(userdata)
return if methods[property] then methods[property]
elseif property == "Name" then data.name
elseif property == "Parent" then (data.parent and get_proxy(data.parent))
elseif property == "Parent" then data.parent
elseif property == "Destroying" then data.destroying
else data.properties[property]
end
__newindex = function(userdata: userdata, property: string, value: unknown)
local function __newindex(userdata: userdata, property: string, value: unknown)
local data = get_data(userdata)
if property == "Name" then
if type(value) ~= "string" then error("name must be a string", 2) end
@ -151,6 +135,19 @@ local Instance = {} :: any do
end
end
local function get_proxy(data: Data): userdata
return proxies[data] or (function()
local userdata = newproxy(true)
local proxy = getmetatable(userdata)
proxy.proxy = userdata
proxy.data = data
proxy.__index = __index
proxy.__newindex = __newindex
proxies[data] = userdata
return userdata
end)()
end
function Instance.new(class: string): Instance
local data = {
name = "UNNAMED",
@ -260,6 +257,40 @@ local Vector2 = { __type = "Vector2" } :: any do
end
end
local Vector3 = { __type = "Vector3" } :: any do
local function new(x, y, z)
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
end
function Vector3.new(x, y, z)
return new(x or 0, y or 0, z or 0)
end
function Vector3.__add(a, b)
return new(a.X + b.X, a.Y + b.Y, a.Z + b.Z)
end
function Vector3.__sub(a, b)
return new(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
end
function Vector3.__mul(a, b)
return new(a.X * b, a.Y * b, a.Z * b)
end
function Vector3.__unm(v)
return new(-v.X, -v.Y, -v.Z)
end
function Vector3.__eq(a, b)
return a.X == b.X and a.Y == b.Y
end
function Vector3.__index(v)
return (v.X^2 + v.Y^2 + v.Z^2)^0.5
end
end
local UDim2 = { __type = "UDim2" } :: any do
function UDim2.new(sx, ox, sy, oy)
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
@ -299,6 +330,7 @@ return {
Instance = Instance :: typeof(Instance),
Color3 = Color3 :: typeof(Color3),
Vector2 = Vector2 :: typeof(Vector2),
Vector3 = Vector3 :: typeof(Vector3),
UDim2 = UDim2 :: typeof(UDim2),
Enum = Enum :: typeof(Enum),
typeof = typeof :: typeof(typeof)

View file

@ -0,0 +1,9 @@
local function dir(directory: string)
return setmetatable({} :: { [string]: any },
{ __index = function(_, path) return directory .. path end })
end
local script = dir "src/"
script.Parent = dir "src/"
return script

View file

@ -1,34 +1,25 @@
local vide = require "../../vide"
local vide = require "src/init"
local testkit = require("test/testkit")
local function system(): (number) -> number
local MAX = 40
local MIN = 10
local program_time = os.clock()
local _, input, output = vide.root(function()
local input = vide.source(MAX)
local output = vide.spring(input, 1, .3)
return input, output
end)
local function step(): number
local FPS = 60
local DT = 1/FPS
local T = 10
local t = 0
return function(dt)
t += dt
if t >= T then
t -= T
input(input() == MAX and MIN or MAX)
end
vide.step(dt)
return output()
end
repeat until os.clock() - program_time >= DT
program_time += DT
return DT
end
--------------------------------------------------------------------------------
local function main()
local TERMINAL_HEIGHT = 73 --* REDUCE IF BAR DOES NOT FIT IN TERMINAL
local MIN_ALPHA = 0.3
local MAX_ALPHA = 0.7
local function redraw_block(h: number)
local OFFSET = 70
local MIN = TERMINAL_HEIGHT * MIN_ALPHA
local MAX = TERMINAL_HEIGHT * MAX_ALPHA
local OFFSET = TERMINAL_HEIGHT - MAX
local BLOCK = "█"
@ -44,35 +35,37 @@ local function redraw_block(h: number)
else "▁"
end
local h_f = math.floor(h)
local reset = "\27[H\27[2J" -- ANSI clear terminal
local offset = string.rep("\n", OFFSET - h_f)
local bar = remainder_to_block(h - h_f) .. "\n" .. string.rep(BLOCK .. "\n", h_f)
--print(reset .. offset .. bar .. "\n" .. string.format("%.1f", h))
print(reset .. offset .. bar .. "\n" .. h)
local source = vide.source
local spring = vide.spring
local effect = vide.effect
local value = source(MAX)
local sprung = spring(value, 1, 0.3)
effect(function()
local v = sprung()
local fv = math.floor(v)
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 .. "\n" .. v)
end)
local T = 3
local elapsed = T/1.2
repeat local dt = step()
vide.step(dt)
elapsed += dt
while elapsed >= T do
elapsed -= T
value(value() == MAX and MIN or MAX)
end
until false
end
local program_time = os.clock()
local function step(): number
local FPS = 30
local DT = 1/FPS
repeat until os.clock() - program_time >= DT
program_time += DT
return DT
end
local function loop()
local callback = system()
while true do
local dt = step()
local x = callback(dt)
redraw_block(x)
end
end
loop()
vide.root(main)

View file

@ -1,110 +0,0 @@
local vide = require "../"
do
print "============================================================="
local a = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end
do
print "============================================================="
local a = vide.source(1)
local b = vide.source(1)
local c = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
b(vide.untrack(b) + 1)
end)
vide.effect(function()
b()
c(vide.untrack(c) + 1)
end)
vide.effect(function()
c()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end
do
print "============================================================="
local a = vide.source(1)
local b = vide.source(1)
local c = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
vide.untrack(function() -- todo: this trace appearing twice
b(b() + 1)
return nil
end)
end)
vide.effect(function()
b()
vide.batch(function()
c(vide.untrack(c) + 1)
end)
end)
vide.effect(function()
c()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
name = "centau/vide"
description = "A reactive Luau library for creating UI. "
license = "MIT"
version = "0.4.1"
version = "0.3.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "LICENSE", "src"]