mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Compare commits
No commits in common. "main" and "0.2.0" have entirely different histories.
89 changed files with 3121 additions and 4392 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.luau linguist-language=Lua
|
||||
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
13
.github/workflows/deploy.yml
vendored
13
.github/workflows/deploy.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/unit-test.yml
vendored
6
.github/workflows/unit-test.yml
vendored
|
|
@ -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"
|
||||
latest: true
|
||||
fileName: luau-ubuntu.zip
|
||||
out-file-path: bin
|
||||
|
||||
|
|
|
|||
54
.github/workflows/wallypesde.yml
vendored
54
.github/workflows/wallypesde.yml
vendored
|
|
@ -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
8
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
77
CHANGELOG.md
77
CHANGELOG.md
|
|
@ -6,83 +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
|
||||
|
||||
- `context()`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `root()` now returns its destructor as the first value by default.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error stack traces being lost.
|
||||
- `root()` now destroys the scope automatically if an error occurs during call.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.2.0] - 2023-11-22
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ for a quick introduction to the library.
|
|||
|
||||
## Code sample
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,6 @@ export default withMermaid({
|
|||
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: "/" },
|
||||
|
|
@ -34,7 +26,7 @@ export default withMermaid({
|
|||
items: [
|
||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
||||
{ text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
|
||||
{ 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" },
|
||||
|
|
@ -51,26 +43,22 @@ export default withMermaid({
|
|||
{ 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: "Root Scopes", link: "/tut/crash-course/6-root" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||
{ text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "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: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/13-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Advanced",
|
||||
items: [
|
||||
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Design Patterns",
|
||||
text: "Advanced Reactivity",
|
||||
items: [
|
||||
{ text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './vars.css'
|
||||
import './index.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
}
|
||||
export default DefaultTheme
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
The output source value is updated every step 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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,4 +1,39 @@
|
|||
# Element Creation
|
||||
# Element Creation API
|
||||
|
||||
<br/>
|
||||
|
||||
## mount()
|
||||
|
||||
Runs a function in a new reactive 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 reactive scope, just like
|
||||
[root()](reactivity-core.md#root).
|
||||
|
||||
Returns a function that when called will destroy the reactive 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 reactive 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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,155 +1,125 @@
|
|||
# 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.
|
||||
Yielding is not allowed in any reactive scope. Strict mode can 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 reactive scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
||||
|
||||
type Destructor = () -> ()
|
||||
```lua
|
||||
function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a destructor and any values returned by the callback.
|
||||
Returns the result of the given function.
|
||||
|
||||
Creates a new root reactive scope, where creation and derivations of sources
|
||||
can be tracked and properly disposed of.
|
||||
|
||||
A function to destroy the root is passed into the callback, which will run
|
||||
any cleanups and allow derived sources created to garbage collect.
|
||||
|
||||
## source()
|
||||
|
||||
Creates a new source.
|
||||
Creates a new source with the given value.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function source<T>(value: T): Source<T>
|
||||
|
||||
type Source<T> =
|
||||
() -> T -- get
|
||||
& (T) -> () -- set
|
||||
```lua
|
||||
function source<T>(value: T): (T?) -> T
|
||||
```
|
||||
|
||||
- **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.
|
||||
|
||||
Reading from the source from within a reactive scope will cause changes
|
||||
to that source to be tracked and anything depending on it to update.
|
||||
|
||||
Sources can be created outside of reactive scopes.
|
||||
|
||||
- **Example**
|
||||
|
||||
```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 changed, the callback will
|
||||
be reran.
|
||||
|
||||
The callback is ran to initially ran on first call to find dependent sources.
|
||||
|
||||
- **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 to initially ran on first call to find dependent sources.
|
||||
|
||||
- **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"
|
||||
```
|
||||
|
||||
:::
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
218
docs/api/reactivity-flow.md
Normal 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 reactive scope. If it changes from a
|
||||
truthy to falsey value, the reactive scope the component was created in will
|
||||
be destroyed, and the returned source will output `nil`, or a fallback
|
||||
component if given.
|
||||
|
||||
The fallback component is also ran under a new reactive scope, and destroyed
|
||||
when the input source switches back to truthy.
|
||||
|
||||
## switch()
|
||||
|
||||
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 reactive
|
||||
scope. If the input source changes, the reactive scope the component was
|
||||
created in will be destroyed, and a new component created under a new
|
||||
reactive scope. If no component is found for an input value, the switch will
|
||||
output `nil`.
|
||||
|
||||
- **Example**
|
||||
|
||||
```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 reactive
|
||||
scope to produce a new instance.
|
||||
- For any removed index, the reactive scope for that index is destroyed.
|
||||
- Unchanged indexes are untouched.
|
||||
|
||||
The transform function is called only ever *once* for each index in the
|
||||
source table.
|
||||
|
||||
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 reactive
|
||||
scope to produce a new instance.
|
||||
- For any removed value, the reactive scope for that value is destroyed.
|
||||
- Unchanged values are untouched.
|
||||
|
||||
The transform function is only ever called *once* for each value in the
|
||||
source table.
|
||||
|
||||
1. First argument is the *value itself*.
|
||||
2. Second argument is a *source containing the value's index*.
|
||||
|
||||
Anytime an existing value's index changes, the transform function is not
|
||||
rerun, instead the source index for that value will update, causing anything
|
||||
depending on it to update too.
|
||||
|
||||
::: 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 value. 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 easily be put through a spring.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
@ -1,58 +1,53 @@
|
|||
# Reactivity: Utility
|
||||
# Reactivity API: Utility
|
||||
|
||||
## cleanup()
|
||||
|
||||
Queues a callback to run when a scope is reran or destroyed.
|
||||
Runs a callback anytime a reactive 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()
|
||||
local label = create "TextLabel" { Text = data() }
|
||||
|
||||
cleanup(function()
|
||||
print "cleaned"
|
||||
label:Destroy()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
-- nothing printed yet
|
||||
count(1) -- prints "cleaned"
|
||||
count(2) -- prints "cleaned"
|
||||
destroy() -- prints "cleaned"
|
||||
```
|
||||
|
||||
## 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 where any sources read will not be tracked by a reactive
|
||||
scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function untrack<T>(source: () -> T): T
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Can be used inside a reactive scope to read from sources you do not want
|
||||
tracked by the reactive scope.
|
||||
Updates made to a source passed to `untrack()` will not cause updates to
|
||||
anything depending on that source.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local a = source(0)
|
||||
local b = source(0)
|
||||
|
||||
|
|
@ -61,98 +56,39 @@ 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 runs.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function batch(fn: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Improves performance when an effect depends on multiple sources, and those
|
||||
sources need to be 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>
|
||||
|
||||
Creates a new context.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function context<T>(default: T): Context<T>
|
||||
|
||||
type Context<T> =
|
||||
() -> T -- get
|
||||
& <U>(T, () -> U) -> U -- 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.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local theme = context()
|
||||
|
||||
local function Button()
|
||||
print(theme())
|
||||
end
|
||||
|
||||
root(function()
|
||||
theme("light", function()
|
||||
Button() -- prints "light"
|
||||
|
||||
theme("dark", function()
|
||||
Button() -- prints "dark"
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
which strict mode can 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
|
||||
It is recommend to develop UI with strict mode and to disable it when pushing to
|
||||
production. In Roblox, production code compiles at O2 by default, so you don't
|
||||
need to worry about disabling strict mode unless you have manually enabled it.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
"vitepress": "^1.0.0-rc.4",
|
||||
"vitepress-plugin-mermaid": "^2.0.14"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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.
|
||||
253
docs/tut/advanced/reactive-scoping.md
Normal file
253
docs/tut/advanced/reactive-scoping.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
# Reactive Scoping
|
||||
|
||||
This is a brief document designed to give the user more insight into how Vide's
|
||||
reactive system works.
|
||||
|
||||
## Graph Basics
|
||||
|
||||
Vide's reactivity can be represented as a graph, where each source, derived
|
||||
source, and effect is a node on that graph. The term "*reactive scope*" is just
|
||||
an abstraction used to refer to these nodes. Each node is a reactive scope.
|
||||
|
||||
Each node stores a cached value, a side-effect function, cleanup functions,
|
||||
its parents and children, and its owner and owned.
|
||||
|
||||
Whenever a node is updated it will:
|
||||
|
||||
1. destroy its owned nodes
|
||||
2. run its cleanups
|
||||
3. rerun its side-effect and update its cached value
|
||||
4. if its cached value changes, update its children recursively.
|
||||
|
||||
There is a difference between children nodes and owned nodes:
|
||||
|
||||
- children nodes are updated when a parent is updated.
|
||||
- owned nodes are destroyed when a parent is updated.
|
||||
- both children and owned are destroyed when a parent is destroyed.
|
||||
|
||||
Nodes created by `root()` generally have no children, and only tracks owned.
|
||||
Nodes created by `derive()` generally have no owned, and only tracks children.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```lua
|
||||
root(function()
|
||||
local forename = source "quan"
|
||||
local surname = source "xi"
|
||||
|
||||
local name = derive(function()
|
||||
return forename() .. " " .. surname()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
print("new name: " .. name())
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
This code will produce a graph that looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
subgraph root
|
||||
forename & surname --> name
|
||||
name --> effect
|
||||
end
|
||||
```
|
||||
|
||||
Nodes connected by arrows represent parent and children connections.
|
||||
Nodes within other nodes represent owner and owned connections.
|
||||
|
||||
Any time a node is updated, Vide will traverse and update that node's children,
|
||||
its children's children, etc, until all nodes descending from that node has been
|
||||
updated. Traversal will stop at a node if that node's cached value does not
|
||||
change after an update.
|
||||
|
||||
When the side-effect for a node is being reran when a node is updated, any
|
||||
other nodes read within that side-effect are set as parents of the node
|
||||
currently being reran. As those nodes are read, we know that the current node
|
||||
depends on them, so any time those nodes are updated, they will update dependent
|
||||
nodes since they will be stored as children.
|
||||
|
||||
When destroying a node, its descendents are traversed and also destroyed.
|
||||
When being destroyed, a node's connections (parents and children, owner and
|
||||
owned) are cleared, and any pending cleanup functions are ran.
|
||||
|
||||
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
||||
the root node which will track any node created inside its scope, or any
|
||||
cleanups registered. Without it, nodes could be garbage collected without a
|
||||
chance to run pending cleanups which can cause memory leakage.
|
||||
|
||||
Nodes created by `source()` can actually exist outside of root nodes, since
|
||||
they do not have direct side-effects or cleanups, they do not have to be
|
||||
explicitly destroyed.
|
||||
|
||||
## Control-flow Graph Example
|
||||
|
||||
Control flow functions in Vide are special, as they can dynamically create and
|
||||
destroy new root scopes.
|
||||
|
||||
It is the combination of the above which allows us to write components like so:
|
||||
|
||||
```lua
|
||||
local function Counter(props: { text: string })
|
||||
local count = source(0)
|
||||
|
||||
local connection = stepped:Connect(function() count(count() + 1) end)
|
||||
|
||||
cleanup(function() connection:Disconnect() end)
|
||||
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return props.text() .. ": " .. count()
|
||||
end
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
|
||||
just sees this as a function that creates nodes in the reactive graph.
|
||||
|
||||
```lua
|
||||
root(function()
|
||||
local counters = { "A", "B" }
|
||||
|
||||
indexes(counters, function(name)
|
||||
return Counter { text = name }
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
This code produces a graph like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#fff"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
subgraph root
|
||||
counters --> indexes
|
||||
|
||||
subgraph root1[subroot 1]
|
||||
n1[name] --> p1[prop binding]
|
||||
end
|
||||
|
||||
subgraph root2[subroot 2]
|
||||
n2[name] --> p2[prop binding]
|
||||
end
|
||||
end
|
||||
|
||||
indexes .-> root1 & root2
|
||||
```
|
||||
|
||||
This shows how the `indexes()` control flow function creates and manages new
|
||||
root scopes. The function creates an effect seen as `indexes` in the graph,
|
||||
which manages the new roots `subroot 1` and `subroot 2`, as well as the sources
|
||||
`name` for which one exists for each index value in the input table.
|
||||
|
||||
When the input table changes, `indexes()` can automatically destroy and create
|
||||
subroots based on the changed indexes. Destroyed nodes run any cleanups made, in
|
||||
this case it is the cleanups to disconnect the counters connection. The same
|
||||
applies to all other control flow functions.
|
||||
|
||||
Whenever the root reactive scope is destroyed, all its children, `counters` and
|
||||
`indexes` will be destroyed too, which means that `indexes` children, the
|
||||
subroots, will also be destroyed. Everything is nicely cleaned up.
|
||||
|
||||
## Custom Control-flow Example
|
||||
|
||||
Below is a simple example of the `show()` control-flow function.
|
||||
|
||||
Each time `visible` changes, `show()` will destroy the current reactive scope
|
||||
and rerun its function in a new one.
|
||||
|
||||
```lua
|
||||
local visible = source(true)
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
show(visible, function()
|
||||
return create "TextLabel" { Text = count }
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
The above code produces a graph like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1B1B1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
subgraph root
|
||||
direction LR
|
||||
show
|
||||
|
||||
subgraph subroot["show() subroot"]
|
||||
p1[prop binding]
|
||||
end
|
||||
end
|
||||
|
||||
visible --> show
|
||||
count --> p1
|
||||
show -.- subroot
|
||||
```
|
||||
|
||||
This can be recreated without the `show()` control-flow function, with the
|
||||
following code:
|
||||
|
||||
```lua
|
||||
local visible = source(true)
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local output = derive(function()
|
||||
visible()
|
||||
|
||||
-- untrack so any source read from within this scope
|
||||
-- will not cause the outer `derive()` call to rerun,
|
||||
-- we only want `derive()` to rerun when `visible` changes
|
||||
return untrack(function()
|
||||
local label = create "TextLabel" {}
|
||||
|
||||
effect(function()
|
||||
label.Text = count()
|
||||
end)
|
||||
|
||||
return label
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
Both of the above code samples will produce the same visible result.
|
||||
0
docs/tut/control-flow/1-intro.md
Normal file
0
docs/tut/control-flow/1-intro.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# show()
|
||||
1
docs/tut/control-flow/3-switch.md
Normal file
1
docs/tut/control-flow/3-switch.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# switch()
|
||||
1
docs/tut/control-flow/4-indexes.md
Normal file
1
docs/tut/control-flow/4-indexes.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# indexes()
|
||||
1
docs/tut/control-flow/5-values.md
Normal file
1
docs/tut/control-flow/5-values.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# values()
|
||||
|
|
@ -4,18 +4,22 @@ This is a tutorial that introduces the concepts and usage of Vide.
|
|||
|
||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
This tutorial assumes familiarity with Luau and Roblox UI.
|
||||
|
||||
## Why Vide?
|
||||
|
||||
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.
|
||||
Creating UI is complicated, slow, and tedious.
|
||||
|
||||
Some of Vide's main design choices:
|
||||
Vide tries to simplify and speed up this process by providing a declarative and
|
||||
reactive of style programming, which lets you focus more on designing the UI
|
||||
itself and not having to manually update or reparent UI instances.
|
||||
|
||||
- Syntax minimal.
|
||||
- Data oriented.
|
||||
- Typechecking compatible.
|
||||
- Instance independent.
|
||||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
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.
|
||||
|
||||
As with most declarative libraries, there is an initial learning curve to
|
||||
understand the concepts and usage. This tutorial tries to comprehensively
|
||||
cover these concepts and usage, more so than you need just to use it.
|
||||
|
|
|
|||
|
|
@ -2,37 +2,83 @@
|
|||
|
||||
Sometimes you may need to do some cleanup when destroying a component or after
|
||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||
is used to queue a callback for the next time a reactive scope is rerun or
|
||||
destroyed, or when a stable scope is destroyed.
|
||||
is used to queue a cleanup callback for the next time a reactive scope re-runs.
|
||||
|
||||
```luau
|
||||
local root = vide.root
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
|
||||
local destroy = root(function()
|
||||
effect(function()
|
||||
local x = count()
|
||||
cleanup(function() print(x) end)
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
end)
|
||||
|
||||
cleanup(function() print "root destroyed" end)
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
|
||||
count(1) -- prints "0"
|
||||
count(2) -- prints "1"
|
||||
destroy() -- prints "2" and "root destroyed"
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. math.floor(count())
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local unmount = mount(Timer)
|
||||
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection stopped
|
||||
```
|
||||
|
||||
::: tip
|
||||
Roblox instances do not need to be explicitly destroyed for their
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the reactive scope responsible for creating the timer component is
|
||||
destroyed, such as when it is unmounted.
|
||||
|
||||
Vide does not see "components", it only sees reactive scopes and how they are
|
||||
linked together. Components are just a user pattern that creates UI instances
|
||||
alongside effects. In other words, instances are just a side-effect of the
|
||||
reactive graph. When a reactive scope is created, you create a corresponding
|
||||
instance to display that data, when that reactive scope is destroyed, any
|
||||
cleanups queued will be ran and take care of anything that needs to be, such
|
||||
as disconnecting connections.
|
||||
|
||||
This is another reason why `mount()` is used at the top level of your app, so
|
||||
that any registered cleanups created by your app components can be ran when
|
||||
they are destroyed.
|
||||
|
||||
Side note: Roblox instances do not need to be explicitly destroyed for their
|
||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
||||
need to use `cleanup()` to destroy instances. However, be wary of connecting
|
||||
a function that references an instance to an event from the same instance,
|
||||
this causes the instance to reference itself and never be freed. In such a case
|
||||
you would need to use `cleanup()` to disconnect this connection or to explicitly
|
||||
destroy the instance.
|
||||
:::
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
cleanup([cleanup]) ~~~ count
|
||||
count --> bind["effect (text binding)"]
|
||||
end
|
||||
```
|
||||
|
|
|
|||
276
docs/tut/crash-course/11-control-flow.md
Normal file
276
docs/tut/crash-course/11-control-flow.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# Control Flow
|
||||
|
||||
Eventually you will 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.
|
||||
These sources can be assigned as children, meaning the displayed children
|
||||
will update when the input source updates.
|
||||
|
||||
Control flow functions are special, because they run their components in a new
|
||||
reactive scope, which can be destroyed independently of the reactive scope that
|
||||
called the control flow function itself. This means that parts of your app can
|
||||
be independently created then destroyed.
|
||||
|
||||
## show()
|
||||
|
||||
The most basic control flow function is `show()`, which is used to conditionally
|
||||
show a component.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
show(function() return not joined() end, JoinButton)
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
This will make a button to join if you have not joined already.
|
||||
|
||||
You can also pass a third argument, a fallback to show if the condition is falsey.
|
||||
|
||||
```lua
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
local function LeaveButton()
|
||||
return Button {
|
||||
Activated = function() joined(false) end
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
show(joined, LeaveButton, JoinButton)
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root["mount() scope"]
|
||||
direction LR
|
||||
joined --> show -.- subroot
|
||||
|
||||
subgraph subroot["show() scope"]
|
||||
direction LR
|
||||
Button
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`show()` will implicitly create an effect depending on `joined`, which can be
|
||||
seen as `show` on the graph. This effect manages, and can create or destroy
|
||||
a separate reactive scope seen as `show() scope` on the graph. The dotted line
|
||||
indicates that it isn't actually connected, only indirectly managed through
|
||||
code.
|
||||
|
||||
## switch()
|
||||
|
||||
Similar to `show()`, `switch()`, also condtionally displays one instance at a
|
||||
time. It is more flexible since it can show one of many components, based on a
|
||||
table used to map a source value to a component.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
local function LeaveButton()
|
||||
return Button {
|
||||
Activated = function() joined(false) end
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
switch(joined) {
|
||||
[true] = LeaveButton,
|
||||
[false] = JoinButton
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
This example is equivalent to the previous one.
|
||||
|
||||
The switch can map any value to any component.
|
||||
|
||||
```lua
|
||||
type ActiveMenu = "none" | "inventory" | "shop" | "settings"
|
||||
|
||||
local menu = source "inventory"
|
||||
|
||||
switch(menu) {
|
||||
inventory = InventoryMenu,
|
||||
shop = ShopMenu,
|
||||
settings = SettingsMenu
|
||||
}
|
||||
```
|
||||
|
||||
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["mount() scope"]
|
||||
direction LR
|
||||
menu --> switch -.- subroot
|
||||
|
||||
subgraph subroot["switch() scope"]
|
||||
direction LR
|
||||
Menu
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## indexes()
|
||||
|
||||
Often, you will have a table of values with each value displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, `indexes()` allows you to create elements for each table index, to
|
||||
display the value at that index.
|
||||
|
||||
```lua
|
||||
local todoList = source {
|
||||
"finish the crash course",
|
||||
"star vide's GitHub"
|
||||
}
|
||||
|
||||
local function TodoList(props: { list: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(todoList, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
TodoList { list = todoList }
|
||||
```
|
||||
|
||||
For each index in the given source table, the given function will be called
|
||||
with:
|
||||
|
||||
1. a source containing the value of the index
|
||||
2. the index itself
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
given source for that index is updated.
|
||||
|
||||
Any time the input source table is updated, the given function will be ran for
|
||||
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||
value), will have its corresponding reactive scope destroyed to clean up that
|
||||
element.
|
||||
|
||||
`indexes()` is said to *map* each table index to a new UI element that can
|
||||
update to display the current value at that index.
|
||||
|
||||
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 ["mount() scope"]
|
||||
direction LR
|
||||
todoList --> indexes -.- subroot1 & subroot2
|
||||
|
||||
subgraph subroot1 ["indexes() scope 1"]
|
||||
direction LR
|
||||
value1[todo] --> prop1["prop binding"]
|
||||
end
|
||||
|
||||
subgraph subroot2 ["indexes() scope 2"]
|
||||
direction LR
|
||||
value2[todo] --> prop2[prop binding]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
One thing to note regarding table sources, is that when you edit a table in a
|
||||
source, you must set that table again to actually update the source.
|
||||
|
||||
```lua
|
||||
local src = source { 1, 2 }
|
||||
local data = src()
|
||||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
||||
Together, these control flow functions cover the majority of cases where you
|
||||
need to dynamically create and destroy parts of your UI.
|
||||
|
||||
If you need to do something that these control flow functions cannot, you can
|
||||
always use `mount()` within an effect to dynamically create and destroy
|
||||
components on your own terms. Just remember to use `cleanup()` to unmount when
|
||||
the effect reruns.
|
||||
|
|
@ -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.
|
||||
120
docs/tut/crash-course/12-property-nesting.md
Normal file
120
docs/tut/crash-course/12-property-nesting.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Nested Properties
|
||||
|
||||
Often when creating components from existing components, you can find yourself
|
||||
repetitively passing through properties such as size or position.
|
||||
|
||||
Example below:
|
||||
|
||||
```lua
|
||||
function Background(props: {
|
||||
Color: Color3,
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return create "Frame" {
|
||||
Color = props.Color
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: {
|
||||
Color = props.Color
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
One way this can be avoided is by using *property nesting*. In Vide, passing a
|
||||
table value inside `props` has special semantics. Any key with a table value is
|
||||
not assigned like a property, instead the table is iterated and processed just
|
||||
like the outer table is. Any properties in the nested table will be assigned
|
||||
to the instance just the same.
|
||||
|
||||
Below is an example of how you can use this to pass groups of similar properties
|
||||
together such as position and size, while also using typechecking.
|
||||
|
||||
```lua
|
||||
type Layout = {
|
||||
Layout = {
|
||||
Position: UDim2?,
|
||||
Size: UDim2?,
|
||||
AnchorPoint: Vector2?
|
||||
}
|
||||
}
|
||||
|
||||
function Background(props: Layout & { Color: Color3 })
|
||||
return create "Frame" {
|
||||
Color = props.Color,
|
||||
props.Layout
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: Layout & { Color: Color3 })
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
Layout = props.Layout
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
Here we created a nested group with the key `Layout` that can accept
|
||||
layout-related properties. Any name could be chosen for the key.
|
||||
This allows us to write much more concise syntax that is also typecheckable.
|
||||
|
||||
In another example we use a key named `Children` to pass arrays of instances to
|
||||
be parented.
|
||||
|
||||
```lua
|
||||
type Children = {
|
||||
-- also can optionally pass a source that returns an array of children too
|
||||
Children = Array<Instance> | () -> Array<Instance>
|
||||
}
|
||||
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
|
||||
List {
|
||||
Layout = {
|
||||
Position = UDim2.new()
|
||||
},
|
||||
|
||||
Children = {
|
||||
create "TextLabel" { Text = "1" },
|
||||
create "TextLabel" { Text = "2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deeper nested properties are guaranteed to be set after shallower nested
|
||||
properties, this can be used to create overridable default properties.
|
||||
|
||||
```lua
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
|
||||
-- can be overriden by `props.Layout`
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
|
@ -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,13 @@ 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)
|
||||
|
||||
|
|
@ -44,11 +44,9 @@ local instance = create "TextBox" {
|
|||
changed("Text", output)
|
||||
}
|
||||
|
||||
effect(function()
|
||||
print(output())
|
||||
end)
|
||||
instance.Text = "foo"
|
||||
|
||||
instance.Text = "foo" -- "foo" will be printed by the effect
|
||||
print(output()) -- "foo"
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
# Concepts Summary
|
||||
|
||||
A summary of all the concepts covered during the crash course.
|
||||
|
||||
## Source
|
||||
|
||||
A source of data.
|
||||
|
||||
Stores a single value that can be updated.
|
||||
|
||||
Created with `source()`.
|
||||
|
||||
## Derived Source
|
||||
|
||||
A new source composed of other sources.
|
||||
|
||||
Created with a plain function or with `derive()`.
|
||||
|
||||
## Effect
|
||||
|
||||
Anything that happens in response to a source update.
|
||||
|
||||
Created with `effect()`.
|
||||
|
||||
## Stable Scope
|
||||
|
||||
One of the two types of Vide scopes.
|
||||
|
||||
Created by:
|
||||
|
||||
- `root()`
|
||||
- `untrack()`
|
||||
- `show()`
|
||||
- `indexes()`
|
||||
|
||||
Stable scopes do not track sources and never rerun.
|
||||
|
||||
New stable or reactive scopes can be created within a stable scope.
|
||||
|
||||
## Reactive Scope
|
||||
|
||||
Created by:
|
||||
|
||||
- `effect()`
|
||||
- `derive()`
|
||||
|
||||
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.
|
||||
|
||||
## Scope Cleanup
|
||||
|
||||
When a scope is rerun or destroyed, all scopes created within it are
|
||||
automatically destroyed.
|
||||
|
||||
Any functions queued by `cleanup()` are also ran.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
The combination of stable and reactive scopes can viewed graphically, called a
|
||||
*reactive graph*. This can be a more intuitive way to think of the
|
||||
relationships between effects and the sources they depend on.
|
||||
|
||||
### Code
|
||||
|
||||
```luau
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local text = derive(function()
|
||||
return "count: " .. count()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
print(text())
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
### Graph resulting from code
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#202530"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
|
||||
subgraph root
|
||||
text --> effect
|
||||
end
|
||||
|
||||
count --> text
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Since `count` is a source, not an effect, it can exist
|
||||
outside of scopes.
|
||||
- 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.
|
||||
|
|
@ -11,9 +11,9 @@ want this.
|
|||
|
||||
Strict mode will run derived sources and effects twice each time they update.
|
||||
This is to help ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done properly.
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
|
||||
```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)
|
||||
|
||||
132
docs/tut/crash-course/15-concepts.md
Normal file
132
docs/tut/crash-course/15-concepts.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Concepts Summary
|
||||
|
||||
A summary of all the concepts covered during the crash course.
|
||||
|
||||
## Source
|
||||
|
||||
A source of data.
|
||||
|
||||
Stores a single value that can be updated by the user.
|
||||
|
||||
## Effect
|
||||
|
||||
Anything that happens in reponse to a source update.
|
||||
|
||||
Vide has built-in functions to create effects such as
|
||||
|
||||
- `effect()` - runs arbitrary user code on source update
|
||||
- `derive()` - updates a derived source on source update
|
||||
|
||||
## Reactive Scope
|
||||
|
||||
A scope created by certain Vide functions where source updates can be tracked,
|
||||
and cleanups queued.
|
||||
|
||||
When a source used inside a reactive scope is updated, the reactive scope will
|
||||
rerun.
|
||||
|
||||
Reactive scopes are created by functions such as
|
||||
|
||||
- `root()`
|
||||
- `effect()`
|
||||
- `derive()`
|
||||
|
||||
## Owner
|
||||
|
||||
A reactive scope created within an outer reactive scope, is *owned* by the outer
|
||||
reactive scope.
|
||||
|
||||
When a reactive scope is re-ran or destroyed, all reactive scopes owned by it
|
||||
are also destroyed.
|
||||
|
||||
Vide does not let you create reactive scopes without owners.
|
||||
|
||||
## Root Reactive Scope
|
||||
|
||||
A top-level reactive scope. These scopes are an exception to the owner rule.
|
||||
|
||||
Created by `root()`, which `mount()` uses internally.
|
||||
|
||||
A root reactive scope can be created on its own. It allows other reactive scopes
|
||||
to be created with an owner.
|
||||
|
||||
Root reactive scopes must be destroyed manually by the user, a function to do
|
||||
this is given by `root()`.
|
||||
|
||||
A root reactive scope can be created within another reactive scope and it will
|
||||
not automatically be owned by that scope.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Cleans up the result from an effect.
|
||||
|
||||
Unneeded in most cases, a cleanup is arbitrary code that can be ran before
|
||||
a reactive scope is rerun or destroyed, so that the result from the previous
|
||||
run can be cleaned up. A cleanup can be queued by using `cleanup()` within
|
||||
a reactive scope.
|
||||
|
||||
## Tracking
|
||||
|
||||
Reactive scopes are tracking by default, meaning sources read from within scope
|
||||
will be tracked.
|
||||
|
||||
A reactive scope can be made temporarily non-tracking within `untrack()`, so
|
||||
that any source used will be ignored. The only function that creates a
|
||||
nontracking reactive scope by default is `root()`.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
The combination of reactive scopes can viewed graphically, called a
|
||||
*reactive graph*. This can be a more intuitive way to think of the
|
||||
relationships between effects and the sources they depend on.
|
||||
|
||||
### Code
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local text = derive(function()
|
||||
return "count: " .. text()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
print(text())
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
### Graph resulting from code
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
|
||||
subgraph root
|
||||
text --> effect
|
||||
end
|
||||
|
||||
count --> text
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Since `count` is a source, not an effect, it can exist
|
||||
outside of a root reactive scope.
|
||||
- An update to `count` will cause `text` to rerun, which
|
||||
then causes `effect` to rerun.
|
||||
- When the root reactive scope is destroyed, `text` and
|
||||
`effect` will be destroyed alongside it, since they are
|
||||
owned by it. `count` will be untouched and future updates
|
||||
to `count` will have no effect.
|
||||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
Instances are created using `create()`.
|
||||
|
||||
Parentheses `()` can be omitted when calling functions with string or
|
||||
table literals for brevity.
|
||||
`create()` returns a constructor for a class which then takes a table of
|
||||
properties to assign when creating a new instance for that class.
|
||||
|
||||
```luau
|
||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||
table literals which Vide takes advantage of for brevity.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
||||
return create "ScreenGui" {
|
||||
|
|
@ -36,3 +39,11 @@ return create "ScreenGui" {
|
|||
Assign a value to a string key to set a property, and assign a value to a
|
||||
number key to set a child. Events can be connected to by assigning a function
|
||||
to a string key.
|
||||
|
||||
::: warning
|
||||
When creating an instance with no properties, it is important to not forget to
|
||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||
that class, not an instance of that class. This would result in you attempting
|
||||
to parent a function instead of an instance which is not correct.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ together.
|
|||
|
||||
::: code-group
|
||||
|
||||
```luau [Button.luau]
|
||||
```lua [Button.luau]
|
||||
local create = vide.create
|
||||
|
||||
local function Button(props: {
|
||||
|
|
@ -34,12 +34,13 @@ end
|
|||
return Button
|
||||
```
|
||||
|
||||
```luau [Menu.luau]
|
||||
```lua [App.luau]
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
||||
local function Menu()
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
Button {
|
||||
Position = UDim2.fromOffset(200, 200),
|
||||
|
|
@ -58,8 +59,25 @@ local function Menu()
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Above is a simple example of a button component being used across files.
|
||||
|
||||
A single parameter `props` is used to pass properties to the component.
|
||||
|
||||
You can only modify the component in ways that you allow in the component,
|
||||
through the `props` parameter.
|
||||
|
||||
To create a new button all you must do is call the `Button` function, passing in
|
||||
values. This saves having to create and set every property each time. Also, when
|
||||
updating the button component in future, any changes to the button file will be
|
||||
seen anywhere the button is used in your app.
|
||||
|
||||
The `mount()` function is used to set up Vide's reactivity system when creating
|
||||
your UI. It only needs to be called once at the top-level with the function that
|
||||
puts together your entire app. It also parents the returned instance to another
|
||||
a target instance for you.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Sources
|
||||
|
||||
Sources are special objects that store a single value and are the core of
|
||||
Vide's reactivity.
|
||||
Sources are special objects that store a single value. They are the core of
|
||||
Vide's reactivity. They are called sources because they act as sources of data.
|
||||
|
||||
A source can be created using `source()`.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
|
||||
local count = source(0)
|
||||
|
|
@ -16,13 +16,14 @@ 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.
|
||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||
effectively becomes a new source.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
|
|
@ -34,5 +35,7 @@ count(1)
|
|||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
While the above can be achieved with plain variables, the use for sources will
|
||||
be obvious in the next part.
|
||||
Sources on their own aren't very special, the above can be achieved with plain
|
||||
variables. The real use for sources become apparent when used in combination
|
||||
with *effects*. Similar to a signal and connection, a source and effect allows
|
||||
you to do things like automatically updating UI when a source is updated.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# Effects
|
||||
|
||||
Effects are functions that are ran in response to source updates.
|
||||
A source and effect is analogous to a signal and connection.
|
||||
Effects are functions that are ran in response to source updates. They are
|
||||
called effects because they cause *side-effects* when reacting to source
|
||||
updates.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -20,16 +21,14 @@ count(1)
|
|||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
Any source read inside an effect is tracked and will rerun the effect when
|
||||
that source is updated.
|
||||
The callback given to `effect()` is initially ran immediately in a
|
||||
*reactive scope*. Any source read from inside a reactive scope will be tracked,
|
||||
so that if any of those sources update, the effect will be reran too.
|
||||
|
||||
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
|
||||
Effects also work with derived sources, 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 +49,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"
|
||||
```
|
||||
|
|
|
|||
74
docs/tut/crash-course/6-root.md
Normal file
74
docs/tut/crash-course/6-root.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Root Reactive Scopes
|
||||
|
||||
Any reactive scopes created, such as by `effect()`, must be done so within a
|
||||
"root" reactive scope. This is the main purpose of `mount()`, which you use
|
||||
once at the top level to create your UI.
|
||||
|
||||
This is so that if you want to destroy your UI, it can stop any reactive scopes
|
||||
created within it, since reactive scopes track any reactive scopes created
|
||||
within them.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function App()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
end
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
App() -- will error since effect() was not called within a reactive scope
|
||||
```
|
||||
|
||||
Mounting returns a function that when called will destroy any reactive scopes
|
||||
created during the `mount()` call.
|
||||
|
||||
```lua
|
||||
local unmount = mount(App)
|
||||
|
||||
unmount()
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
||||
The reactive graph for the above example looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> effect
|
||||
end
|
||||
```
|
||||
|
||||
When the root reactive scope created by `mount()` is destroyed, the `effect`
|
||||
scope will also be destroyed since it was created within it.
|
||||
|
||||
This is important because you may have an effect that updates the property of a
|
||||
UI instance, meaning the effect is referencing and holding that instance in
|
||||
memory. The effect being destroyed will remove this reference, allowing the
|
||||
instance to be garbage collected.
|
||||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
root scope, since you should be creating all your UI and corresponding effects
|
||||
within a top-level `mount()` call that puts all your UI together. So it is safe
|
||||
to assume that any effect you create will be created under this top level scope.
|
||||
Vide will prevent you from accidently doing otherwise anyways.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Scopes
|
||||
|
||||
Just like how a signal's connection may need to be disconnected, a source's
|
||||
effect also may need to be disconnected.
|
||||
|
||||
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.
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
Whenever a scope is destroyed, any scope created within that scope is also
|
||||
destroyed, and so on.
|
||||
|
||||
```luau
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(0)
|
||||
|
||||
local function setup()
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
end
|
||||
|
||||
setup() -- error, effect() tried to create a reactive scope with no stable scope
|
||||
|
||||
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
||||
|
||||
count(1) -- prints "1"
|
||||
count(2) -- prints "2"
|
||||
|
||||
destroy()
|
||||
|
||||
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
||||
The reactive graph for the above example looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> effect
|
||||
end
|
||||
```
|
||||
|
||||
When the stable `root()` scope 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
|
||||
UI instance, meaning the effect is referencing and holding that instance in
|
||||
memory. The effect being destroyed will remove this reference, allowing the
|
||||
instance to be garbage collected.
|
||||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
stable scope, since you should be creating all your UI and effects within a
|
||||
single top-level `root()` call that puts all your UI together, making it safe to
|
||||
assume any effect created will be created under this stable scope.
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
# Reactive Components
|
||||
# Stateful Components
|
||||
|
||||
Reactive components in Vide are created using sources and effects - sources to
|
||||
A stateful component is a component that can update in reponse to data.
|
||||
|
||||
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
|
||||
|
|
@ -29,15 +33,21 @@ Above is an example of a counter component, that when clicked, will increment
|
|||
its internal count, and automatically update its text to reflect that count.
|
||||
|
||||
Each instance of `Counter()` will maintain its own independent count, since the
|
||||
count source is created inside the component.
|
||||
count source is created inside the scope of 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,13 +58,13 @@ 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
|
||||
restrictions on how they are used as long as the effect using it is created
|
||||
within a stable scope.
|
||||
within a reactive scope.
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
# Implicit Effects
|
||||
# Property Binding
|
||||
|
||||
Explicitly creating effects to update properties is tedious. You can
|
||||
*implicitly* create an effect to update properties instead.
|
||||
Explicitly creating effects to update properties can become verbose when there
|
||||
are a lot of properties to update. Vide provides a way to *implicitly* create
|
||||
an effect to update properties on source update.
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [Implicit Effect]
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
@ -13,54 +12,39 @@ local function Counter()
|
|||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end,
|
||||
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end
|
||||
}
|
||||
end
|
||||
```
|
||||
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
|
||||
function will implicitly create an effect to update that property.
|
||||
function will implicitly create an effect to update that property anytime a
|
||||
source used within is updated.
|
||||
|
||||
## Children
|
||||
Just like effects, the function is ran immediately in a reactive scope to set
|
||||
the property initially and determine what sources are being used.
|
||||
|
||||
This allows you as the programmer to not need to manually update UI as the state
|
||||
of your program changes. You just define how data sources map to UI, and Vide's
|
||||
reactive system will automatically update any properties depending on those
|
||||
sources.
|
||||
|
||||
## Children Binding
|
||||
|
||||
Children can also be set in a similar manner. A source passed as a child (passed
|
||||
with a number key instead of string key) can return an instance or an array of
|
||||
instances. An effect is automatically created to unparent removed instances and
|
||||
parent new instances on source update.
|
||||
instances. Vide will automatically unparent removed instances and parent new
|
||||
instances when that source's stored instances change.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local items = source {
|
||||
create "TextLabel" { Text = "A" }
|
||||
}
|
||||
|
|
@ -72,12 +56,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".
|
||||
```
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -32,17 +32,18 @@ end
|
|||
effect(function() text() end)
|
||||
effect(function() text() end)
|
||||
|
||||
count(1) -- prints "ran" x2
|
||||
source(1) -- prints "ran" x2
|
||||
```
|
||||
|
||||
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.
|
||||
run a callback in a new reactive scope only when a dependent source has updated.
|
||||
Reading this derived source multiple times will just return a cached result from
|
||||
when it last updated.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(0)
|
||||
|
||||
|
|
@ -54,11 +55,10 @@ end)
|
|||
effect(function() text() end)
|
||||
effect(function() text() end)
|
||||
|
||||
count(1) -- prints "ran" x1
|
||||
source(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 reactive scope, just like `effect()`.
|
||||
|
||||
If the recalculated value is the same as the old value, the derived source will
|
||||
not rerun the effects using it.
|
||||
|
|
@ -69,12 +69,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 +87,6 @@ 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.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
10
init.luau
10
init.luau
|
|
@ -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
|
||||
15
pesde.toml
15
pesde.toml
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
182
src/apply.luau
182
src/apply.luau
|
|
@ -1,20 +1,23 @@
|
|||
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 +26,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 +58,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 +96,79 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
-- process all properties
|
||||
local depth = 1
|
||||
repeat
|
||||
process_properties(properties, instance, caches, depth)
|
||||
depth = table.remove(nested_stack) :: number
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
until not properties
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
for i = 1, #events, 2 do
|
||||
local event_name = events[i]
|
||||
local event_listener = events[i + 1]
|
||||
;(instance :: any)[event_name]:Connect(event_listener)
|
||||
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
|
||||
|
||||
for _, queued in actions do
|
||||
for _, callback in queued do
|
||||
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 :: () -> Instance | Array<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 event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
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
|
||||
local from
|
||||
|
||||
if not already_batching then
|
||||
flags.batch = true
|
||||
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)
|
||||
|
||||
if not already_batching then
|
||||
graph.flush_update_queue()
|
||||
end
|
||||
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
|
||||
|
|
|
|||
125
src/bind.luau
Normal file
125
src/bind.luau
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local trace = require(script.Parent.trace)
|
||||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
function create_binding<T>(updater: (T) -> T, binding: T)
|
||||
if flags.strict then
|
||||
-- track bind creation trace
|
||||
local fn = updater
|
||||
local bind_trace = debug.traceback(nil, trace()-1)
|
||||
updater = function(...)
|
||||
local ok, result = xpcall(fn, function(err: string)
|
||||
return err
|
||||
end, ...)
|
||||
|
||||
if not ok then
|
||||
local btype =
|
||||
if (binding :: any).property then (binding :: any).property
|
||||
elseif (binding :: any).parent then "Parent"
|
||||
else "children"
|
||||
error(`PROPERTY BINDING ERROR: Property {btype}\n{result}\nBIND CREATION TRACE:\n{bind_trace}`, 0)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(binding, updater)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
end
|
||||
|
||||
type PropertyBinding = {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function update_property(p: PropertyBinding)
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
return p
|
||||
end
|
||||
|
||||
type ParentBinding = {
|
||||
instance: Instance,
|
||||
parent: () -> Instance
|
||||
}
|
||||
|
||||
local function update_parent(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 }
|
||||
}
|
||||
|
||||
local function update_children(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
|
||||
|
||||
if new_children then
|
||||
for _, child in next, new_children :: { Instance } do
|
||||
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
|
||||
|
||||
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_binding(update_property, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
})
|
||||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
return create_binding(update_parent, {
|
||||
instance = instance,
|
||||
parent = parent
|
||||
})
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
return create_binding(update_children, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
children = children
|
||||
})
|
||||
end
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,33 @@
|
|||
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 add_cleanup = graph.add_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 in a non-reactive scope"
|
||||
end; assert(scope)
|
||||
|
||||
if type(value) == "function" then
|
||||
push_cleanup(scope, value :: () -> ())
|
||||
add_cleanup(scope, value :: () -> ())
|
||||
else
|
||||
push_cleanup(scope, helper(value))
|
||||
add_cleanup(scope, helper(value))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -35,7 +36,6 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
|||
|
||||
return cleanup ::
|
||||
( (callback: () -> ()) -> () ) &
|
||||
( (thread: thread) -> () ) &
|
||||
( (instance: Destroyable) -> () ) &
|
||||
( (connection: Disconnectable) -> () ) &
|
||||
( (instance: Instance) -> () ) &
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_scope = graph.get_scope
|
||||
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)
|
||||
|
||||
local nil_symbol = newproxy()
|
||||
local count = 0
|
||||
|
||||
local function context<T>(...: T): Context<T>
|
||||
count += 1
|
||||
local id = count
|
||||
|
||||
local has_default = select("#", ...) > 0
|
||||
local default_value = ...
|
||||
|
||||
return function<T>(...): any -- todo: fix type error
|
||||
local scope: Node<unknown>? | false = get_scope()
|
||||
|
||||
if select("#", ...) == 0 then -- get
|
||||
while scope do
|
||||
local ctx = scope.context
|
||||
|
||||
if not ctx then
|
||||
scope = scope.owner
|
||||
continue
|
||||
end
|
||||
|
||||
local value = (ctx :: { unknown })[id]
|
||||
|
||||
if value == nil then
|
||||
scope = scope.owner
|
||||
continue
|
||||
end
|
||||
|
||||
return (if value ~= nil_symbol then value else nil) :: T
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
else -- set
|
||||
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
||||
|
||||
local value, component = ...
|
||||
|
||||
local new_scope = create_node(scope, false, false)
|
||||
set_context(new_scope, id, if value == nil then nil_symbol else value)
|
||||
|
||||
push_scope(new_scope)
|
||||
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local ok, result = xpcall(component, efn)
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
error(`error while running context:\n\n{result}`, 0)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return nil :: any
|
||||
end
|
||||
end
|
||||
|
||||
return context
|
||||
163
src/create.luau
163
src/create.luau
|
|
@ -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
|
||||
for i, v in next, default do
|
||||
(instance :: any)[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
local function ctor(properties: Props): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
|
||||
return apply(instance, properties)
|
||||
self[class] = ctor
|
||||
return ctor
|
||||
end
|
||||
})
|
||||
|
||||
local function create_instance(class: string)
|
||||
return ctor_cache[class]
|
||||
end
|
||||
|
||||
return constructor
|
||||
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_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)
|
||||
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)
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
return nil :: never
|
||||
end
|
||||
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 )
|
||||
|
|
|
|||
|
|
@ -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} }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
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 assert_stable_scope = graph.assert_stable_scope
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
local function derive<T>(source: () -> T): () -> T
|
||||
local node = create_node(assert_stable_scope(), source, false :: any)
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(false :: any, source)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
push_scope_as_child_of(node)
|
||||
track(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
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 assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
local function effect<T>(callback: (T) -> T, initial_value: T)
|
||||
local node = create_node(assert_stable_scope(), callback, initial_value)
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(initial_value, callback)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
203
src/graph.luau
203
src/graph.luau
|
|
@ -1,6 +1,9 @@
|
|||
local flags = require "./flags"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
export type SourceNode<T> = {
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
export type StartNode<T> = {
|
||||
cache: T,
|
||||
[number]: Node<T>
|
||||
}
|
||||
|
|
@ -10,40 +13,24 @@ export type Node<T> = {
|
|||
effect: ((T) -> T) | false,
|
||||
cleanups: { () -> () } | false,
|
||||
|
||||
context: { [number]: unknown } | false,
|
||||
|
||||
owned: { Node<T> } | false,
|
||||
owner: Node<T> | false,
|
||||
|
||||
parents: { SourceNode<T> },
|
||||
parents: { StartNode<T> },
|
||||
[number]: Node<T> -- children
|
||||
}
|
||||
|
||||
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
|
||||
-- reactive scope stack
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
|
||||
|
||||
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 resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||
local thread = coroutine.create(pcall)
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg)
|
||||
|
||||
assert(resume_ok)
|
||||
|
||||
if coroutine.status(thread) ~= "dead" then
|
||||
return false, debug.traceback(thread, "attempt to yield in reactive scope")
|
||||
return false, "attempt to yield in reactive scope"
|
||||
end
|
||||
|
||||
return run_ok, result
|
||||
|
|
@ -53,37 +40,46 @@ local function get_scope(): Node<unknown>?
|
|||
return scopes[scopes.n]
|
||||
end
|
||||
|
||||
local function assert_stable_scope(): Node<unknown>
|
||||
local function assert_owning_scope(): Node<unknown>
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
local caller_name = debug.info(2, "n")
|
||||
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
|
||||
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
|
||||
elseif scope.effect then
|
||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
||||
throw("cannot create new reactive scope inside of a tracking scope") -- todo: allow this?
|
||||
end
|
||||
|
||||
return scope
|
||||
end
|
||||
|
||||
local function push_child<T>(parent: SourceNode<any>, child: Node<any>)
|
||||
local function add_child<T>(parent: StartNode<any>, child: Node<any>)
|
||||
table.insert(parent, child)
|
||||
table.insert(child.parents, parent)
|
||||
end
|
||||
|
||||
local function push_scope<T>(node: Node<T>)
|
||||
local function set_owner(node: Node<any>, owner: Node<any>)
|
||||
node.owner = owner
|
||||
if owner.owned then
|
||||
table.insert(owner.owned, node)
|
||||
else
|
||||
owner.owned = { node }
|
||||
end
|
||||
end
|
||||
|
||||
local function open_scope<T>(node: Node<T>)
|
||||
local n = scopes.n + 1
|
||||
scopes.n = n
|
||||
scopes[n] = node
|
||||
end
|
||||
|
||||
local function pop_scope()
|
||||
local function close_scope()
|
||||
local n = scopes.n
|
||||
scopes.n = n - 1
|
||||
scopes[n] = nil
|
||||
end
|
||||
|
||||
local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||
local function add_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||
if node.cleanups then
|
||||
table.insert(node.cleanups, cleanup)
|
||||
else
|
||||
|
|
@ -91,39 +87,34 @@ local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
|||
end
|
||||
end
|
||||
|
||||
local function flush_cleanups<T>(node: Node<T>)
|
||||
local function run_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)
|
||||
end
|
||||
end
|
||||
|
||||
local function find_and_swap_pop<T>(t: { T }, v: T)
|
||||
local i = table.find(t, v) :: number
|
||||
local idx = table.find(t, v) :: number
|
||||
local n = #t
|
||||
t[i] = t[n]
|
||||
t[idx] = t[n]
|
||||
t[n] = nil
|
||||
end
|
||||
|
||||
local function unparent<T>(node: Node<T>)
|
||||
local parents = node.parents
|
||||
|
||||
for i, parent in parents do
|
||||
for i, parent in next, parents do
|
||||
find_and_swap_pop(parent, node)
|
||||
parents[i] = nil
|
||||
end
|
||||
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)
|
||||
run_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
if node.owner then
|
||||
|
|
@ -147,55 +138,44 @@ end
|
|||
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
|
||||
local cur_value = node.cache
|
||||
|
||||
flush_cleanups(node)
|
||||
if flags.strict then
|
||||
run_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
push_scope(node)
|
||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
||||
pop_scope()
|
||||
open_scope(node)
|
||||
|
||||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
||||
end
|
||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then throw(new_value :: string) end
|
||||
|
||||
node.cache = new_value :: T
|
||||
end
|
||||
|
||||
return initial_value ~= node.cache
|
||||
else
|
||||
local cur_value = node.cache
|
||||
|
||||
flush_cleanups(node)
|
||||
run_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
push_scope(node)
|
||||
open_scope(node)
|
||||
|
||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
||||
pop_scope()
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
error(`effect error:\n{new_value}\n`, 0)
|
||||
throw(`side-effect error from source update\n{new_value}`)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
||||
return cur_value ~= new_value
|
||||
end
|
||||
end
|
||||
|
||||
local function queue_children_for_update<T>(node: SourceNode<T>)
|
||||
local function queue_children<T>(node: StartNode<T>)
|
||||
local i = update_queue.n
|
||||
while node[1] do
|
||||
i += 1
|
||||
|
|
@ -205,30 +185,34 @@ local function queue_children_for_update<T>(node: SourceNode<T>)
|
|||
update_queue.n = i
|
||||
end
|
||||
|
||||
local function get_update_queue_length()
|
||||
return update_queue.n
|
||||
end
|
||||
local _flushing = false
|
||||
local function flush_update_queue()
|
||||
assert(not _flushing, "recursive queue flush occured") -- todo
|
||||
_flushing = true
|
||||
|
||||
local function flush_update_queue(from: number)
|
||||
local i = from + 1
|
||||
local n0 = 0
|
||||
|
||||
local i = n0 + 1
|
||||
while i <= update_queue.n do
|
||||
local node = update_queue[i]
|
||||
--assert(node.effect)
|
||||
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children_for_update(node)
|
||||
queue_children(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any
|
||||
i += 1
|
||||
end
|
||||
|
||||
update_queue.n = from
|
||||
update_queue.n = n0
|
||||
|
||||
_flushing = false
|
||||
end
|
||||
|
||||
local function update_descendants<T>(root: SourceNode<T>)
|
||||
local function update<T>(root: StartNode<T>)
|
||||
local n0 = update_queue.n
|
||||
queue_children_for_update(root)
|
||||
queue_children(root)
|
||||
|
||||
if flags.batch then return end
|
||||
|
||||
|
|
@ -239,7 +223,7 @@ local function update_descendants<T>(root: SourceNode<T>)
|
|||
|
||||
-- check if node is still owned in case destroyed after queued
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children_for_update(node)
|
||||
queue_children(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
||||
|
|
@ -249,39 +233,27 @@ 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 track<T>(node: StartNode<T>)
|
||||
local scope = get_scope()
|
||||
if scope and scope.effect then -- do not track nodes with no effect
|
||||
push_child(node, scope)
|
||||
add_child(node, scope)
|
||||
end
|
||||
end
|
||||
|
||||
local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T, value: T): Node<T>
|
||||
local node: Node<T> = {
|
||||
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
|
||||
return {
|
||||
cache = value,
|
||||
effect = effect,
|
||||
cleanups = false,
|
||||
|
||||
context = false,
|
||||
|
||||
owner = owner,
|
||||
owner = false,
|
||||
owned = false,
|
||||
|
||||
parents = {},
|
||||
}
|
||||
|
||||
if owner then
|
||||
if owner.owned then
|
||||
table.insert(owner.owned, node)
|
||||
else
|
||||
owner.owned = { node }
|
||||
end
|
||||
end
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
local function create_source_node<T>(value: T): SourceNode<T>
|
||||
local function create_start_node<T>(value: T): StartNode<T>
|
||||
return { cache = value }
|
||||
end
|
||||
|
||||
|
|
@ -289,33 +261,22 @@ local function get_children<T>(node: Node<T>): { Node<unknown> }
|
|||
return { unpack(node) } :: { Node<any> }
|
||||
end
|
||||
|
||||
local function set_context<T>(node: Node<T>, key: number, value: unknown)
|
||||
if node.context then
|
||||
node.context[key] = value
|
||||
else
|
||||
node.context = { [key] = value }
|
||||
end
|
||||
end
|
||||
|
||||
return table.freeze {
|
||||
push_scope = push_scope,
|
||||
pop_scope = pop_scope,
|
||||
open_scope = open_scope,
|
||||
close_scope = close_scope,
|
||||
evaluate_node = evaluate_node,
|
||||
get_scope = get_scope,
|
||||
assert_stable_scope = assert_stable_scope,
|
||||
push_cleanup = push_cleanup,
|
||||
assert_owning_scope = assert_owning_scope,
|
||||
add_cleanup = add_cleanup,
|
||||
set_owner = set_owner,
|
||||
destroy = destroy,
|
||||
flush_cleanups = flush_cleanups,
|
||||
push_scope_as_child_of = push_scope_as_child_of,
|
||||
update_descendants = update_descendants,
|
||||
push_child = push_child,
|
||||
run_cleanups = run_cleanups,
|
||||
track = track,
|
||||
update = update,
|
||||
add_child = add_child,
|
||||
create_node = create_node,
|
||||
create_source_node = create_source_node,
|
||||
create_start_node = create_start_node,
|
||||
get_children = get_children,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
121
src/indexes.luau
121
src/indexes.luau
|
|
@ -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
|
||||
117
src/init.luau
117
src/init.luau
|
|
@ -1,12 +1,113 @@
|
|||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
||||
--------------------------------------------------------------------------------
|
||||
-- vide.luau
|
||||
-- v0.2.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 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>
|
||||
|
||||
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,
|
||||
|
||||
-- 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
|
||||
|
|
|
|||
122
src/lib.luau
122
src/lib.luau
|
|
@ -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
|
||||
228
src/maps.luau
Normal file
228
src/maps.luau
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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 StartNode<T> = graph.StartNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
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" 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_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
||||
local input_cache = {} :: Map<K, VI>
|
||||
local output_cache = {} :: Map<K, VO>
|
||||
local input_nodes = {} :: Map<K, StartNode<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)
|
||||
|
||||
open_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(false, false)
|
||||
scopes[i] = scope :: Node<any>
|
||||
|
||||
local node = create_start_node(v)
|
||||
|
||||
set_owner(scope, subowner)
|
||||
open_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, function()
|
||||
track(node)
|
||||
return node.cache
|
||||
end, i)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then
|
||||
close_scope() -- subowner scope
|
||||
error(result, 0)
|
||||
end
|
||||
|
||||
input_nodes[i] = node
|
||||
output_cache[i] = result
|
||||
else -- update source
|
||||
input_nodes[i].cache = v
|
||||
update(input_nodes[i])
|
||||
end
|
||||
|
||||
input_cache[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
close_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(false :: any, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
|
||||
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, StartNode<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
|
||||
|
||||
open_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(false, false)
|
||||
scopes[v] = scope :: Node<any>
|
||||
|
||||
local node = create_start_node(i)
|
||||
|
||||
set_owner(scope, subowner)
|
||||
open_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, v, function()
|
||||
track(node)
|
||||
return node.cache
|
||||
end)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then
|
||||
close_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(input_nodes[v])
|
||||
end
|
||||
|
||||
cur_input_cache[v] = nil
|
||||
end
|
||||
end
|
||||
|
||||
close_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(false :: any, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
||||
return function() return indexes, values end
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
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()
|
||||
return root(function(destroy)
|
||||
local result = component()
|
||||
if target then apply(target, { result }) end
|
||||
return destroy
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,35 +1,38 @@
|
|||
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
|
||||
local pop_scope = graph.pop_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local destroy = graph.destroy
|
||||
|
||||
local refs = {}
|
||||
|
||||
local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
||||
local node = create_node(false, false, false)
|
||||
local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||
local node = create_node(false, false)
|
||||
|
||||
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)
|
||||
open_scope(node)
|
||||
|
||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
||||
local result = { pcall(fn, destroy) }
|
||||
|
||||
pop_scope()
|
||||
close_scope()
|
||||
|
||||
if not result[1] then
|
||||
destroy()
|
||||
error(`error while running root():\n\n{result[2]}`, 0)
|
||||
refs[node] = nil
|
||||
throw(`mount error\n{result[2]}`)
|
||||
end
|
||||
|
||||
return destroy, unpack(result :: any, 2)
|
||||
return unpack(result :: any, 2)
|
||||
end
|
||||
|
||||
return root :: <T...>(fn: (destroy: () -> ()) -> T...) -> (() -> (), T...)
|
||||
return root :: (<T...>(fn: (destroy: () -> ()) -> T...) -> T...) & ((fn: (destroy: () -> ()) -> ()) -> ())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
effect(function()
|
||||
local v = input()
|
||||
if v then
|
||||
filtered_input(v)
|
||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||
local function truthy()
|
||||
return not not source()
|
||||
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)?)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
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 update_descendants = graph.update_descendants
|
||||
local create_start_node = graph.create_start_node
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
|
||||
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 node = create_start_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)
|
||||
track(node)
|
||||
return node.cache
|
||||
end
|
||||
|
||||
|
|
@ -21,11 +23,9 @@ local function source<T>(initial_value: T): Source<T>
|
|||
end
|
||||
|
||||
node.cache = v
|
||||
update_descendants(node)
|
||||
update(node)
|
||||
return v
|
||||
end
|
||||
|
||||
return update_source
|
||||
end
|
||||
|
||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||
|
|
|
|||
291
src/spring.luau
291
src/spring.luau
|
|
@ -1,84 +1,104 @@
|
|||
local graph = require "./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 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 UPDATE_RATE = 120
|
||||
local TOLERANCE_FACTOR = 10_000
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||
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
|
||||
|
||||
]]
|
||||
|
||||
type SpringState<T> = {
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type StartNode<T> = graph.StartNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local update = graph.update
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
|
||||
local UPDATE_RATE = 120
|
||||
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
|
||||
|
||||
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 = {
|
||||
|
|
@ -91,15 +111,15 @@ local vec6_to_type = {
|
|||
end :: Vec6ToType<CFrame>,
|
||||
|
||||
Color3 = function(v)
|
||||
return Color3.new(math.clamp(v.X, 0, 1), math.clamp(v.Y, 0, 1), math.clamp(v.Z, 0, 1))
|
||||
return Color3.new(v.X, v.Y, v.Z)
|
||||
end :: Vec6ToType<Color3>,
|
||||
|
||||
UDim = function(v)
|
||||
return UDim.new(v.X, math.round(v.Y))
|
||||
return UDim.new(v.X, v.Y)
|
||||
end :: Vec6ToType<UDim>,
|
||||
|
||||
UDim2 = function(a, b)
|
||||
return UDim2.new(a.X, math.round(a.Y), a.Z, math.round(b.X))
|
||||
return UDim2.new(a.X, a.Y, a.Z, b.X)
|
||||
end :: Vec6ToType<UDim2>,
|
||||
|
||||
Vector2 = function(v)
|
||||
|
|
@ -112,16 +132,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 +146,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>]: StartNode<any> } = {}
|
||||
setmetatable(springs, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
|
||||
local owner = assert_stable_scope()
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local owner = assert_owning_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,117 +164,63 @@ 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,
|
||||
}
|
||||
|
||||
local output = create_source_node(false :: any)
|
||||
local output = create_start_node(false :: any)
|
||||
|
||||
local function updater_effect()
|
||||
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)
|
||||
local updater = create_node(false :: any, updater_effect)
|
||||
|
||||
set_owner(updater, owner)
|
||||
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)
|
||||
return function()
|
||||
track(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)
|
||||
|
||||
-- reset velocity
|
||||
data.v_123 = vector.zero
|
||||
data.v_456 = vector.zero
|
||||
|
||||
-- schedule spring
|
||||
springs[data] = output
|
||||
|
||||
-- set output to value
|
||||
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
|
||||
|
||||
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 +230,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 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))
|
||||
local dx_123, dx_456 =
|
||||
x0_123 - x1_123,
|
||||
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)
|
||||
update(output)
|
||||
end
|
||||
|
||||
for _, data in next, remove_queue do
|
||||
springs[data] = nil
|
||||
end
|
||||
|
||||
table.clear(remove_queue)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
164
src/switch.luau
164
src/switch.luau
|
|
@ -1,113 +1,67 @@
|
|||
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 StartNode<T> = graph.StartNode<T>
|
||||
local create_node = graph.create_node
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local destroy = graph.destroy
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_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_owning_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(false, false)
|
||||
last_scope = new_scope :: Node<any>
|
||||
|
||||
set_owner(new_scope, owner)
|
||||
open_scope(new_scope)
|
||||
|
||||
local ok, result = pcall(component)
|
||||
|
||||
close_scope()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local node = create_node(nil :: U?, update)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
9
src/throw.luau
Normal file
9
src/throw.luau
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local trace = require(script.Parent.trace)
|
||||
|
||||
local function throw(msg): any
|
||||
error(msg, trace() - 1)
|
||||
end
|
||||
|
||||
return throw
|
||||
|
|
@ -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
|
||||
29
src/trace.luau
Normal file
29
src/trace.luau
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- returns path to file as an array with each directory
|
||||
-- accounts for Roblox and Luau contexts
|
||||
local function get_path(s)
|
||||
if string.sub(s, #s - 4, #s) == ".luau" then
|
||||
s = string.sub(s, 1, #s - 5)
|
||||
end
|
||||
|
||||
return string.split(s, string.match(s, "%w+/") and "/" or ".")
|
||||
end
|
||||
|
||||
-- get directory of vide root
|
||||
local root do
|
||||
local path = get_path(debug.info(1, "s"))
|
||||
root = path[#path - 1]
|
||||
end
|
||||
|
||||
-- finds the first stack depth outside of any vide library function
|
||||
return function(): number
|
||||
local stack = 1
|
||||
|
||||
local path = get_path(debug.info(stack, "s"))
|
||||
|
||||
while path[#path] == root or path[#path - 1] == root do
|
||||
stack += 1
|
||||
path = get_path(debug.info(stack, "s"))
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
131
src/values.luau
131
src/values.luau
|
|
@ -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
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
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
|
||||
local indexes = vide.indexes
|
||||
local values = vide.values
|
||||
local batch = vide.batch
|
||||
local cleanup = vide.cleanup
|
||||
local untrack = vide.untrack
|
||||
local create = vide.create
|
||||
local context = vide.context
|
||||
|
||||
assert(not vide.strict)
|
||||
|
||||
|
|
@ -27,7 +24,8 @@ local function ROOT_BENCH(name: string, fn: () -> ())
|
|||
end)()
|
||||
end
|
||||
|
||||
local N = 2^20
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
|
||||
TITLE "sources"
|
||||
|
||||
|
|
@ -200,7 +198,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 +207,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 +225,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 +240,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)
|
||||
|
|
@ -449,99 +447,106 @@ ROOT_BENCH("values() all remove", function()
|
|||
src(data)
|
||||
end)
|
||||
|
||||
TITLE "context()"
|
||||
N *= 1024
|
||||
|
||||
ROOT_BENCH("set context", function()
|
||||
local ctx = context()
|
||||
TITLE "cleanup"
|
||||
|
||||
for i = 1, START(N) do
|
||||
ctx(i, function() end)
|
||||
end
|
||||
end)
|
||||
ROOT_BENCH("register new cleanup", function()
|
||||
local cleanup = cleanup
|
||||
|
||||
ROOT_BENCH("get context (depth=1)", function()
|
||||
local ctx = context()
|
||||
local cleaner = function() end
|
||||
|
||||
local function run()
|
||||
for i = 1, START(N) do
|
||||
ctx()
|
||||
end
|
||||
end
|
||||
local callers = {}
|
||||
|
||||
ctx(1, function()
|
||||
run()
|
||||
end)
|
||||
end)
|
||||
|
||||
local depth = 10
|
||||
ROOT_BENCH(`get context (depth={depth})`, function()
|
||||
|
||||
local ctx = context()
|
||||
|
||||
local function run()
|
||||
for i = 1, START(N) do
|
||||
ctx()
|
||||
end
|
||||
end
|
||||
|
||||
local function nest_effect(fn)
|
||||
untrack(function()
|
||||
effect(fn)
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
local f = run
|
||||
for i = 1, depth - 1 do
|
||||
local f_inner = f
|
||||
f = function()
|
||||
nest_effect(f_inner)
|
||||
end
|
||||
end
|
||||
|
||||
ctx(1, function()
|
||||
f()
|
||||
end)
|
||||
end)
|
||||
|
||||
TITLE "spring()"
|
||||
|
||||
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)
|
||||
callers[i] = function(fn, v)
|
||||
fn(v)
|
||||
return i -- return unique upvalue to ensure unique closure
|
||||
end
|
||||
end
|
||||
|
||||
START(N)
|
||||
|
||||
src(1)
|
||||
|
||||
return nil
|
||||
end)
|
||||
for i = 1, START(N) do
|
||||
callers[i](cleanup, cleaner)
|
||||
end
|
||||
end)
|
||||
|
||||
ROOT_BENCH("spring step", 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
|
||||
end)
|
||||
|
||||
ROOT_BENCH("set aggregate mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
|
||||
local label = create "TextLabel" {
|
||||
AnchorPoint = Vector2.new(1, 1)
|
||||
}
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(label, {
|
||||
AnchorPoint = { i, i }
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
src(1)
|
||||
-- innacurate due to no Vector3 in vanilla Luau
|
||||
-- mock vector is 200x slower than native vector
|
||||
|
||||
START(N)
|
||||
-- ROOT_BENCH("spring update", function()
|
||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
vide.step(1/60)
|
||||
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
-- 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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
9
test/relative-string.luau
Normal file
9
test/relative-string.luau
Normal 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
|
||||
|
|
@ -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)
|
||||
repeat until os.clock() - program_time >= DT
|
||||
program_time += DT
|
||||
return DT
|
||||
end
|
||||
|
||||
vide.step(dt)
|
||||
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
|
||||
|
||||
return output()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
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 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", 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 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
|
||||
|
||||
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
|
||||
until false
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
1406
test/tests.luau
1406
test/tests.luau
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
name = "centau/vide"
|
||||
description = "A reactive Luau library for creating UI. "
|
||||
license = "MIT"
|
||||
version = "0.4.1"
|
||||
version = "0.1.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
include = ["default.project.json", "LICENSE", "src"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue