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.3.0" have entirely different histories.
84 changed files with 2207 additions and 3442 deletions
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
|
name: site-deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main] # todo: remove later
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -21,21 +24,21 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v3
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Build with VitePress
|
- name: Build with VitePress
|
||||||
run: npm run docs:build
|
run: npm run docs:build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v2
|
||||||
with:
|
with:
|
||||||
path: docs/.vitepress/dist
|
path: docs/.vitepress/dist
|
||||||
|
|
||||||
|
|
@ -49,4 +52,4 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Luau zip
|
- name: Install Luau zip
|
||||||
uses: robinraju/release-downloader@v1.9
|
uses: robinraju/release-downloader@v1.6
|
||||||
with:
|
with:
|
||||||
repository: Roblox/luau
|
repository: Roblox/luau
|
||||||
tag: "0.679"
|
tag: "0.620"
|
||||||
fileName: luau-ubuntu.zip
|
fileName: luau-ubuntu.zip
|
||||||
out-file-path: bin
|
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
|
.vscode
|
||||||
_local
|
_local
|
||||||
|
|
||||||
|
aftman.toml
|
||||||
sourcemap.json
|
sourcemap.json
|
||||||
|
|
||||||
docs/.vitepress/dist
|
docs/.vitepress/dist
|
||||||
docs/.vitepress/cache
|
docs/.vitepress/cache
|
||||||
docs/node_modules
|
docs/node_modules
|
||||||
docs/package-lock.json
|
docs/package-lock.json
|
||||||
|
|
||||||
luau_packages/
|
|
||||||
lune_packages/
|
|
||||||
.pesde/
|
|
||||||
|
|
||||||
wally.lock
|
|
||||||
pesde.lock
|
|
||||||
|
|
|
||||||
60
CHANGELOG.md
60
CHANGELOG.md
|
|
@ -6,66 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
## [0.4.1] - 2026-07-11
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `branch()` is now allowed to be used within a reactive scope.
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
- `create()` overloads. Supported is now only `create(class)(props)`.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `create()` types in the new solver should now work without `::`.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.4.0] - 2026-01-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `create("ClassName", { props })` and `create(Instance, { props })` syntax.
|
|
||||||
- `cleanup()` now accepts `thread` types.
|
|
||||||
- Implicit effects to set children can now recursively create more implicit
|
|
||||||
effects to set children.
|
|
||||||
- `spring()` returns a second value, a setter to set position, velocity and
|
|
||||||
impulse.
|
|
||||||
- Improved `spring()` updating and unscheduling.
|
|
||||||
- `show()` now receives a source to its callback returning the current value
|
|
||||||
of the condition.
|
|
||||||
- Ignore `false` passed as a child.
|
|
||||||
- Flag `vide.defaults` to disable the setting of default properties.
|
|
||||||
- Delayed scope destruction for control flow functions: `show()` `switch()` `indexes()` `values()`.
|
|
||||||
- Better `create()` types for the new type solver.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- A scope can no longer be destroyed while it is active. Strict mode will check
|
|
||||||
for this.
|
|
||||||
- Implicit effects to set children now unparent all children when the effect is
|
|
||||||
destroyed.
|
|
||||||
- Error reporting should be improved with better formatting when effects invoke
|
|
||||||
other effects and no more loss of stack traces.
|
|
||||||
- Nesting parent properties now work, and they are now also checked for
|
|
||||||
duplicates like other properties.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Aggregate construction when setting properties with `create()`.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.3.1] - 2024-10-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Context functions now also return results.
|
|
||||||
- `version` table with current version.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.3.0] - 2024-10-06
|
## [0.3.0] - 2024-10-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ for a quick introduction to the library.
|
||||||
|
|
||||||
## Code sample
|
## Code sample
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,6 @@ export default withMermaid({
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: "/logo.svg",
|
logo: "/logo.svg",
|
||||||
|
|
||||||
search: {
|
|
||||||
provider: "local"
|
|
||||||
},
|
|
||||||
|
|
||||||
footer: {
|
|
||||||
message: 'Released under the MIT License.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://vitepress.dev/reference/default-theme-config
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
nav: [
|
nav: [
|
||||||
{ text: "Home", link: "/" },
|
{ text: "Home", link: "/" },
|
||||||
|
|
@ -34,7 +26,7 @@ export default withMermaid({
|
||||||
items: [
|
items: [
|
||||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||||
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
{ 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: "Element Creation", link: "/api/creation" },
|
||||||
{ text: "Animation", link: "/api/animation" },
|
{ text: "Animation", link: "/api/animation" },
|
||||||
{ text: "Strict Mode", link: "/api/strict-mode" },
|
{ text: "Strict Mode", link: "/api/strict-mode" },
|
||||||
|
|
@ -52,25 +44,20 @@ export default withMermaid({
|
||||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||||
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
|
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
||||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||||
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
|
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
||||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||||
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Advanced",
|
text: "Advanced Reactivity",
|
||||||
items: [
|
|
||||||
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Design Patterns",
|
|
||||||
items: [
|
items: [
|
||||||
|
{ text: "Nested Scopes", link: "/tut/advanced/nested-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
|
// .vitepress/theme/index.js
|
||||||
import DefaultTheme from 'vitepress/theme'
|
import DefaultTheme from 'vitepress/theme'
|
||||||
import './vars.css'
|
import './vars.css'
|
||||||
import './index.css'
|
export default DefaultTheme
|
||||||
|
|
||||||
export default {
|
|
||||||
extends: DefaultTheme,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,3 @@
|
||||||
/* Colors */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-c-brand-1: #3086ff;
|
--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.
|
Returns a new source with a value always moving torwards the input source value.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function spring<T>(
|
function spring<T>(
|
||||||
source: () -> T & Animatable,
|
source: () -> T & Animatable,
|
||||||
period: number = 1,
|
period: number = 1,
|
||||||
damping_ratio: number = 1
|
damping_ratio: number = 1
|
||||||
): (() -> T, SpringControl<T>)
|
): () -> T
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
||||||
|
|
||||||
type SpringControl<T> = ({
|
|
||||||
position: T?,
|
|
||||||
velocity: T?,
|
|
||||||
impulse: T?
|
|
||||||
}) -> ()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Creates a reactive scope internally to detect source updates.
|
An effect is created to update the new source every frame based on the input
|
||||||
|
source value.
|
||||||
|
|
||||||
The movement is physically simulated according to a
|
The movement is physically simulated according to a
|
||||||
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
[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
|
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
|
advance the simulation time by `dt` seconds and automatically stop the
|
||||||
solver running in heartbeat.
|
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 stable scope and optionally applies its result to a
|
||||||
|
target instance.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
The result of the function is applied to a target in the same way
|
||||||
|
properties are using `create()`.
|
||||||
|
|
||||||
|
The function is ran in a new stable scope, just like
|
||||||
|
[root()](reactivity-core.md#root).
|
||||||
|
|
||||||
|
Returns a function that when called will destroy the stable scope.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function App()
|
||||||
|
return create "ScreenGui" {
|
||||||
|
create "TextLabel" { Text = "Vide" }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
|
```
|
||||||
|
|
||||||
## create()
|
## create()
|
||||||
|
|
||||||
|
|
@ -6,11 +41,11 @@ Creates a new UI element, applying any given properties.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function create(class: string): (Properties) -> Instance
|
function create(class: string): (Properties) -> Instance
|
||||||
function create(instance: Instance): (Properties) -> Instance
|
function create(instance: Instance): (Properties) -> Instance
|
||||||
|
|
||||||
type Properties = Map<string|number, unknown>
|
type Properties = Map<string|number, any>
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
@ -41,53 +76,73 @@ Creates a new UI element, applying any given properties.
|
||||||
|
|
||||||
Basic element creation.
|
Basic element creation.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local frame = create "TextButton" {
|
local frame = create "Frame" {
|
||||||
Name = "Button",
|
Name = "NewFrame",
|
||||||
Size = UDim2.fromOffset(200, 160),
|
Position = UDim2.fromScale(1, 0)
|
||||||
|
|
||||||
Activated = function()
|
|
||||||
print "clicked"
|
|
||||||
end,
|
|
||||||
|
|
||||||
create "UICorner" {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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()
|
## action()
|
||||||
|
|
||||||
Creates a special object that can be passed to `create()` to invoke custom
|
Creates a callback that can be passed to `create()` to invoke custom actions on
|
||||||
actions on instances.
|
instances.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function action((Instance) -> (), priority: number = 1): Action
|
function action((Instance) -> (), priority: number = 1): Action
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
When passed to `create()`, the function is called with the instance being
|
When passed to `create()`, the given callback is called with the instance
|
||||||
created as the only argument. Actions take precedence over property and
|
being created as the only argument. Actions take precedence over property
|
||||||
child assignments.
|
and child assignments.
|
||||||
|
|
||||||
A priority can be optionally specified to ensure certain actions run after
|
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**
|
- **Example**
|
||||||
|
|
||||||
An action to listen to changed properties:
|
An action to listen to changed properties:
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local function changed(property: string, fn: (new) -> ())
|
local function changed(property: string, callback: (new) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
|
local con - instance:GetPropertyChangedSignal(property):Connect(function()
|
||||||
fn(instance[property])
|
callback(instance[property])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- disconnect on scope destruction to allow gc of instance
|
-- disconnect on reactive scope destruction to allow gc of instance
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
cn:Disconnect()
|
con:Disconnect()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -95,7 +150,7 @@ actions on instances.
|
||||||
local output = source ""
|
local output = source ""
|
||||||
|
|
||||||
create "TextBox" {
|
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)
|
changed("Text", output)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -106,47 +161,16 @@ A wrapper for `action()` to listen for property changes.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function changed(property: string, fn: (unknown) -> ()): Action
|
function changed(property: string, callback: (...unknown) -> ()): Action
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Will run the given function immediately and whenever the property updates.
|
Will run the given callback any time the property is changed, as well as
|
||||||
|
when the action is initially run.
|
||||||
|
|
||||||
The function is called with the updated property value.
|
The changed connection is disconnected when the scope the action is ran in
|
||||||
|
is destroyed.
|
||||||
|
|
||||||
Runs with an action priority of 1.
|
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,44 +1,36 @@
|
||||||
# Reactivity: Core
|
# Reactivity API: Core
|
||||||
|
|
||||||
## Scopes
|
<br/>
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
||||||
for this.
|
for this.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
## root()
|
||||||
|
|
||||||
Runs a function in a new stable scope.
|
Creates and runs a function in a new stable scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
function root<T...>(fn: (() -> ()) -> T...): (() -> (), T...)
|
||||||
|
|
||||||
type Destructor = () -> ()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Returns a destructor and any values returned by the callback.
|
Returns a function to destroy the root scope. Also passes this function as
|
||||||
|
the first argument into its callback.
|
||||||
|
|
||||||
|
All values returned by the callback are also returned following the destructor.
|
||||||
|
|
||||||
## source()
|
## source()
|
||||||
|
|
||||||
Creates a new source.
|
Creates a new source with the given value.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function source<T>(value: T): Source<T>
|
function source<T>(value: T): Source<T>
|
||||||
|
|
||||||
type Source<T> =
|
type Source<T> =
|
||||||
|
|
@ -48,108 +40,83 @@ Creates a new source.
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Call the returned source with no argument to read its value.
|
Calling the returned source with no argument will return its stored value,
|
||||||
Call the returned source with an argument to set its value.
|
calling with an argument will set a new value.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
print(count())-- 0
|
|
||||||
count(count() + 1)
|
count() -- 0
|
||||||
print(count()) -- 1
|
|
||||||
|
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**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function effect(fn: () -> ())
|
function effect(callback: () -> ())
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
The function is ran once immediately.
|
Any time a source referenced in the callback is updated, the callback will
|
||||||
|
be reran.
|
||||||
|
|
||||||
|
The callback is ran once immediately.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(1)
|
local num = source(1)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
print(count())
|
print(num())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- prints 1
|
-- prints 1
|
||||||
|
|
||||||
count(2)
|
num(num() + 1)
|
||||||
|
|
||||||
-- prints 2
|
-- 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**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function derive<T>(fn: () -> T): () -> T
|
function derive<T>(source: () -> T): () -> T
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Anytime the reactive scope reruns, the output source value is set to what is
|
The derived source will have its value recalculated when any source source
|
||||||
returned.
|
it derives from is updated.
|
||||||
|
|
||||||
The function is ran once immediately.
|
Anytime its value is recalculated it is also cached, subsequent calls will
|
||||||
|
retun this cached value until it recalculates again.
|
||||||
|
|
||||||
|
The callback is ran once immediately.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
local text = derive(function() return `count: {count()}` end)
|
local text = derive(function() return `count: {count()}` end)
|
||||||
|
|
||||||
print(text()) -- "count: 0"
|
text() -- "count: 0"
|
||||||
|
|
||||||
count(1)
|
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 stable scope. If it changes from a
|
||||||
|
truthy to falsey value, the stable scope the component was created in will
|
||||||
|
be destroyed, and the returned source will output `nil`, or a fallback
|
||||||
|
component if given.
|
||||||
|
|
||||||
|
The fallback component is also ran under a new stable scope, and destroyed
|
||||||
|
when the input source switches back to truthy.
|
||||||
|
|
||||||
|
## switch()
|
||||||
|
|
||||||
|
Shows one of a set of components depending on an input source and a mapping table.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an instance of the currently shown component.
|
||||||
|
|
||||||
|
When the input source changes, the new value will be used to lookup a given
|
||||||
|
mapping table to get a component, which will be ran under a new stable
|
||||||
|
scope. If the input source changes, the stable scope the component was
|
||||||
|
created in will be destroyed, and a new component created under a new
|
||||||
|
stable scope. If no component is found for an input value, the switch will
|
||||||
|
output `nil`.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local logged = source(false)
|
||||||
|
|
||||||
|
local button = switch(logged) {
|
||||||
|
[true] = function()
|
||||||
|
return Button { Text = "Log out", Toggle = logged }
|
||||||
|
end,
|
||||||
|
|
||||||
|
[false] = function()
|
||||||
|
return Button { Text = "Log in", Toggle = logged }
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## indexes()
|
||||||
|
|
||||||
|
Maps each index in a table source to an object.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function indexes<KI, VI, VO>(
|
||||||
|
source: () -> Map<KI, VI>,
|
||||||
|
transform: (value: () -> VI, index: KI) -> VO
|
||||||
|
): Array<VO>
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an array of instances currently shown.
|
||||||
|
|
||||||
|
When the input source changes, each *index* in the new table is compared with
|
||||||
|
the last input table.
|
||||||
|
|
||||||
|
- For any new index, the `transform` function is ran under a new stable
|
||||||
|
scope to produce a new instance.
|
||||||
|
- For any removed index, the stable scope for that index is destroyed.
|
||||||
|
- Unchanged indexes are untouched.
|
||||||
|
|
||||||
|
The transform function is called only ever *once* for each index in the
|
||||||
|
source table.
|
||||||
|
|
||||||
|
1. First argument is a *source containing the index's value*.
|
||||||
|
2. Second argument is the *index itself*.
|
||||||
|
|
||||||
|
Anytime an existing index's value changes, the transform function is not
|
||||||
|
rerun, instead the source value for that index will update, causing anything
|
||||||
|
depending on it to update too.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
The intended purpose of this function is to map each index in a table to
|
||||||
|
a UI element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Item = {
|
||||||
|
name: string,
|
||||||
|
icon: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local items = source {} :: () -> Array<Item>
|
||||||
|
|
||||||
|
local displays = indexes(items, function(item, i)
|
||||||
|
return ItemDisplay {
|
||||||
|
Name = function()
|
||||||
|
return i .. ": " .. item().name
|
||||||
|
end,
|
||||||
|
|
||||||
|
Image = function()
|
||||||
|
return "rbxassetid://" .. item().icon
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## values()
|
||||||
|
|
||||||
|
Maps each value in a table source to an object.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function values<KI, VI, VO>(
|
||||||
|
source: () -> Map<KI, VI>,
|
||||||
|
transform: (value: VI, index: () -> KI) -> VO
|
||||||
|
): Array<VO>
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an array of instances currently shown.
|
||||||
|
|
||||||
|
When the input source changes, each *value* in the new table is compared with
|
||||||
|
the last input table. Similar to `indexes()` but for values instead of indexes.
|
||||||
|
|
||||||
|
- For any new value, the `transform` function is ran under a new stable
|
||||||
|
scope to produce a new instance.
|
||||||
|
- For any removed value, the stable scope for that value is destroyed.
|
||||||
|
- Unchanged values are untouched.
|
||||||
|
|
||||||
|
The transform function is only ever called *once* for each value in the
|
||||||
|
source table.
|
||||||
|
|
||||||
|
1. First argument is the *value itself*.
|
||||||
|
2. Second argument is a *source containing the value's index*.
|
||||||
|
|
||||||
|
Anytime an existing value's index changes, the transform function is not
|
||||||
|
rerun, instead the source index for that value will update, causing anything
|
||||||
|
depending on it to update too.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Having primitive values in the input source table can cause unexpected
|
||||||
|
behavior, as duplicate values can result in multiple tranforms being ran for
|
||||||
|
a single value, meaning there can be multiple source indexes bound to the
|
||||||
|
same UI element. Strict mode has checks for this.
|
||||||
|
:::
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
The intended purpose of this function is to map each value in a table to
|
||||||
|
a UI element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Item = {
|
||||||
|
name: string,
|
||||||
|
icon: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local items = source {} :: () -> Array<Item>
|
||||||
|
|
||||||
|
local displays = values(items, function(item, i)
|
||||||
|
return ItemDisplay {
|
||||||
|
Name = function()
|
||||||
|
return i() .. ": " .. item.Name
|
||||||
|
end
|
||||||
|
|
||||||
|
Image = "rbxassetid://" .. item.icon,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Extra**
|
||||||
|
|
||||||
|
When should you use `indexes()` and `values()`?
|
||||||
|
|
||||||
|
`values()` should be used when you have a fixed set of objects where the
|
||||||
|
same objects can be re-arranged in the source table. It maps a value to a
|
||||||
|
UI element.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
- List of all players.
|
||||||
|
- Inventory of items.
|
||||||
|
- Chat message history.
|
||||||
|
- Toast notifications.
|
||||||
|
|
||||||
|
`indexes()` should be used in other cases, especially when your source table
|
||||||
|
has primitive values. It maps an index to a UI element.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
- List of character or weapon stats.
|
||||||
|
|
||||||
|
In most cases, both functions will appear to have the same behavior.
|
||||||
|
The main difference is performance, picking the right function to use can
|
||||||
|
result in less property updates and less re-renders. One case to note is
|
||||||
|
that `values()` works nicely when animating re-ordering of instances, since
|
||||||
|
the value is not destroyed when indexes are changed, and the source index
|
||||||
|
can be used to animate a change in position for the UI element.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
@ -1,47 +1,41 @@
|
||||||
# Reactivity: Utility
|
# Reactivity API: Utility
|
||||||
|
|
||||||
## cleanup()
|
## cleanup()
|
||||||
|
|
||||||
Queues a callback to run when a scope is reran or destroyed.
|
Runs a callback anytime a scope is reran or destroyed.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function cleanup(v: Function | Disconnectable | Destroyable | thread)
|
function cleanup(callback: () -> ())
|
||||||
|
function cleanup(obj: Destroyable)
|
||||||
|
function cleanup(obj: Disconnectable)
|
||||||
|
|
||||||
type Function = () -> ()
|
|
||||||
type Destroyable = { destroy: () -> () }
|
type Destroyable = { destroy: () -> () }
|
||||||
type Disconnectable = { disconnect: () -> () }
|
type Disconnectable = { disconnect: () -> () }
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local data = source(1)
|
||||||
|
|
||||||
local destroy = root(function()
|
|
||||||
effect(function()
|
effect(function()
|
||||||
count()
|
local label = create "TextLabel" { Text = data() }
|
||||||
|
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
print "cleaned"
|
label:Destroy()
|
||||||
end)
|
end)
|
||||||
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 in a new stable scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function untrack<T>(source: () -> T): T
|
function untrack<T>(source: () -> T): T
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -52,7 +46,7 @@ Runs a function in a new stable scope.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local a = source(0)
|
local a = source(0)
|
||||||
local b = source(0)
|
local b = source(0)
|
||||||
|
|
||||||
|
|
@ -61,84 +55,65 @@ Runs a function in a new stable scope.
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(sum()) -- 0
|
print(sum()) -- 0
|
||||||
b(1) -- untracked so reactive scope created by derive() does not rerun
|
b(1)
|
||||||
print(sum()) -- 0
|
print(sum()) -- 0
|
||||||
a(1) -- reactive scope created by derive() reruns
|
a(1)
|
||||||
print(sum()) -- 2
|
print(sum()) -- 2
|
||||||
```
|
```
|
||||||
|
|
||||||
## read()
|
## 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**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function read<T>(value: T | () -> T): T
|
function read<T>(value: T | () -> T): T
|
||||||
```
|
```
|
||||||
|
|
||||||
## batch()
|
## batch()
|
||||||
|
|
||||||
Runs a function where any source updates made within the function do not
|
Runs a given function where any source updates made within the function do not
|
||||||
trigger effects until after the function ends.
|
trigger effects until after the function finishes running.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function batch(fn: () -> ())
|
function batch(fn: () -> ())
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Improves performance when an effect depends on multiple sources, and those
|
Improves performance when an effect depends on multiple sources, and those
|
||||||
sources need to be updated.
|
sources need to be updated. Updating those sources inside a batch call will
|
||||||
|
only cause the effect to run once after the batch call ends instead of after
|
||||||
|
each time a source is updated.
|
||||||
|
|
||||||
- **Example**
|
## context()
|
||||||
|
|
||||||
```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.
|
Creates a new context.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function context<T>(default: T): Context<T>
|
function context<T>(default: T): Context<T>
|
||||||
|
|
||||||
type Context<T> =
|
type Context<T> =
|
||||||
() -> T -- get
|
() -> T -- get
|
||||||
& <U>(T, () -> U) -> U -- set
|
& (T, () -> ()) -> () -- set
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Calling `context()` returns a new context function.
|
Calling `context()` returns a new context function.
|
||||||
Call this function with no arguments to get the context value.
|
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
|
Call this function with a value and a callback to set a new context with the
|
||||||
the given value.
|
given value.
|
||||||
|
|
||||||
The new context is run under a stable scope.
|
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local theme = context()
|
local theme = context()
|
||||||
|
|
||||||
local function Button()
|
local function Button()
|
||||||
|
|
@ -156,3 +131,4 @@ Creates a new context.
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Strict mode is library-wide and can get set by doing:
|
Strict mode is library-wide and can get set by doing:
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
vide.strict = true
|
vide.strict = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -14,24 +14,25 @@ and identifying improper usage.
|
||||||
|
|
||||||
Currently, strict mode will:
|
Currently, strict mode will:
|
||||||
|
|
||||||
1. Run reactive scopes twice when a source updates.
|
1. Run derived sources twice a source updates.
|
||||||
2. Throw an error if yields occur where they are not allowed.
|
2. Run effects twice when a source updates.
|
||||||
3. Checks for `indexes()` and `values()` outputting primitive values.
|
3. Throw an error if yields occur where they are not allowed.
|
||||||
4. Checks for `values()` input having duplicate values.
|
4. Checks for `indexes()` and `values()` returning primitive values.
|
||||||
5. Checks for duplicate nested properties at same depth.
|
5. Checks for `values()` input having duplicate values.
|
||||||
6. Checks for destruction of an active scope.
|
6. Checks for duplicate nested properties at same depth.
|
||||||
7. Better error reporting and stack traces.
|
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
|
By rerunning derived sources and effects twice each time they update, it helps
|
||||||
computations are pure, and that any cleanup is done correctly.
|
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,
|
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||||
which strict mode will catch.
|
which strict mode will catch.
|
||||||
|
|
||||||
As well as additional safety checks, Vide will dedicate extra resources to
|
As well as additional safety checks, Vide will dedicate extra resources to
|
||||||
recording and better emitting stack traces where errors occur, particularly
|
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
|
It is recommended to develop UI with strict mode and to disable it when pushing to
|
||||||
production. In Roblox, production code compiles at O2 by default, so you do not
|
production. In Roblox, production code compiles at O2 by default, so you don't
|
||||||
need to worry about disabling strict mode unless you have manually enabled it.
|
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
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
layout: home
|
layout: home
|
||||||
pageClass: home
|
|
||||||
next:
|
|
||||||
text: 'Introduction'
|
|
||||||
link: '/tut/crash-course/1-introduction'
|
|
||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: "Vide"
|
name: Vide
|
||||||
|
text: ""
|
||||||
tagline: A reactive UI library for Luau.
|
tagline: A reactive UI library for Luau.
|
||||||
image:
|
image:
|
||||||
src: /logo.svg
|
src: /logo.svg
|
||||||
|
alt: Vide
|
||||||
actions:
|
actions:
|
||||||
- theme: brand
|
- theme: brand
|
||||||
text: Tutorials
|
text: Tutorials
|
||||||
|
|
@ -19,4 +17,8 @@ hero:
|
||||||
text: API Reference
|
text: API Reference
|
||||||
link: /api/reactivity-core
|
link: /api/reactivity-core
|
||||||
|
|
||||||
|
features:
|
||||||
|
- title: In Development
|
||||||
|
details: Not recommended for production use.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "vitepress dev",
|
"docs:dev": "vitepress dev",
|
||||||
"docs:build": "vitepress build",
|
"docs:build": "vitepress build",
|
||||||
"docs:preview": "vitepress preview"
|
"docs:preview": "vitepress preview"
|
||||||
},
|
},
|
||||||
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitepress": "^1.4.5",
|
"vitepress": "1.0.0-rc.25",
|
||||||
"vitepress-plugin-mermaid": "2.0.17"
|
"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 filter="url(#filter0_d_1_41)">
|
||||||
<g clip-path="url(#clip0_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)"/>
|
<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">
|
<svg width="384" height="384" viewBox="8 8 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g filter="url(#filter0_ii_282_2)">
|
<g filter="url(#filter0_ddd_1_134)">
|
||||||
<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="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="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="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="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"/>
|
<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>
|
</g>
|
||||||
<defs>
|
<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"/>
|
<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"/>
|
<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"/>
|
<feOffset dx="2" dy="4"/>
|
||||||
<feGaussianBlur stdDeviation="96"/>
|
<feGaussianBlur stdDeviation="8"/>
|
||||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<feOffset dx="1" dy="2"/>
|
||||||
<feGaussianBlur stdDeviation="48"/>
|
<feGaussianBlur stdDeviation="4"/>
|
||||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
<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"/>
|
<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>
|
</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 stop-color="#24447F"/>
|
||||||
<stop offset="1" stop-color="#3661A2"/>
|
<stop offset="1" stop-color="#3661A2"/>
|
||||||
</radialGradient>
|
</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 stop-color="#1D314F"/>
|
||||||
<stop offset="1" stop-color="#0D1A2E"/>
|
<stop offset="1" stop-color="#0D1A2E"/>
|
||||||
</radialGradient>
|
</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.
|
|
||||||
192
docs/tut/advanced/nested-scoping.md
Normal file
192
docs/tut/advanced/nested-scoping.md
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
# Nested Scopes
|
||||||
|
|
||||||
|
Nesting scopes gives you finer control over the reactive graph, but needs more work to do. The built-in control flow functions try to cover the
|
||||||
|
most common cases, but they do not cover all of them.
|
||||||
|
|
||||||
|
This tutorial will demonstrate how to implement a `show()` control flow function
|
||||||
|
using just sources and effects.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local mount = vide.mount
|
||||||
|
local source = vide.source
|
||||||
|
local show = vide.show
|
||||||
|
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
return create "TextButton" {
|
||||||
|
Text = count,
|
||||||
|
Activated = function() count(count() + 1) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
local toggled = source(true)
|
||||||
|
|
||||||
|
show(toggled, Button)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph mount
|
||||||
|
direction LR
|
||||||
|
toggle --> show
|
||||||
|
|
||||||
|
subgraph show[show effect]
|
||||||
|
text[Text effect]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Above is the reactive graph for `show()`. It creates a new effect depending on
|
||||||
|
`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
|
||||||
|
`show` effect calls `Counter`, which creates a new reactive scope to update its
|
||||||
|
text whenever `count` changes. As per the rules of reactive scopes, a reactive
|
||||||
|
scope rerunning will destroy any scopes created within it. So the text
|
||||||
|
effect's reactive scope is destroyed whenever the show effect is rerun.
|
||||||
|
|
||||||
|
The same can be achieved without the use of `show()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local mount = vide.mount
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
return create "TextButton" {
|
||||||
|
Text = count,
|
||||||
|
Activated = function() count(count() + 1) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(function()
|
||||||
|
local toggled = source(true)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
if toggled() then
|
||||||
|
local destroy = root(function()
|
||||||
|
Counter()
|
||||||
|
end)
|
||||||
|
cleanup(destroy)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph mount
|
||||||
|
direction LR
|
||||||
|
toggle --> effect
|
||||||
|
|
||||||
|
subgraph effect
|
||||||
|
subgraph mount2[inner mount]
|
||||||
|
text[Text effect]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This is another way to achieve the same. Here we use `root()` within the effect
|
||||||
|
to manually create and destroy a new stable scope whenever the effect reruns.
|
||||||
|
|
||||||
|
The reason for creating a stable scope is to prevent the effect from tracking
|
||||||
|
any sources that may be read inside the `Counter()` call. Otherwise, the effect
|
||||||
|
may be rerun needlessly and recreate the counter.
|
||||||
|
|
||||||
|
Alternatively, instead of using `root()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local mount = vide.mount
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
local untrack = vide.untrack
|
||||||
|
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
return create "TextButton" {
|
||||||
|
Text = count,
|
||||||
|
Activated = function() count(count() + 1) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(function()
|
||||||
|
local toggled = source(true)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
if toggled() then
|
||||||
|
untrack(Button)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph mount
|
||||||
|
direction LR
|
||||||
|
toggle --> effect
|
||||||
|
|
||||||
|
subgraph effect
|
||||||
|
text[Text effect]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Without the use of `untrack()`, an error would occur, since Vide does not allow
|
||||||
|
the creation of reactive scopes inside reactive scopes. `untrack()` creates a
|
||||||
|
stable scope inside the reactive scope, and we can create another reactive scope
|
||||||
|
inside that stable scope. The
|
||||||
|
reason for this, is because if the `Counter` component reads from a source
|
||||||
|
internally, that can cause the reactive scope calling `Counter()` to track that
|
||||||
|
source, causing unintentional reruns. As a guard against this, you are forced to
|
||||||
|
use `untrack()` to create nested reactive scopes.
|
||||||
|
|
||||||
|
The final result is the same as using the `show()` component. An effect is
|
||||||
|
created which creates the counter, which creates its own reactive scope. The
|
||||||
|
effect rerunning causes the counter's internal reactive scope to be destroyed,
|
||||||
|
making sure everything is cleaned up.
|
||||||
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()
|
||||||
|
|
@ -6,16 +6,10 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||||
|
|
||||||
## Why Vide?
|
## Why Vide?
|
||||||
|
|
||||||
Vide's reactive and declarative API aims to let you program UI as simply as
|
Vide provides a reactive and declarative API to simplify managing UI.
|
||||||
possible, with a strong focus on how data flows through your application.
|
|
||||||
|
|
||||||
Some of Vide's main design choices:
|
Some of the main focuses behind Vide's design choices:
|
||||||
|
|
||||||
- Syntax minimal.
|
- Minimal syntax
|
||||||
- Data oriented.
|
- Complete typechecking
|
||||||
- Typechecking compatible.
|
- Independence from instances
|
||||||
- Instance independent.
|
|
||||||
|
|
||||||
Vide's reactivity operates with the concept
|
|
||||||
of scopes which carries a learning curve, though is what makes Vide's minimal
|
|
||||||
syntax possible. The crash course will introduce these concepts gradually.
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||||
is used to queue a callback for the next time a reactive scope is rerun or
|
is used to queue a callback for the next time a reactive scope is rerun or
|
||||||
destroyed, or when a stable scope is destroyed.
|
destroyed, or when a stable scope is destroyed.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local root = vide.root
|
local root = vide.root
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local cleanup = vide.cleanup
|
|
||||||
|
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
|
|
||||||
local destroy = root(function()
|
local destroy = root(function()
|
||||||
effect(function()
|
effect(function()
|
||||||
local x = count()
|
local x = count()
|
||||||
|
|
|
||||||
98
docs/tut/crash-course/11-control-flow.md
Normal file
98
docs/tut/crash-course/11-control-flow.md
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
# Control Flow
|
||||||
|
|
||||||
|
Eventually you may need a way to dynamically create and destroy UI elements
|
||||||
|
resulting from source updates. Vide provides functions to help you do this,
|
||||||
|
known as *control flow* functions.
|
||||||
|
|
||||||
|
These functions return new sources, which hold the instances to be displayed.
|
||||||
|
The new sources can be used in `create()` to update the children of a container
|
||||||
|
instance.
|
||||||
|
|
||||||
|
## indexes()
|
||||||
|
|
||||||
|
`indexes()` *maps* each table index to a new UI element that can
|
||||||
|
update to display the current value at that index. Each table index is given a
|
||||||
|
single corresponding UI element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local list = source {
|
||||||
|
"finish the crash course",
|
||||||
|
"star Vide's GitHub"
|
||||||
|
}
|
||||||
|
|
||||||
|
local function TodoList(props: { list: () -> Array<string> })
|
||||||
|
return create "Frame" {
|
||||||
|
create "UIListLayout" {},
|
||||||
|
|
||||||
|
indexes(list, function(todo, i)
|
||||||
|
return create "TextLabel" {
|
||||||
|
Text = function()
|
||||||
|
return i .. ": " .. todo()
|
||||||
|
end,
|
||||||
|
|
||||||
|
LayoutOrder = i
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
TodoList { list = list }
|
||||||
|
```
|
||||||
|
|
||||||
|
For each index in the given source table, the given function to `indexes()` will
|
||||||
|
be run in a new stable scope with:
|
||||||
|
|
||||||
|
1. a source containing the value at the index
|
||||||
|
2. the index itself
|
||||||
|
|
||||||
|
When the value at an index is changed, the function is not reran. Instead, the
|
||||||
|
given source for that index is updated.
|
||||||
|
|
||||||
|
Any time the input source table is updated, the given function will be ran for
|
||||||
|
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||||
|
value), will have its corresponding stable scope destroyed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph root ["root scope"]
|
||||||
|
direction LR
|
||||||
|
todoList --> indexes -.- subroot1 & subroot2
|
||||||
|
|
||||||
|
subgraph subroot1 ["indexes scope 1"]
|
||||||
|
direction LR
|
||||||
|
value1[todo] --> prop1["prop binding"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph subroot2 ["indexes scope 2"]
|
||||||
|
direction LR
|
||||||
|
value2[todo] --> prop2[prop binding]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
When you edit a table in a source, you must set that table again to actually
|
||||||
|
update the source.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local src = source { 1, 2 }
|
||||||
|
local data = src()
|
||||||
|
table.insert(data, 3) -- no effects will run
|
||||||
|
src(data) -- effects will run
|
||||||
|
```
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
# Actions
|
# 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.
|
to run some code on an instance receiving them.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "test",
|
Text = "test",
|
||||||
|
|
||||||
|
|
@ -20,15 +22,14 @@ create "TextLabel" {
|
||||||
Actions can be wrapped with functions for reuse. Below is an example of an
|
Actions can be wrapped with functions for reuse. Below is an example of an
|
||||||
action used to listen for property changes:
|
action used to listen for property changes:
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local cleanup = vide.cleanup
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
local function changed(property: string, callback: (new) -> ())
|
local function changed(prop: string, callback: (new) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
|
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
|
||||||
callback(instance[property])
|
callback(instance[property])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ effect(function()
|
||||||
print(output())
|
print(output())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
instance.Text = "foo" -- "foo" will be printed by the effect
|
instance.Text = "foo" -- "foo" will be printed from the effect
|
||||||
```
|
```
|
||||||
|
|
||||||
The source `output` will be updated with the new property value any time it is
|
The source `output` will be updated with the new property value any time it is
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Strict mode will run derived sources and effects twice each time they update.
|
||||||
This is to help ensure that derived source computations are pure, and that any
|
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 properly.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -23,7 +23,6 @@ local count = source(0)
|
||||||
|
|
||||||
local ran = 0
|
local ran = 0
|
||||||
effect(function()
|
effect(function()
|
||||||
count()
|
|
||||||
ran += 1
|
ran += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Created by:
|
||||||
|
|
||||||
- `root()`
|
- `root()`
|
||||||
- `untrack()`
|
- `untrack()`
|
||||||
- `show()`
|
- `switch()`
|
||||||
- `indexes()`
|
- `indexes()`
|
||||||
|
|
||||||
Stable scopes do not track sources and never rerun.
|
Stable scopes do not track sources and never rerun.
|
||||||
|
|
@ -47,14 +47,23 @@ Created by:
|
||||||
Reactive scopes do track sources and will rerun when those sources update.
|
Reactive scopes do track sources and will rerun when those sources update.
|
||||||
|
|
||||||
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
||||||
can be created within a reactive scope.
|
can.
|
||||||
|
|
||||||
## Scope Cleanup
|
## Scope Owners
|
||||||
|
|
||||||
When a scope is rerun or destroyed, all scopes created within it are
|
A scope created within another scope is *owned* by the other scope, with the
|
||||||
automatically destroyed.
|
exception of the scope created by `root()`.
|
||||||
|
|
||||||
Any functions queued by `cleanup()` are also ran.
|
When a scope is rerun or destroyed, all scopes owned by it are automatically
|
||||||
|
destroyed.
|
||||||
|
|
||||||
|
`root()` creates a stable scope with no owner, instead it is destroyed manually.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
|
||||||
|
|
||||||
|
Queue a function to run using `cleanup()`.
|
||||||
|
|
||||||
## Reactive Graph
|
## Reactive Graph
|
||||||
|
|
||||||
|
|
@ -64,12 +73,12 @@ relationships between effects and the sources they depend on.
|
||||||
|
|
||||||
### Code
|
### Code
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
root(function()
|
root(function()
|
||||||
local text = derive(function()
|
local text = derive(function()
|
||||||
return "count: " .. count()
|
return "count: " .. text()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
|
|
@ -84,12 +93,12 @@ end)
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#111720",
|
"primaryColor": "#1B1B1F",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#111720",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#0d131b",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#202530"
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -109,5 +118,6 @@ Notes:
|
||||||
- An update to `count` will cause `text` to rerun, which
|
- An update to `count` will cause `text` to rerun, which
|
||||||
then causes `effect` to rerun.
|
then causes `effect` to rerun.
|
||||||
- When the root scope is destroyed, `text` and
|
- When the root scope is destroyed, `text` and
|
||||||
`effect` will be destroyed alongside it, since they were created within it.
|
`effect` will be destroyed alongside it, since they are
|
||||||
`count` will be untouched and future updates to `count` will have no effect.
|
owned by it. `count` will be untouched and future updates
|
||||||
|
to `count` will have no effect.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Instances are created using `create()`.
|
||||||
Parentheses `()` can be omitted when calling functions with string or
|
Parentheses `()` can be omitted when calling functions with string or
|
||||||
table literals for brevity.
|
table literals for brevity.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
return create "ScreenGui" {
|
return create "ScreenGui" {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ together.
|
||||||
|
|
||||||
::: code-group
|
::: code-group
|
||||||
|
|
||||||
```luau [Button.luau]
|
```lua [Button.luau]
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local function Button(props: {
|
local function Button(props: {
|
||||||
|
|
@ -34,7 +34,7 @@ end
|
||||||
return Button
|
return Button
|
||||||
```
|
```
|
||||||
|
|
||||||
```luau [Menu.luau]
|
```lua [Menu.luau]
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local Button = require(Button)
|
local Button = require(Button)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Vide's reactivity.
|
||||||
|
|
||||||
A source can be created using `source()`.
|
A source can be created using `source()`.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
@ -16,13 +16,13 @@ The value passed to `source()` is the initial value of the source.
|
||||||
The value of a source can be set by calling it with an argument, and can be read
|
The value of a source can be set by calling it with an argument, and can be read
|
||||||
by calling it with no arguments.
|
by calling it with no arguments.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
count(count() + 1) -- increment count by 1
|
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.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
local text = function()
|
local text = function()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Effects
|
# Effects
|
||||||
|
|
||||||
Effects are functions that are ran in response to source updates.
|
Effects are functions that are ran in response to source updates. They are
|
||||||
A source and effect is analogous to a signal and connection.
|
A source and effect is analogous to a signal and connection.
|
||||||
|
|
||||||
Effects are created using `effect()`.
|
Effects are created using `effect()`.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -23,13 +23,10 @@ count(1)
|
||||||
Any source read inside an effect is tracked and will rerun the effect when
|
Any source read inside an effect is tracked and will rerun the effect when
|
||||||
that source is updated.
|
that source is updated.
|
||||||
|
|
||||||
The effect runs its callback once immediately to initially figure out what
|
Derived sources are also tracked, it doesn't matter how deeply nested
|
||||||
sources are being read.
|
|
||||||
|
|
||||||
Derived sources are also tracked, it does not matter how deeply nested
|
|
||||||
inside a function a source is.
|
inside a function a source is.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -50,22 +47,3 @@ count(2)
|
||||||
|
|
||||||
If a source is updated with the same value it already had, it will not rerun
|
If a source is updated with the same value it already had, it will not rerun
|
||||||
effects depending on it.
|
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"
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -7,46 +7,62 @@ But the disconnecting of many signals and connections is tedious and verbose.
|
||||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||||
API, given that you follow a few rules.
|
API, given that you follow a few rules.
|
||||||
|
|
||||||
There are two types of scopes: stable and reactive.
|
Scopes come in two flavors; stable and reactive.
|
||||||
|
|
||||||
- A scope must be created within another scope.
|
- All scopes must be created within another scope with the exception of `root()`
|
||||||
- Stable scopes never rerun.
|
- Stable scopes never rerun
|
||||||
- Reactive scopes can rerun.
|
- Reactive scopes can rerun
|
||||||
- A reactive scope cannot be created within another reactive scope, only within
|
- A reactive scope cannot be created within another reactive scope
|
||||||
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.
|
`effect()` creates a reactive scope.
|
||||||
|
`root()` creates a stable scope.
|
||||||
|
|
||||||
Whenever a scope is destroyed, any scope created within that scope is also
|
Whenever a scope is destroyed, any scope created within that scope is also
|
||||||
destroyed, and so on.
|
destroyed, and so on. This is why all scopes must be created within another
|
||||||
|
scope, except `root()` which is used to create the initial scope that you can
|
||||||
|
manually destroy.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local root = vide.root
|
local root = vide.root
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local function setup()
|
local function setup()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
print(count())
|
print(count())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
setup() -- error, effect() tried to create a reactive scope with no stable scope
|
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
|
||||||
|
|
||||||
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
local count = root(setup) -- ok since effect() was called within a stable scope
|
||||||
|
count(1) -- prints "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
The scope created by `root()` can be destroyed.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function setup()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
print(count())
|
||||||
|
end)
|
||||||
|
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local destroy, count = root(setup)
|
||||||
|
|
||||||
count(1) -- prints "1"
|
count(1) -- prints "1"
|
||||||
count(2) -- prints "2"
|
|
||||||
|
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
|
count(2) -- effect is destroyed; no longer prints
|
||||||
```
|
```
|
||||||
|
|
||||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||||
|
|
@ -57,12 +73,12 @@ The reactive graph for the above example looks like so:
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#111720",
|
"primaryColor": "#1B1B1F",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#111720",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#0d131b",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#0d131b"
|
"tertiaryBorderColor": "#161618"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -74,7 +90,7 @@ subgraph root
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
When the stable `root()` scope is destroyed, the reactive `effect()`
|
When the stable `root()` is destroyed, the reactive `effect()`
|
||||||
scope will also be destroyed since it was created within it.
|
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
|
This is important because you may have an effect that updates the property of a
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
# Reactive Components
|
# Stateful Components
|
||||||
|
|
||||||
Reactive components in Vide are created using sources and effects - sources to
|
Stateful components in Vide are created using sources and effects - sources to
|
||||||
store the data, and effects to display the data.
|
store the data, and effects to display the data.
|
||||||
|
|
||||||
```luau
|
## Internal State
|
||||||
|
|
||||||
|
```lua
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
@ -31,13 +33,19 @@ its internal count, and automatically update its text to reflect that count.
|
||||||
Each instance of `Counter()` will maintain its own independent count, since the
|
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 component.
|
||||||
|
|
||||||
|
## External State
|
||||||
|
|
||||||
External sources can also be passed into components for them to use.
|
External sources can also be passed into components for them to use.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local function CountDisplay(props: { count: () -> number })
|
local function Counter(props: { count: () -> number })
|
||||||
local count = props.count
|
local count = props.count
|
||||||
|
|
||||||
local instance = create "TextLabel" {}
|
local instance = create "TextButton" {
|
||||||
|
Activated = function()
|
||||||
|
count(count() + 1)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
instance.Text = "count: " .. count()
|
instance.Text = "count: " .. count()
|
||||||
|
|
@ -48,11 +56,11 @@ end
|
||||||
|
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
CountDisplay {
|
Counter {
|
||||||
count = count
|
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
|
Sources can be created internally or passed in from externally, there are no
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
Explicitly creating effects to update properties is tedious. You can
|
Explicitly creating effects to update properties is tedious. You can
|
||||||
*implicitly* create an effect to update properties instead.
|
*implicitly* create an effect to update properties instead.
|
||||||
|
|
||||||
::: code-group
|
```lua
|
||||||
|
|
||||||
```luau [Implicit Effect]
|
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
||||||
|
|
@ -24,30 +22,6 @@ local function Counter()
|
||||||
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.
|
This example is equivalent to the example seen on the previous page.
|
||||||
|
|
||||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||||
|
|
@ -60,7 +34,7 @@ with a number key instead of string key) can return an instance or an array of
|
||||||
instances. An effect is automatically created to unparent removed instances and
|
instances. An effect is automatically created to unparent removed instances and
|
||||||
parent new instances on source update.
|
parent new instances on source update.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local items = source {
|
local items = source {
|
||||||
create "TextLabel" { Text = "A" }
|
create "TextLabel" { Text = "A" }
|
||||||
}
|
}
|
||||||
|
|
@ -72,12 +46,12 @@ local function List(props: { children: () -> { Instance } })
|
||||||
}
|
}
|
||||||
end
|
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 {
|
items {
|
||||||
create "TextLabel" { Text = "B" },
|
create "TextLabel" { Text = "B" },
|
||||||
create "TextLabel" { Text = "C" }
|
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:
|
We have seen the basic way to derive a source:
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
local text = function()
|
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 same time, the function wrapping the source will needlessly rerun to convert
|
||||||
the count into a string for each effect using it.
|
the count into a string for each effect using it.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ To avoid this, you can use `derive()` to derive a new source instead. This will
|
||||||
run a function in a reactive scope only when a source used inside updated.
|
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.
|
Reading this derived source multiple times will just return a cached result.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
|
|
@ -57,8 +57,7 @@ effect(function() text() end)
|
||||||
count(1) -- prints "ran" x1
|
count(1) -- prints "ran" x1
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `derive()` creates a reactive scope, it must be called within a stable
|
`derive()` must also be called within a stable scope, just like `effect()`.
|
||||||
scope, just like `effect()`.
|
|
||||||
|
|
||||||
If the recalculated value is the same as the old value, the derived source will
|
If the recalculated value is the same as the old value, the derived source will
|
||||||
not rerun the effects using it.
|
not rerun the effects using it.
|
||||||
|
|
@ -69,12 +68,12 @@ The reactive graph for the above example:
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#111720",
|
"primaryColor": "#1B1B1F",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#111720",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#0d131b",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#0d131b"
|
"tertiaryBorderColor": "#161618"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -87,7 +86,7 @@ end
|
||||||
```
|
```
|
||||||
|
|
||||||
Deriving a source in this manner is similar to creating an effect to update
|
Deriving a source in this manner is similar to creating an effect to update
|
||||||
another source. You should avoid doing this using an effect however.
|
another source. You should never manually do this using an effect however.
|
||||||
Improper usage could accidently create infinite loops in the reactive graph.
|
Improper usage could accidently create infinite loops in the reactive graph.
|
||||||
Always favour deriving when you need one source to update based on another
|
Always favour deriving when you need one source to update based on another
|
||||||
source.
|
source.
|
||||||
|
|
|
||||||
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"
|
|
||||||
181
src/apply.luau
181
src/apply.luau
|
|
@ -1,20 +1,24 @@
|
||||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
if not game then script = require "test/relative-string" end
|
||||||
|
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||||
|
local Vector2 = game and Vector2 or require "test/mock".Vector2 :: never
|
||||||
|
local UDim2 = game and UDim2 or require "test/mock".UDim2 :: never
|
||||||
|
|
||||||
local flags = require "./flags"
|
local flags = require(script.Parent.flags)
|
||||||
local implicit_effect = require "./implicit_effect"
|
local throw = require(script.Parent.throw)
|
||||||
local _, is_action = require "./action"()
|
local bind = require(script.Parent.bind)
|
||||||
local graph = require "./graph"
|
local _, is_action = require(script.Parent.action)()
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
|
|
||||||
type Array<V> = { V }
|
type Array<V> = { V }
|
||||||
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
||||||
type Map<K, V> = { [K]: V }
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
type Cache = {
|
local free_caches: {
|
||||||
-- event listeners to connect after properties are set
|
-- event listeners to connect after properties are set
|
||||||
events: Array<
|
events: Map<
|
||||||
| string -- 1. event name
|
string, -- event name
|
||||||
| () -> () -- 2. listener
|
() -> () -- listener
|
||||||
>,
|
>,
|
||||||
|
|
||||||
-- actions to run after events are connected
|
-- actions to run after events are connected
|
||||||
|
|
@ -23,34 +27,30 @@ type Cache = {
|
||||||
Array<(Instance) -> ()> -- action callbacks
|
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
|
-- cache to detect duplicate property setting at same nesting depth
|
||||||
nested_debug: Map<
|
nested_debug: Map<
|
||||||
number, -- depth
|
number, -- depth
|
||||||
Map<string, true> -- set of property names
|
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
|
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||||
nested_stack: { {} | number }
|
nested_stack: { {} | number }
|
||||||
}
|
}?
|
||||||
|
|
||||||
local free_cache: Cache?
|
local function borrow_caches(): typeof(assert(free_caches))
|
||||||
|
if free_caches then
|
||||||
local function borrow_cache(): Cache
|
local caches = free_caches :: typeof(assert(free_caches))
|
||||||
if free_cache then
|
free_caches = nil
|
||||||
local cache = free_cache
|
return caches
|
||||||
free_cache = nil
|
|
||||||
return cache
|
|
||||||
else
|
else
|
||||||
return {
|
return {
|
||||||
events = {},
|
events = {},
|
||||||
actions = setmetatable({} :: any, { -- lazy init
|
actions = setmetatable({} :: any, { -- lazy init
|
||||||
__index = function(self, i) self[i] = {}; return self[i] end
|
__index = function(self, i) self[i] = {}; return self[i] end
|
||||||
}),
|
}),
|
||||||
parent = nil,
|
|
||||||
nested_debug = setmetatable({} :: any, {
|
nested_debug = setmetatable({} :: any, {
|
||||||
__index = function(self, i: number) self[i] = {}; return self[i] end
|
__index = function(self, i: number) self[i] = {}; return self[i] end
|
||||||
}),
|
}),
|
||||||
|
|
@ -59,61 +59,36 @@ local function borrow_cache(): Cache
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function return_cache(cache: Cache )
|
local function return_caches(caches: typeof(free_caches) )
|
||||||
free_cache = cache
|
free_caches = caches
|
||||||
end
|
end
|
||||||
|
|
||||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
-- map of datatype names to class default constructor for aggregate init
|
||||||
for property, value in properties do
|
local aggregates = {}
|
||||||
if type(property) == "string" then
|
for name, class in {
|
||||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
CFrame = CFrame,
|
||||||
if cache.nested_debug[depth][property] then
|
Color3 = Color3,
|
||||||
error(`duplicate property {property} at depth {depth}`, 0)
|
UDim = UDim,
|
||||||
end
|
UDim2 = UDim2,
|
||||||
cache.nested_debug[depth][property] = true
|
Vector2 = Vector2,
|
||||||
end
|
Vector3 = Vector3,
|
||||||
|
Rect = Rect
|
||||||
if property == "Parent" then
|
} :: Map<string, { [string]: any }> do
|
||||||
cache.parent = value
|
aggregates[name] = class.new
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- applies table of nested properties to an instance using full vide semantics
|
-- applies table of nested properties to an instance using full vide semantics
|
||||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||||
if not properties then
|
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
|
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 events = caches.events
|
||||||
local actions = caches.actions
|
local actions = caches.actions
|
||||||
local nested_debug = caches.nested_debug
|
local nested_debug = caches.nested_debug
|
||||||
|
|
@ -122,39 +97,79 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
-- process all properties
|
-- process all properties
|
||||||
local depth = 1
|
local depth = 1
|
||||||
repeat
|
repeat
|
||||||
process_properties(properties, instance, caches, depth)
|
for property, value in properties do
|
||||||
depth = table.remove(nested_stack) :: number
|
if property == "Parent" then continue end
|
||||||
properties = table.remove(nested_stack) :: {}
|
|
||||||
until not properties
|
|
||||||
|
|
||||||
for i = 1, #events, 2 do
|
if type(property) == "string" then
|
||||||
local event_name = events[i]
|
if strict then -- check for duplicate prop assignment at nesting depth
|
||||||
local event_listener = events[i + 1]
|
if nested_debug[depth][property] then
|
||||||
;(instance :: any)[event_name]:Connect(event_listener)
|
throw(`duplicate property {property} at depth {depth}`)
|
||||||
|
end
|
||||||
|
nested_debug[depth][property] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, queued in actions do
|
if type(value) == "table" then -- attempt aggregate init
|
||||||
for _, callback in queued do
|
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||||
|
if ctor == nil then
|
||||||
|
throw(`cannot aggregate type {typeof(value)} for property {property}`)
|
||||||
|
end
|
||||||
|
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||||
|
elseif type(value) == "function" then
|
||||||
|
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||||
|
events[property] = value :: () -> () -- add event to buffer
|
||||||
|
else
|
||||||
|
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||||
|
end
|
||||||
|
else
|
||||||
|
(instance :: any)[property] = value -- set property
|
||||||
|
end
|
||||||
|
elseif type(property) == "number" then
|
||||||
|
if type(value) == "function" then
|
||||||
|
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||||
|
elseif type(value) == "table" then
|
||||||
|
if is_action(value) then
|
||||||
|
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||||
|
else
|
||||||
|
table.insert(nested_stack, value :: {})
|
||||||
|
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||||
|
end
|
||||||
|
else
|
||||||
|
(value :: Instance).Parent = instance -- parent child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
depth = table.remove(nested_stack) :: number
|
||||||
|
properties = table.remove(nested_stack) :: {}
|
||||||
|
|
||||||
|
until not properties
|
||||||
|
|
||||||
|
for 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)
|
callback(instance)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local parent = caches.parent
|
-- finally set parent if any
|
||||||
if parent then
|
if parent then
|
||||||
if type(parent) == "function" then
|
if type(parent) == "function" then
|
||||||
implicit_effect.parent(instance, parent :: () -> Instance)
|
bind.parent(instance, parent :: () -> Instance)
|
||||||
else
|
else
|
||||||
instance.Parent = parent :: Instance
|
instance.Parent = parent :: Instance
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- clear caches
|
||||||
table.clear(events)
|
table.clear(events)
|
||||||
for _, queued in actions do table.clear(queued) end
|
for _, queued in next, actions do table.clear(queued) end
|
||||||
caches.parent = nil
|
if strict then table.clear(nested_debug) end
|
||||||
if flags.strict then table.clear(nested_debug) end
|
|
||||||
table.clear(nested_stack)
|
table.clear(nested_stack)
|
||||||
|
|
||||||
return_cache(caches)
|
return_caches(caches)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
local flags = require "./flags"
|
if not game then script = require "test/relative-string" end
|
||||||
local graph = require "./graph"
|
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
|
||||||
local function batch(setter: () -> ())
|
local function batch(setter: () -> ())
|
||||||
local already_batching = flags.batch
|
local already_batching = flags.batch
|
||||||
|
|
@ -10,14 +13,14 @@ local function batch(setter: () -> ())
|
||||||
from = graph.get_update_queue_length()
|
from = graph.get_update_queue_length()
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err: string? = xpcall(setter, debug.traceback)
|
local ok, err: string? = pcall(setter)
|
||||||
|
|
||||||
if not already_batching then
|
if not already_batching then
|
||||||
flags.batch = false
|
flags.batch = false
|
||||||
graph.flush_update_queue(from)
|
graph.flush_update_queue(from)
|
||||||
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
|
end
|
||||||
|
|
||||||
return batch
|
return batch
|
||||||
|
|
|
||||||
105
src/bind.luau
Normal file
105
src/bind.luau
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
|
||||||
|
function create_implicit_effect<T>(updater: (T) -> T, binding: T)
|
||||||
|
evaluate_node(create_node(assert_stable_scope(), updater, binding))
|
||||||
|
end
|
||||||
|
|
||||||
|
type PropertyBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
property: string,
|
||||||
|
source: () -> unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_property_effect(p: PropertyBinding)
|
||||||
|
(p.instance :: any)[p.property] = p.source()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
type ParentBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
parent: () -> Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_parent_effect(p: ParentBinding)
|
||||||
|
p.instance.Parent = p.parent()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
type ChildrenBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
cur_children_set: { [Instance]: true },
|
||||||
|
new_children_set: { [Instance]: true },
|
||||||
|
children: () -> Instance | { Instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArrayOrV<V> = V | { V }
|
||||||
|
local function update_children_effect(p: ChildrenBinding)
|
||||||
|
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||||
|
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||||
|
|
||||||
|
local new_children = p.children() -- all (and only) children that should be parented after this update
|
||||||
|
|
||||||
|
if type(new_children) ~= "table" then
|
||||||
|
new_children = { new_children }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function process_child(child: ArrayOrV<Instance>)
|
||||||
|
if type(child) == "table" then
|
||||||
|
for _, child in next, child do
|
||||||
|
process_child(child)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if new_child_set[child] then return end -- stops redundant reparenting
|
||||||
|
|
||||||
|
new_child_set[child] = true -- record child set from this update
|
||||||
|
if not cur_children_set[child] then
|
||||||
|
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||||
|
else
|
||||||
|
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
process_child(new_children)
|
||||||
|
|
||||||
|
for child in next, cur_children_set do
|
||||||
|
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||||
|
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
|
||||||
|
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
property = function(instance, property, source)
|
||||||
|
return create_implicit_effect(update_property_effect, {
|
||||||
|
instance = instance,
|
||||||
|
property = property,
|
||||||
|
source = source
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
parent = function(instance, parent)
|
||||||
|
return create_implicit_effect(update_parent_effect, {
|
||||||
|
instance = instance,
|
||||||
|
parent = parent
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
children = function(instance, children)
|
||||||
|
return create_implicit_effect(update_children_effect, {
|
||||||
|
instance = instance,
|
||||||
|
cur_children_set = {},
|
||||||
|
new_children_set = {},
|
||||||
|
children = children
|
||||||
|
})
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
@ -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"()
|
if not game then script = require "test/relative-string" end
|
||||||
local cleanup = require "./cleanup"
|
|
||||||
|
local action = require(script.Parent.action)()
|
||||||
|
local cleanup = require(script.Parent.cleanup)
|
||||||
|
|
||||||
local function changed<T>(property: string, callback: (T) -> ())
|
local function changed<T>(property: string, callback: (T) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,27 @@
|
||||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
if not game then script = require "test/relative-string" end
|
||||||
|
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||||
|
|
||||||
local graph = require "./graph"
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
local push_cleanup = graph.push_cleanup
|
local push_cleanup = graph.push_cleanup
|
||||||
|
|
||||||
local function helper(obj: any)
|
local function helper(obj: any)
|
||||||
return
|
return
|
||||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
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 typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||||
elseif obj.destroy then function() obj:destroy() end
|
elseif obj.destroy then function() obj:destroy() end
|
||||||
elseif obj.disconnect then function() obj:disconnect() end
|
elseif obj.disconnect then function() obj:disconnect() end
|
||||||
elseif obj.Destroy then function() obj:Destroy() end
|
elseif obj.Destroy then function() obj:Destroy() end
|
||||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||||
else error "cannot cleanup given object"
|
else throw("cannot cleanup given object")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanup(value: unknown)
|
local function cleanup(value: unknown)
|
||||||
local scope = get_scope()
|
local scope = get_scope()
|
||||||
|
|
||||||
if not scope then
|
if not scope then
|
||||||
error "cannot cleanup outside a stable or reactive scope"
|
throw "cannot cleanup outside a stable or reactive scope"
|
||||||
end; assert(scope)
|
end; assert(scope)
|
||||||
|
|
||||||
if type(value) == "function" then
|
if type(value) == "function" then
|
||||||
|
|
@ -35,7 +36,6 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
||||||
|
|
||||||
return cleanup ::
|
return cleanup ::
|
||||||
( (callback: () -> ()) -> () ) &
|
( (callback: () -> ()) -> () ) &
|
||||||
( (thread: thread) -> () ) &
|
|
||||||
( (instance: Destroyable) -> () ) &
|
( (instance: Destroyable) -> () ) &
|
||||||
( (connection: Disconnectable) -> () ) &
|
( (connection: Disconnectable) -> () ) &
|
||||||
( (instance: Instance) -> () ) &
|
( (instance: Instance) -> () ) &
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
|
|
@ -6,7 +9,7 @@ local push_scope = graph.push_scope
|
||||||
local pop_scope = graph.pop_scope
|
local pop_scope = graph.pop_scope
|
||||||
local set_context = graph.set_context
|
local set_context = graph.set_context
|
||||||
|
|
||||||
export type Context<T> = (() -> T) & (<U>(T, () -> U) -> U)
|
export type Context<T> = (() -> T) & ((T, () -> ()) -> ())
|
||||||
|
|
||||||
local nil_symbol = newproxy()
|
local nil_symbol = newproxy()
|
||||||
local count = 0
|
local count = 0
|
||||||
|
|
@ -18,7 +21,7 @@ local function context<T>(...: T): Context<T>
|
||||||
local has_default = select("#", ...) > 0
|
local has_default = select("#", ...) > 0
|
||||||
local default_value = ...
|
local default_value = ...
|
||||||
|
|
||||||
return function<T>(...): any -- todo: fix type error
|
return function(...)
|
||||||
local scope: Node<unknown>? | false = get_scope()
|
local scope: Node<unknown>? | false = get_scope()
|
||||||
|
|
||||||
if select("#", ...) == 0 then -- get
|
if select("#", ...) == 0 then -- get
|
||||||
|
|
@ -43,10 +46,10 @@ local function context<T>(...: T): Context<T>
|
||||||
if has_default ~= nil then
|
if has_default ~= nil then
|
||||||
return default_value
|
return default_value
|
||||||
else
|
else
|
||||||
error("attempt to get context when no context is set and no default context is set", 0)
|
throw("attempt to get context when no context is set and no default context is set")
|
||||||
end
|
end
|
||||||
else -- set
|
else -- set
|
||||||
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
if not scope then return throw("attempt to set context outside of a vide scope") end
|
||||||
|
|
||||||
local value, component = ...
|
local value, component = ...
|
||||||
|
|
||||||
|
|
@ -61,10 +64,8 @@ local function context<T>(...: T): Context<T>
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
error(`error while running context:\n\n{result}`, 0)
|
throw(`error while running context:\n\n{result}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil :: any
|
return nil :: any
|
||||||
|
|
|
||||||
159
src/create.luau
159
src/create.luau
|
|
@ -1,119 +1,84 @@
|
||||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
if not game then script = require "test/relative-string" end
|
||||||
local Instance = game and Instance or require "../test/mock".Instance :: never
|
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 throw = require(script.Parent.throw)
|
||||||
local apply = require "./apply"
|
local defaults = require(script.Parent.defaults)
|
||||||
local flags = require "./flags"
|
local apply = require(script.Parent.apply)
|
||||||
|
|
||||||
local function create_constructor_for_class(class: string): ({ [unknown]: unknown }) -> Instance
|
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||||
local function constructor(properties: { [unknown]: unknown }): Instance
|
|
||||||
|
setmetatable(ctor_cache :: any, {
|
||||||
|
__index = function(self, class)
|
||||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
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]
|
local default: { [string]: unknown }? = defaults[class]
|
||||||
if default then
|
if default then
|
||||||
for i, v in default do
|
for i, v in next, default do
|
||||||
(instance :: any)[i] = v
|
(instance :: any)[i] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function ctor(properties: Props): Instance
|
||||||
|
return apply(instance:Clone(), properties)
|
||||||
end
|
end
|
||||||
|
|
||||||
return apply(instance, properties)
|
self[class] = ctor
|
||||||
|
return ctor
|
||||||
end
|
end
|
||||||
|
})
|
||||||
|
|
||||||
return constructor
|
local function create_instance(class: string)
|
||||||
|
return ctor_cache[class]
|
||||||
end
|
end
|
||||||
|
|
||||||
local constructor_cache = {} :: { [string]: ({ [unknown]: unknown }) -> Instance }
|
local function clone_instance(instance: Instance)
|
||||||
|
return function(properties: Props): Instance
|
||||||
-- todo: remove support for different overloads
|
local clone = instance:Clone()
|
||||||
local function create(class_or_instance: string|Instance, properties: { [unknown]: unknown }?): unknown
|
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||||
if type(class_or_instance) ~= "string" and typeof(class_or_instance) ~= "Instance" then
|
return apply(clone, properties)
|
||||||
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local constructor: ({ [unknown]: unknown }) -> Instance
|
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
||||||
if type(class_or_instance) == "string" then
|
if type(class_or_instance) == "string" then
|
||||||
constructor = constructor_cache[class_or_instance]
|
return create_instance(class_or_instance)
|
||||||
if not constructor then
|
elseif typeof(class_or_instance) == "Instance" then
|
||||||
constructor = create_constructor_for_class(class_or_instance)
|
return clone_instance(class_or_instance)
|
||||||
constructor_cache[class_or_instance] = constructor
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
constructor = function(props)
|
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||||
local clone = assert(class_or_instance:Clone(), "attempt to clone a non-archivable instance")
|
return nil :: never
|
||||||
return apply(clone, props)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
return if properties
|
|
||||||
then constructor(properties)
|
|
||||||
else constructor
|
|
||||||
end
|
end
|
||||||
|
|
||||||
export type Instances = {
|
type Props = { [any]: any }
|
||||||
Folder: Folder,
|
return (create :: any) ::
|
||||||
BillboardGui: BillboardGui,
|
( <T>(T & Instance) -> (Props) -> T ) &
|
||||||
CanvasGroup: CanvasGroup,
|
( ("Folder") -> (Props) -> Folder ) &
|
||||||
Frame: Frame,
|
( ("BillboardGui") -> (Props) -> BillboardGui ) &
|
||||||
ImageButton: ImageButton,
|
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
|
||||||
ImageLabel: ImageLabel,
|
( ("Frame") -> (Props) -> Frame ) &
|
||||||
ScreenGui: ScreenGui,
|
( ("ImageButton") -> (Props) -> ImageButton ) &
|
||||||
ScrollingFrame: ScrollingFrame,
|
( ("ImageLabel") -> (Props) -> ImageLabel ) &
|
||||||
SurfaceGui: SurfaceGui,
|
( ("ScreenGui") -> (Props) -> ScreenGui ) &
|
||||||
TextBox: TextBox,
|
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
|
||||||
TextButton: TextButton,
|
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
|
||||||
TextLabel: TextLabel,
|
( ("TextBox") -> (Props) -> TextBox ) &
|
||||||
UIAspectRatioConstraint: UIAspectRatioConstraint,
|
( ("TextButton") -> (Props) -> TextButton ) &
|
||||||
UICorner: UICorner,
|
( ("TextLabel") -> (Props) -> TextLabel ) &
|
||||||
UIGradient: UIGradient,
|
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
|
||||||
UIGridLayout: UIGridLayout,
|
( ("UICorner") -> (Props) -> UICorner ) &
|
||||||
UIListLayout: UIListLayout,
|
( ("UIGradient") -> (Props) -> UIGradient ) &
|
||||||
UISizeConstraint: UISizeConstraint,
|
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
|
||||||
UITextSizeConstraint: UITextSizeConstraint,
|
( ("UIListLayout") -> (Props) -> UIListLayout ) &
|
||||||
UIScale: UIScale,
|
( ("UIPadding") -> (Props) -> UIPadding ) &
|
||||||
UIPadding: UIPadding,
|
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
|
||||||
UIStroke: UIStroke,
|
( ("UIScale") -> (Props) -> UIScale ) &
|
||||||
UIFlexItem: UIFlexItem,
|
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
|
||||||
UIPageLayout: UIPageLayout,
|
( ("UIStroke") -> (Props) -> UIStroke ) &
|
||||||
UITableLayout: UITableLayout,
|
( ("UITableLayout") -> (Props) -> UITableLayout ) &
|
||||||
VideoFrame: VideoFrame,
|
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
|
||||||
ViewportFrame: ViewportFrame,
|
( ("VideoFrame") -> (Props) -> VideoFrame ) &
|
||||||
ProximityPrompt: ProximityPrompt,
|
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
|
||||||
UIDragDetector: UIDragDetector,
|
( (string) -> (Props) -> Instance )
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
local Enum = game and Enum or require "../test/mock".Enum :: never
|
local Enum = game and Enum or require "test/mock".Enum :: never
|
||||||
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
|
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
||||||
|
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Part = {
|
Part = {
|
||||||
Material = Enum.Material.SmoothPlastic,
|
Material = Enum.Material.SmoothPlastic,
|
||||||
Size = vector.create(1, 1, 1),
|
Size = Vector3.new(1, 1, 1),
|
||||||
Anchored = true
|
Anchored = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -110,4 +111,4 @@ return {
|
||||||
BorderColor3 = Color3.new(0, 0, 0),
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
BorderSizePixel = 0
|
BorderSizePixel = 0
|
||||||
}
|
}
|
||||||
} :: { [string]: { [string]: unknown} }
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
|
|
||||||
|
|
@ -10,7 +12,7 @@ local function derive<T>(source: () -> T): () -> T
|
||||||
evaluate_node(node)
|
evaluate_node(node)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
push_scope_as_child_of(node)
|
push_child_to_scope(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end
|
end
|
||||||
end
|
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)
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,4 @@ end
|
||||||
|
|
||||||
local is_O2 = inline_test() ~= "inline_test"
|
local is_O2 = inline_test() ~= "inline_test"
|
||||||
|
|
||||||
return {
|
return { strict = not is_O2, batch = false }
|
||||||
strict = not is_O2,
|
|
||||||
defaults = true,
|
|
||||||
defer_nested_properties = true,
|
|
||||||
batch = false,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
local flags = require "./flags"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
|
||||||
export type SourceNode<T> = {
|
export type SourceNode<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
|
|
@ -21,23 +24,9 @@ export type Node<T> = {
|
||||||
|
|
||||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||||
|
|
||||||
local function efn(err: string)
|
|
||||||
local trace = debug.traceback(err, 2)
|
|
||||||
|
|
||||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
|
||||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
|
||||||
return "\n "
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
trace ..= "\nsource update stacktrace:"
|
|
||||||
return trace
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||||
|
|
||||||
local thread = coroutine.create(xpcall)
|
local thread = coroutine.create(xpcall)
|
||||||
--local function efn(err: string) return debug.traceback(err, 3) end
|
local function efn(err: string) return debug.traceback(err, 3) end
|
||||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||||
|
|
||||||
assert(resume_ok)
|
assert(resume_ok)
|
||||||
|
|
@ -58,9 +47,9 @@ local function assert_stable_scope(): Node<unknown>
|
||||||
|
|
||||||
if not scope then
|
if not scope then
|
||||||
local caller_name = debug.info(2, "n")
|
local caller_name = debug.info(2, "n")
|
||||||
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
|
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
|
||||||
elseif scope.effect then
|
elseif scope.effect then
|
||||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
throw("cannot create a new reactive scope inside another reactive scope")
|
||||||
end
|
end
|
||||||
|
|
||||||
return scope
|
return scope
|
||||||
|
|
@ -93,9 +82,9 @@ end
|
||||||
|
|
||||||
local function flush_cleanups<T>(node: Node<T>)
|
local function flush_cleanups<T>(node: Node<T>)
|
||||||
if node.cleanups then
|
if node.cleanups then
|
||||||
for _, fn in node.cleanups do
|
for _, fn in next, node.cleanups do
|
||||||
local ok, err: string? = xpcall(fn, debug.traceback)
|
local ok, err: string? = pcall(fn)
|
||||||
if not ok then error(`cleanup error: {err}`, 0) end
|
if not ok then throw(`cleanup error: {err}`) end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.clear(node.cleanups)
|
table.clear(node.cleanups)
|
||||||
|
|
@ -119,10 +108,6 @@ local function unparent<T>(node: Node<T>)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function destroy<T>(node: Node<T>)
|
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)
|
flush_cleanups(node)
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
|
||||||
|
|
@ -148,10 +133,6 @@ local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
||||||
|
|
||||||
local function evaluate_node<T>(node: Node<T>)
|
local function evaluate_node<T>(node: Node<T>)
|
||||||
if flags.strict then
|
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
|
local initial_value = node.cache
|
||||||
|
|
||||||
for i = 1, 2 do
|
for i = 1, 2 do
|
||||||
|
|
@ -167,7 +148,7 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
table.clear(update_queue)
|
||||||
update_queue.n = 0
|
update_queue.n = 0
|
||||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
throw(`effect stacktrace:\n{new_value :: string}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value :: T
|
node.cache = new_value :: T
|
||||||
|
|
@ -187,7 +168,7 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
table.clear(update_queue)
|
||||||
update_queue.n = 0
|
update_queue.n = 0
|
||||||
error(`effect error:\n{new_value}\n`, 0)
|
throw(`effect stacktrace:\n{new_value}\n`)
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value
|
node.cache = new_value
|
||||||
|
|
@ -249,7 +230,7 @@ local function update_descendants<T>(root: SourceNode<T>)
|
||||||
update_queue.n = n0
|
update_queue.n = n0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_scope_as_child_of<T>(node: SourceNode<T>)
|
local function push_child_to_scope<T>(node: SourceNode<T>)
|
||||||
local scope = get_scope()
|
local scope = get_scope()
|
||||||
if scope and scope.effect then -- do not track nodes with no effect
|
if scope and scope.effect then -- do not track nodes with no effect
|
||||||
push_child(node, scope)
|
push_child(node, scope)
|
||||||
|
|
@ -306,7 +287,7 @@ return table.freeze {
|
||||||
push_cleanup = push_cleanup,
|
push_cleanup = push_cleanup,
|
||||||
destroy = destroy,
|
destroy = destroy,
|
||||||
flush_cleanups = flush_cleanups,
|
flush_cleanups = flush_cleanups,
|
||||||
push_scope_as_child_of = push_scope_as_child_of,
|
push_child_to_scope = push_child_to_scope,
|
||||||
update_descendants = update_descendants,
|
update_descendants = update_descendants,
|
||||||
push_child = push_child,
|
push_child = push_child,
|
||||||
create_node = create_node,
|
create_node = create_node,
|
||||||
|
|
@ -315,7 +296,5 @@ return table.freeze {
|
||||||
flush_update_queue = flush_update_queue,
|
flush_update_queue = flush_update_queue,
|
||||||
get_update_queue_length = get_update_queue_length,
|
get_update_queue_length = get_update_queue_length,
|
||||||
set_context = set_context,
|
set_context = set_context,
|
||||||
scopes = scopes,
|
scopes = scopes
|
||||||
|
|
||||||
q = update_queue
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
122
src/init.luau
122
src/init.luau
|
|
@ -1,12 +1,118 @@
|
||||||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
--------------------------------------------------------------------------------
|
||||||
|
-- vide.luau
|
||||||
|
-- v0.3.0
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
local vide = require("@self/lib")
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
export type source<T> = vide.source<T>
|
local root = require(script.root)
|
||||||
export type Source<T> = vide.Source<T>
|
local mount = require(script.mount)
|
||||||
export type context<T> = vide.context<T>
|
local create = require(script.create)
|
||||||
export type Context<T> = vide.Context<T>
|
local apply = require(script.apply)
|
||||||
export type Instances = vide.Instances
|
local source = require(script.source)
|
||||||
export type Properties<T> = vide.Properties<T>
|
local effect = require(script.effect)
|
||||||
|
local derive = require(script.derive)
|
||||||
|
local cleanup = require(script.cleanup)
|
||||||
|
local untrack = require(script.untrack)
|
||||||
|
local read = require(script.read)
|
||||||
|
local batch = require(script.batch)
|
||||||
|
local context = require(script.context)
|
||||||
|
local switch = require(script.switch)
|
||||||
|
local show = require(script.show)
|
||||||
|
local indexes, values = require(script.maps)()
|
||||||
|
local spring, update_springs = require(script.spring)()
|
||||||
|
local action = require(script.action)()
|
||||||
|
local changed = require(script.changed)
|
||||||
|
local throw = require(script.throw)
|
||||||
|
local flags = require(script.flags)
|
||||||
|
|
||||||
|
export type Source<T> = source.Source<T>
|
||||||
|
export type source<T> = Source<T>
|
||||||
|
export type Context<T> = context.Context<T>
|
||||||
|
export type context<T> = Context<T>
|
||||||
|
|
||||||
|
local function step(dt: number)
|
||||||
|
if game then
|
||||||
|
debug.profilebegin("VIDE STEP")
|
||||||
|
debug.profilebegin("VIDE SPRING")
|
||||||
|
end
|
||||||
|
|
||||||
|
update_springs(dt)
|
||||||
|
|
||||||
|
if game then
|
||||||
|
debug.profileend()
|
||||||
|
debug.profileend()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||||
|
task.defer(step, dt)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local vide = {
|
||||||
|
-- core
|
||||||
|
root = root,
|
||||||
|
mount = mount,
|
||||||
|
create = create,
|
||||||
|
source = source,
|
||||||
|
effect = effect,
|
||||||
|
derive = derive,
|
||||||
|
switch = switch,
|
||||||
|
show = show,
|
||||||
|
indexes = indexes,
|
||||||
|
values = values,
|
||||||
|
|
||||||
|
-- util
|
||||||
|
cleanup = cleanup,
|
||||||
|
untrack = untrack,
|
||||||
|
read = read,
|
||||||
|
batch = batch,
|
||||||
|
context = context,
|
||||||
|
|
||||||
|
-- animations
|
||||||
|
spring = spring,
|
||||||
|
|
||||||
|
-- actions
|
||||||
|
action = action,
|
||||||
|
changed = changed,
|
||||||
|
|
||||||
|
-- flags
|
||||||
|
strict = (nil :: any) :: boolean,
|
||||||
|
|
||||||
|
-- temporary
|
||||||
|
apply = function(instance: Instance)
|
||||||
|
return function(props: { [any]: any })
|
||||||
|
apply(instance, props)
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- runtime
|
||||||
|
step = function(dt: number)
|
||||||
|
if stepped then
|
||||||
|
stepped:Disconnect()
|
||||||
|
stepped = nil
|
||||||
|
end
|
||||||
|
step(dt)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(vide :: any, {
|
||||||
|
__index = function(_, index: unknown): ()
|
||||||
|
if index == "strict" then
|
||||||
|
return flags.strict
|
||||||
|
else
|
||||||
|
throw(`{tostring(index)} is not a valid member of vide`)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
__newindex = function(_, index: unknown, value: unknown)
|
||||||
|
if index == "strict" then
|
||||||
|
flags.strict = value :: boolean
|
||||||
|
else
|
||||||
|
throw(`{tostring(index)} is not a valid member of vide`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
return vide
|
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
|
|
||||||
219
src/maps.luau
Normal file
219
src/maps.luau
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local create_source_node = graph.create_source_node
|
||||||
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
|
local update_descendants = graph.update_descendants
|
||||||
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
|
local push_scope = graph.push_scope
|
||||||
|
local pop_scope = graph.pop_scope
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local destroy = graph.destroy
|
||||||
|
|
||||||
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
|
local function check_primitives(t: {})
|
||||||
|
if not flags.strict then return end
|
||||||
|
|
||||||
|
for _, v in next, t do
|
||||||
|
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||||
|
throw("table source map cannot return primitives")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||||
|
local owner = assert_stable_scope()
|
||||||
|
local subowner = create_node(owner, false, false)
|
||||||
|
|
||||||
|
local input_cache = {} :: Map<K, VI>
|
||||||
|
local output_cache = {} :: Map<K, VO>
|
||||||
|
local input_nodes = {} :: Map<K, SourceNode<VI>>
|
||||||
|
local remove_queue = {} :: { K }
|
||||||
|
local scopes = {} :: Map<K, Node<unknown>>
|
||||||
|
|
||||||
|
local function update_children(data)
|
||||||
|
-- queue removed values
|
||||||
|
for i in next, input_cache do
|
||||||
|
if data[i] == nil then
|
||||||
|
table.insert(remove_queue, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- remove queued values
|
||||||
|
for _, i in next, remove_queue do
|
||||||
|
destroy(scopes[i])
|
||||||
|
|
||||||
|
input_cache[i] = nil
|
||||||
|
output_cache[i] = nil
|
||||||
|
input_nodes[i] = nil
|
||||||
|
scopes[i] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(remove_queue)
|
||||||
|
|
||||||
|
push_scope(subowner)
|
||||||
|
|
||||||
|
-- process new or changed values
|
||||||
|
for i, v in next, data do
|
||||||
|
local cv = input_cache[i]
|
||||||
|
|
||||||
|
if cv ~= v then
|
||||||
|
if cv == nil then -- create new scope and run transform
|
||||||
|
local scope = create_node(subowner, false, false)
|
||||||
|
scopes[i] = scope :: Node<any>
|
||||||
|
|
||||||
|
local node = create_source_node(v)
|
||||||
|
|
||||||
|
push_scope(scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(transform, function()
|
||||||
|
push_child_to_scope(node)
|
||||||
|
return node.cache
|
||||||
|
end, i)
|
||||||
|
|
||||||
|
pop_scope()
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
pop_scope() -- subowner scope
|
||||||
|
error(result, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
input_nodes[i] = node
|
||||||
|
output_cache[i] = result
|
||||||
|
else -- update source
|
||||||
|
input_nodes[i].cache = v
|
||||||
|
update_descendants(input_nodes[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
input_cache[i] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pop_scope()
|
||||||
|
|
||||||
|
local output_array = table.create(#scopes)
|
||||||
|
for _, v in next, output_cache do
|
||||||
|
table.insert(output_array, v)
|
||||||
|
end
|
||||||
|
check_primitives(output_array)
|
||||||
|
|
||||||
|
return output_array
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(owner, function()
|
||||||
|
return update_children(input())
|
||||||
|
end, false :: any)
|
||||||
|
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
push_child_to_scope(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||||
|
local owner = assert_stable_scope()
|
||||||
|
local subowner = create_node(owner, false, false)
|
||||||
|
|
||||||
|
local cur_input_cache_up = {} :: Map<VI, K>
|
||||||
|
local new_input_cache_up = {} :: Map<VI, K>
|
||||||
|
local output_cache = {} :: Map<VI, VO>
|
||||||
|
local input_nodes = {} :: Map<VI, SourceNode<K>>
|
||||||
|
local scopes = {} :: Map<VI, Node<unknown>>
|
||||||
|
|
||||||
|
local function update_children(data: Map<K, VI>)
|
||||||
|
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
|
||||||
|
|
||||||
|
if flags.strict then
|
||||||
|
local cache = {}
|
||||||
|
for _, v in next, data do
|
||||||
|
if cache[v] ~= nil then
|
||||||
|
throw "duplicate table value detected"
|
||||||
|
end
|
||||||
|
cache[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
push_scope(subowner)
|
||||||
|
|
||||||
|
-- process data
|
||||||
|
for i, v in next, data do
|
||||||
|
new_input_cache[v] = i
|
||||||
|
|
||||||
|
local cv = cur_input_cache[v]
|
||||||
|
|
||||||
|
if cv == nil then -- create new scope and run transform
|
||||||
|
local scope = create_node(subowner, false, false)
|
||||||
|
scopes[v] = scope :: Node<any>
|
||||||
|
|
||||||
|
local node = create_source_node(i)
|
||||||
|
|
||||||
|
push_scope(scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(transform, v, function()
|
||||||
|
push_child_to_scope(node)
|
||||||
|
return node.cache
|
||||||
|
end)
|
||||||
|
|
||||||
|
pop_scope()
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
pop_scope() -- subowner scope
|
||||||
|
error(result, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
input_nodes[v] = node
|
||||||
|
output_cache[v] = result
|
||||||
|
else -- update source
|
||||||
|
if cv ~= i then
|
||||||
|
input_nodes[v].cache = i
|
||||||
|
update_descendants(input_nodes[v])
|
||||||
|
end
|
||||||
|
|
||||||
|
cur_input_cache[v] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pop_scope()
|
||||||
|
|
||||||
|
-- remove old values
|
||||||
|
for v in next, cur_input_cache do
|
||||||
|
destroy(scopes[v])
|
||||||
|
|
||||||
|
output_cache[v] = nil
|
||||||
|
input_nodes[v] = nil
|
||||||
|
scopes[v] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update buffer cache
|
||||||
|
table.clear(cur_input_cache)
|
||||||
|
cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache
|
||||||
|
|
||||||
|
local output_array = table.create(#scopes)
|
||||||
|
for _, v in next, output_cache do
|
||||||
|
table.insert(output_array, v)
|
||||||
|
end
|
||||||
|
check_primitives(output_array)
|
||||||
|
|
||||||
|
return output_array
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(owner, function()
|
||||||
|
return update_children(input())
|
||||||
|
end, false :: any)
|
||||||
|
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
push_child_to_scope(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return function() return indexes, values end
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
local root = require "./root"
|
if not game then script = require "test/relative-string" end
|
||||||
local apply = require "./apply"
|
|
||||||
|
local root = require(script.Parent.root)
|
||||||
|
local apply = require(script.Parent.apply)
|
||||||
|
|
||||||
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||||
return root(function()
|
return root(function()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
local function read<T>(value: T | () -> T): T
|
local function read<T>(value: T | () -> T): T
|
||||||
return if type(value) == "function" then value() else value
|
return if type(value) == "function" then value() else value
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local push_scope = graph.push_scope
|
local push_scope = graph.push_scope
|
||||||
|
|
@ -13,20 +16,21 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
||||||
refs[node] = true -- prevent gc of root node
|
refs[node] = true -- prevent gc of root node
|
||||||
|
|
||||||
local destroy = function()
|
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
|
refs[node] = nil
|
||||||
destroy(node)
|
destroy(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
push_scope(node)
|
push_scope(node)
|
||||||
|
|
||||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
local function efn(err: string) return debug.traceback(err, 3) end
|
||||||
|
local result = { xpcall(fn, efn, destroy) }
|
||||||
|
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not result[1] then
|
if not result[1] then
|
||||||
destroy()
|
destroy()
|
||||||
error(`error while running root():\n\n{result[2]}`, 0)
|
throw(`error while running root():\n\n{result[2]}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return destroy, unpack(result :: any, 2)
|
return destroy, unpack(result :: any, 2)
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,18 @@
|
||||||
local source = require "./source"
|
if not game then script = require "test/relative-string" end
|
||||||
local derive = require "./derive"
|
|
||||||
local effect = require "./effect"
|
|
||||||
local untrack = require "./untrack"
|
|
||||||
local switch = require "./switch"
|
|
||||||
|
|
||||||
type Array<T> = { T }
|
local switch = require(script.Parent.switch)
|
||||||
type Source<T> = () -> T
|
|
||||||
|
|
||||||
local function show<T, Obj>(
|
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||||
input: Source<T?>,
|
local function truthy()
|
||||||
component: (Source<T>, Source<boolean>) -> (Obj, ...number),
|
return not not source()
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
local input_is_truthy = derive(function()
|
return switch(truthy) {
|
||||||
return not not input()
|
[true] = component,
|
||||||
end)
|
[false] = fallback,
|
||||||
|
|
||||||
return switch(input_is_truthy) {
|
|
||||||
[true] = function(present)
|
|
||||||
return component(filtered_input, present)
|
|
||||||
end,
|
|
||||||
[false] = fallback
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return show
|
return show ::
|
||||||
|
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
|
||||||
|
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_source_node = graph.create_source_node
|
local create_source_node = graph.create_source_node
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
local update_descendants = graph.update_descendants
|
local update_descendants = graph.update_descendants
|
||||||
|
|
||||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||||
|
|
@ -9,9 +11,9 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||||
local function source<T>(initial_value: T): Source<T>
|
local function source<T>(initial_value: T): Source<T>
|
||||||
local node = create_source_node(initial_value)
|
local node = create_source_node(initial_value)
|
||||||
|
|
||||||
local function update_source(...): T
|
return function(...): T
|
||||||
if select("#", ...) == 0 then -- no args were given
|
if select("#", ...) == 0 then -- no args were given
|
||||||
push_scope_as_child_of(node)
|
push_child_to_scope(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -24,8 +26,6 @@ local function source<T>(initial_value: T): Source<T>
|
||||||
update_descendants(node)
|
update_descendants(node)
|
||||||
return v
|
return v
|
||||||
end
|
end
|
||||||
|
|
||||||
return update_source
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||||
|
|
|
||||||
257
src/spring.luau
257
src/spring.luau
|
|
@ -1,4 +1,28 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||||
|
|
||||||
|
--[[
|
||||||
|
|
||||||
|
Supported datatypes:
|
||||||
|
- number
|
||||||
|
- CFrame
|
||||||
|
- Color3
|
||||||
|
- UDim
|
||||||
|
- UDim2
|
||||||
|
- Vector2
|
||||||
|
- Vector3
|
||||||
|
- Rect
|
||||||
|
|
||||||
|
Unsupported datatypes:
|
||||||
|
- bool
|
||||||
|
- Vector2int16
|
||||||
|
- Vector3int16
|
||||||
|
- EnumItem
|
||||||
|
|
||||||
|
]]
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
type SourceNode<T> = graph.SourceNode<T>
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
|
|
@ -6,79 +30,74 @@ local create_source_node = graph.create_source_node
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
local update_descendants = graph.update_descendants
|
local update_descendants = graph.update_descendants
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
|
|
||||||
local UPDATE_RATE = 120
|
local UPDATE_RATE = 120
|
||||||
local TOLERANCE_FACTOR = 10_000
|
local TOLERANCE = 0.0001
|
||||||
|
|
||||||
|
type Vec3 = Vector3
|
||||||
|
|
||||||
|
local function Vec3(x: number?, y: number?, z: number?)
|
||||||
|
return Vector3.new(x, y, z)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ZERO = Vec3(0, 0, 0)
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||||
|
|
||||||
--[[
|
type SpringData<T> = {
|
||||||
Unsupported datatypes:
|
|
||||||
- bool
|
|
||||||
- Vector2int16
|
|
||||||
- Vector3int16
|
|
||||||
- EnumItem
|
|
||||||
]]
|
|
||||||
|
|
||||||
type SpringState<T> = {
|
|
||||||
k: number, -- spring constant
|
k: number, -- spring constant
|
||||||
c: number, -- damping coeff
|
c: number, -- damping coeff
|
||||||
|
|
||||||
x0_123: vector, x0_456: vector, -- initial position
|
-- dimensions 1-3
|
||||||
x_123: vector, x_456: vector, -- current position
|
x0_123: Vec3,
|
||||||
x1_123: vector, x1_456: vector, -- target position
|
x1_123: Vec3,
|
||||||
v_123: vector, v_456: vector, -- current velocity
|
v_123: Vec3,
|
||||||
|
|
||||||
|
-- dimensions 4-6
|
||||||
|
x0_456: Vec3,
|
||||||
|
x1_456: Vec3,
|
||||||
|
v_456: Vec3,
|
||||||
|
|
||||||
source_value: T -- current value of spring input source
|
source_value: T -- current value of spring input source
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpringSettings<T> = ({
|
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
||||||
position: T?,
|
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
||||||
velocity: T?,
|
|
||||||
impulse: T?
|
|
||||||
}) -> ()
|
|
||||||
|
|
||||||
type TypeToVec6<T> = (T) -> (vector, vector)
|
|
||||||
type Vec6ToType<T> = (vector, vector) -> T
|
|
||||||
|
|
||||||
local type_to_vec6 = {
|
local type_to_vec6 = {
|
||||||
number = function(v)
|
number = function(v)
|
||||||
return vector.create(v, 0, 0), vector.zero
|
return Vec3(v, 0, 0), ZERO
|
||||||
end :: TypeToVec6<number>,
|
end :: TypeToVec6<number>,
|
||||||
|
|
||||||
CFrame = function(v)
|
CFrame = function(v)
|
||||||
return v.Position, vector.create(v:ToEulerAnglesXYZ())
|
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
||||||
end :: TypeToVec6<CFrame>,
|
end :: TypeToVec6<CFrame>,
|
||||||
|
|
||||||
Color3 = function(v)
|
Color3 = function(v)
|
||||||
-- todo: hsv, oklab?
|
-- 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>,
|
end :: TypeToVec6<Color3>,
|
||||||
|
|
||||||
UDim = function(v)
|
UDim = function(v)
|
||||||
return vector.create(v.Scale, v.Offset, 0), vector.zero
|
return Vec3(v.Scale, v.Offset, 0), ZERO
|
||||||
end :: TypeToVec6<UDim>,
|
end :: TypeToVec6<UDim>,
|
||||||
|
|
||||||
UDim2 = function(v)
|
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>,
|
end :: TypeToVec6<UDim2>,
|
||||||
|
|
||||||
Vector2 = function(v)
|
Vector2 = function(v)
|
||||||
return vector.create(v.X, v.Y, 0), vector.zero
|
return Vec3(v.X, v.Y, 0), ZERO
|
||||||
end :: TypeToVec6<Vector2>,
|
end :: TypeToVec6<Vector2>,
|
||||||
|
|
||||||
Vector3 = function(v)
|
Vector3 = function(v)
|
||||||
return v, vector.zero
|
return v, ZERO
|
||||||
end :: TypeToVec6<Vector3>,
|
end :: TypeToVec6<Vector3>,
|
||||||
|
|
||||||
Rect = function(v)
|
Rect = function(v)
|
||||||
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
|
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
|
||||||
end :: TypeToVec6<Rect>,
|
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 }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local vec6_to_type = {
|
local vec6_to_type = {
|
||||||
|
|
@ -112,16 +131,12 @@ local vec6_to_type = {
|
||||||
|
|
||||||
Rect = function(a, b)
|
Rect = function(a, b)
|
||||||
return Rect.new(a.X, a.Y, a.Z, b.X)
|
return Rect.new(a.X, a.Y, a.Z, b.X)
|
||||||
end :: Vec6ToType<Rect>,
|
end :: Vec6ToType<Rect>
|
||||||
|
|
||||||
table = function(a, b)
|
|
||||||
return { a.X, a.Y, a.Z, b.X }
|
|
||||||
end :: Vec6ToType<{ number }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local invalid_type = {
|
local invalid_type = {
|
||||||
__index = function(_, t: string)
|
__index = function(_, t: string)
|
||||||
error(`cannot spring type {t}`, 0)
|
throw(`cannot spring type {t}`)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,15 +145,17 @@ setmetatable(vec6_to_type, invalid_type)
|
||||||
|
|
||||||
-- maps spring data to its corresponding output node
|
-- maps spring data to its corresponding output node
|
||||||
-- lifetime of spring data is tied to output node
|
-- lifetime of spring data is tied to output node
|
||||||
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
|
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
||||||
setmetatable(springs :: any, { __mode = "v" })
|
setmetatable(springs, { __mode = "v" })
|
||||||
|
|
||||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
|
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||||
local owner = assert_stable_scope()
|
local owner = assert_stable_scope()
|
||||||
|
|
||||||
-- https://en.wikipedia.org/wiki/Damping
|
-- https://en.wikipedia.org/wiki/Damping
|
||||||
|
|
||||||
local w_n = 2*math.pi / (period or 1)
|
local w_n = 2*math.pi / (period or 1)
|
||||||
local z = damping_ratio or 1
|
local z = damping_ratio or 1
|
||||||
|
|
||||||
local k = w_n^2
|
local k = w_n^2
|
||||||
local c_c = 2*w_n
|
local c_c = 2*w_n
|
||||||
local c = z * c_c
|
local c = z * c_c
|
||||||
|
|
@ -146,22 +163,20 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
-- todo: is there a solution other than reducing step size?
|
-- todo: is there a solution other than reducing step size?
|
||||||
-- todo: this does not catch all solver exploding cases
|
-- todo: this does not catch all solver exploding cases
|
||||||
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
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
|
end
|
||||||
|
|
||||||
local data: SpringState<T> = {
|
local data: SpringData<T> = {
|
||||||
k = k,
|
k = k,
|
||||||
c = c,
|
c = c,
|
||||||
|
|
||||||
x0_123 = vector.zero,
|
x0_123 = ZERO,
|
||||||
x_123 = vector.zero,
|
x1_123 = ZERO,
|
||||||
x1_123 = vector.zero,
|
v_123 = ZERO,
|
||||||
v_123 = vector.zero,
|
|
||||||
|
|
||||||
x0_456 = vector.zero,
|
x0_456 = ZERO,
|
||||||
x_456 = vector.zero,
|
x1_456 = ZERO,
|
||||||
x1_456 = vector.zero,
|
v_456 = ZERO,
|
||||||
v_456 = vector.zero,
|
|
||||||
|
|
||||||
source_value = false :: any,
|
source_value = false :: any,
|
||||||
}
|
}
|
||||||
|
|
@ -172,57 +187,33 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
local value = source()
|
local value = source()
|
||||||
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
||||||
data.source_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
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
local updater = create_node(owner, updater_effect, false :: any)
|
local updater = create_node(owner, updater_effect, false :: any)
|
||||||
|
|
||||||
evaluate_node(updater)
|
evaluate_node(updater)
|
||||||
|
|
||||||
-- set initial position to goal
|
-- 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
|
-- set output to goal
|
||||||
output.cache = data.source_value
|
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(...)
|
return function(...)
|
||||||
if select("#", ...) == 0 then -- no args were given
|
if select("#", ...) == 0 then -- no args were given
|
||||||
push_scope_as_child_of(output)
|
push_child_to_scope(output)
|
||||||
return output.cache
|
return output.cache
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set current position to value
|
-- set current position to value
|
||||||
local v = ... :: T
|
local v = ... :: T
|
||||||
data.x_123, data.x_456 = type_to_vec6[typeof(v)](v)
|
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
|
||||||
|
|
||||||
-- reset velocity
|
-- reset velocity
|
||||||
data.v_123 = vector.zero
|
data.v_123 = ZERO
|
||||||
data.v_456 = vector.zero
|
data.v_456 = ZERO
|
||||||
|
|
||||||
-- schedule spring
|
-- schedule spring
|
||||||
springs[data] = output
|
springs[data] = output
|
||||||
|
|
@ -231,32 +222,21 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
output.cache = v
|
output.cache = v
|
||||||
|
|
||||||
return v
|
return v
|
||||||
end, config
|
end
|
||||||
end
|
|
||||||
|
|
||||||
-- calculates a float tolerance, based on the magnitude of the float
|
|
||||||
local function get_min_step(x: number): number
|
|
||||||
return x/TOLERANCE_FACTOR
|
|
||||||
end
|
|
||||||
local function get_min_vector_step(direction: vector): vector
|
|
||||||
return vector.create(
|
|
||||||
get_min_step(direction.x),
|
|
||||||
get_min_step(direction.y),
|
|
||||||
get_min_step(direction.z)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function step_springs(dt: number)
|
local function step_springs(dt: number)
|
||||||
for s in springs do
|
for data in next, springs do
|
||||||
local k = s.k
|
local k, c,
|
||||||
local c = s.c
|
x0_123, x1_123, u_123,
|
||||||
local x_123, x_456 = s.x_123, s.x_456
|
x0_456, x1_456, u_456 =
|
||||||
local x1_123, x1_456 = s.x1_123, s.x1_456
|
data.k, data.c,
|
||||||
local u_123, u_456 = s.v_123, s.v_456
|
data.x0_123, data.x1_123, data.v_123,
|
||||||
|
data.x0_456, data.x1_456, data.v_456
|
||||||
|
|
||||||
-- calculate displacement from target
|
-- calculate displacement from target
|
||||||
local dx_123 = x_123 - x1_123
|
local dx_123 = x0_123 - x1_123
|
||||||
local dx_456 = x_456 - x1_456
|
local dx_456 = x0_456 - x1_456
|
||||||
|
|
||||||
-- calculate spring force
|
-- calculate spring force
|
||||||
local fs_123 = dx_123*-k
|
local fs_123 = dx_123*-k
|
||||||
|
|
@ -266,50 +246,53 @@ local function step_springs(dt: number)
|
||||||
local ff_123 = u_123*-c
|
local ff_123 = u_123*-c
|
||||||
local ff_456 = u_456*-c
|
local ff_456 = u_456*-c
|
||||||
|
|
||||||
-- calculate acceleration
|
-- calculate acceleration step
|
||||||
local a_123 = (fs_123 + ff_123)
|
local dv_123 = (fs_123 + ff_123)*dt
|
||||||
local a_456 = (fs_456 + ff_456)
|
local dv_456 = (fs_456 + ff_456)*dt
|
||||||
|
|
||||||
-- step acceleration
|
-- apply acceleration step
|
||||||
local v_123 = u_123 + a_123*dt
|
local v_123 = u_123 + dv_123
|
||||||
local v_456 = u_456 + a_456*dt
|
local v_456 = u_456 + dv_456
|
||||||
|
|
||||||
-- step velocity
|
-- calculate new position
|
||||||
local y_123 = x_123 + v_123*dt
|
local x_123 = x0_123 + v_123*dt
|
||||||
local y_456 = x_456 + v_456*dt
|
local x_456 = x0_456 + v_456*dt
|
||||||
|
|
||||||
s.x_123, s.x_456 = y_123, y_456
|
data.x0_123, data.x0_456 = x_123, x_456
|
||||||
s.v_123, s.v_456 = v_123, v_456
|
data.v_123, data.v_456 = v_123, v_456
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local remove_queue = {}
|
||||||
|
|
||||||
local function update_spring_sources()
|
local function update_spring_sources()
|
||||||
for data, output in springs do
|
for data, output in next, springs do
|
||||||
local x0_123, x0_456 = data.x0_123, data.x0_456
|
local x0_123, x1_123, v_123,
|
||||||
local x_123, x_456 = data.x_123, data.x_456
|
x0_456, x1_456, v_456 =
|
||||||
local x1_123, x1_456 = data.x1_123, data.x1_456
|
data.x0_123, data.x1_123, data.v_123,
|
||||||
local v_123, v_456 = data.v_123, data.v_456
|
data.x0_456, data.x1_456, data.v_456
|
||||||
|
|
||||||
local tol_123 = vector.abs(get_min_vector_step(x0_123 - x1_123))
|
local dx_123, dx_456 =
|
||||||
local tol_456 = vector.abs(get_min_vector_step(x0_456 - x1_456))
|
x0_123 - x1_123,
|
||||||
|
x0_456 - x1_456
|
||||||
|
|
||||||
if
|
-- todo: can this false positive?
|
||||||
-- position is at goal (within tolerance)
|
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
||||||
vector.max(vector.abs(x_123 - x1_123), tol_123) == tol_123
|
-- close enough to target, unshedule spring and set value to target
|
||||||
and vector.max(vector.abs(x_456 - x1_456), tol_456) == tol_456
|
table.insert(remove_queue, data)
|
||||||
|
|
||||||
-- 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
|
|
||||||
output.cache = data.source_value
|
output.cache = data.source_value
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
update_descendants(output)
|
update_descendants(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for _, data in next, remove_queue do
|
||||||
|
springs[data] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(remove_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
|
||||||
161
src/switch.luau
161
src/switch.luau
|
|
@ -1,113 +1,64 @@
|
||||||
local branch = require "./branch"
|
if not game then script = require "test/relative-string" end
|
||||||
local source = require "./source"
|
|
||||||
local effect = require "./effect"
|
local throw = require(script.Parent.throw)
|
||||||
local timeout = require "./timeout" ()
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
|
local destroy = graph.destroy
|
||||||
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
|
local push_scope = graph.push_scope
|
||||||
|
local pop_scope = graph.pop_scope
|
||||||
|
|
||||||
type Array<T> = { T }
|
|
||||||
type Map<K, V> = { [K]: V }
|
type Map<K, V> = { [K]: V }
|
||||||
type Source<T> = () -> T
|
|
||||||
type Component<T> = (Source<boolean>) -> (T, ...number)
|
|
||||||
|
|
||||||
local function switch_map<K, Obj>(
|
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
|
||||||
input: Source<K>,
|
local owner = assert_stable_scope()
|
||||||
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 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 function(map)
|
||||||
return switch_map(input, map)
|
local last_scope: Node<false>?
|
||||||
|
local last_component: (() -> U)?
|
||||||
|
|
||||||
|
local function update(cached): U?
|
||||||
|
local component = map[source()]
|
||||||
|
if component == last_component then return cached end
|
||||||
|
last_component = component
|
||||||
|
|
||||||
|
if last_scope then
|
||||||
|
destroy(last_scope :: Node<any>)
|
||||||
|
last_scope = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if component == nil then return nil end
|
||||||
|
|
||||||
|
if type(component) ~= "function" then
|
||||||
|
throw "map must map a value to a function"
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_scope = create_node(owner, false, false)
|
||||||
|
last_scope = new_scope :: Node<any>
|
||||||
|
|
||||||
|
push_scope(new_scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(component)
|
||||||
|
|
||||||
|
pop_scope()
|
||||||
|
|
||||||
|
if not ok then error(result, 0) end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(owner, update, nil)
|
||||||
|
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
push_child_to_scope(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
7
src/throw.luau
Normal file
7
src/throw.luau
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local function VIDE_ASSERT(msg): any
|
||||||
|
error(msg, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return VIDE_ASSERT
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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>
|
type Node<T> = graph.Node<T>
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
|
|
||||||
|
|
@ -10,16 +12,16 @@ local function untrack<T>(source: () -> T): T
|
||||||
local effect = scope.effect
|
local effect = scope.effect
|
||||||
scope.effect = false
|
scope.effect = false
|
||||||
|
|
||||||
local ok, result = xpcall(source, debug.traceback)
|
local ok, result = pcall(source)
|
||||||
|
|
||||||
scope.effect = effect :: () -> ()
|
scope.effect = effect :: () -> ()
|
||||||
|
|
||||||
if not ok then error(result, 0) end
|
if not ok then error(result, 0) end
|
||||||
|
|
||||||
return result :: T
|
return result
|
||||||
else
|
else
|
||||||
return source()
|
return source()
|
||||||
end
|
end
|
||||||
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,7 +1,7 @@
|
||||||
local testkit = require("./testkit")
|
local testkit = require("test/testkit")
|
||||||
local BENCH, START = testkit.benchmark()
|
local BENCH, START = testkit.benchmark()
|
||||||
|
|
||||||
local vide = require "../../vide"
|
local vide = require "src/init"
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
@ -27,7 +27,7 @@ local function ROOT_BENCH(name: string, fn: () -> ())
|
||||||
end)()
|
end)()
|
||||||
end
|
end
|
||||||
|
|
||||||
local N = 2^20
|
local N = 2^18 -- 262144
|
||||||
|
|
||||||
TITLE "sources"
|
TITLE "sources"
|
||||||
|
|
||||||
|
|
@ -200,7 +200,7 @@ end)
|
||||||
TITLE "property apply"
|
TITLE "property apply"
|
||||||
|
|
||||||
ROOT_BENCH("apply 0 properties", function()
|
ROOT_BENCH("apply 0 properties", function()
|
||||||
local apply = require "../src/apply"
|
local apply = require "src/apply"
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -209,7 +209,7 @@ ROOT_BENCH("apply 0 properties", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("apply 8 properties", function()
|
ROOT_BENCH("apply 8 properties", function()
|
||||||
local apply = require "../src/apply"
|
local apply = require "src/apply"
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -227,7 +227,7 @@ ROOT_BENCH("apply 8 properties", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("bind property", function()
|
ROOT_BENCH("bind property", function()
|
||||||
local apply = require "../src/apply"
|
local apply = require "src/apply"
|
||||||
|
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
@ -242,7 +242,7 @@ ROOT_BENCH("bind property", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update binding", function()
|
ROOT_BENCH("update binding", function()
|
||||||
local apply = require "../src/apply"
|
local apply = require "src/apply"
|
||||||
|
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
@ -504,44 +504,85 @@ ROOT_BENCH(`get context (depth={depth})`, function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TITLE "spring()"
|
N *= 1024
|
||||||
|
|
||||||
ROOT_BENCH("spring update", function()
|
TITLE "aggregate"
|
||||||
local root, source, spring = vide.root, vide.source, vide.spring
|
|
||||||
|
|
||||||
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()
|
local label = create "TextLabel" {
|
||||||
for i = 1, N do
|
AnchorPoint = Vector2.new(1, 1)
|
||||||
spring(src)
|
}
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
apply(label, {
|
||||||
|
AnchorPoint = Vector2.new(i, i)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
START(N)
|
|
||||||
|
|
||||||
src(1)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
|
||||||
|
|
||||||
ROOT_BENCH("spring step", function()
|
ROOT_BENCH("set aggregate mock vector2", function()
|
||||||
local root, source, spring = vide.root, vide.source, vide.spring
|
local apply = require "src/apply"
|
||||||
|
local Vector2 = require "test/mock".Vector2
|
||||||
|
|
||||||
local src = source(0)
|
local label = create "TextLabel" {
|
||||||
|
AnchorPoint = Vector2.new(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
root(function()
|
for i = 1, START(N) do
|
||||||
for i = 1, N do
|
apply(label, {
|
||||||
spring(src)
|
AnchorPoint = { i, i }
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
src(1)
|
|
||||||
|
|
||||||
START(N)
|
|
||||||
|
|
||||||
vide.step(1/60)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end)
|
end)
|
||||||
end)
|
end
|
||||||
|
|
||||||
|
-- innacurate due to no Vector3 in vanilla Luau
|
||||||
|
-- mock vector is 200x slower than native vector
|
||||||
|
|
||||||
|
-- ROOT_BENCH("spring update", function()
|
||||||
|
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
|
|
||||||
|
-- local src = source(0)
|
||||||
|
|
||||||
|
-- root(function()
|
||||||
|
-- for i = 1, N do
|
||||||
|
-- spring(src)
|
||||||
|
-- end
|
||||||
|
|
||||||
|
-- START(N)
|
||||||
|
|
||||||
|
-- src(1)
|
||||||
|
|
||||||
|
-- return nil
|
||||||
|
-- end)
|
||||||
|
-- end)
|
||||||
|
|
||||||
|
-- N /= 1024
|
||||||
|
|
||||||
|
-- ROOT_BENCH("spring step", function()
|
||||||
|
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
|
|
||||||
|
-- local src = source(0)
|
||||||
|
|
||||||
|
-- root(function()
|
||||||
|
-- for i = 1, N do
|
||||||
|
-- spring(src)
|
||||||
|
-- end
|
||||||
|
|
||||||
|
-- src(1)
|
||||||
|
|
||||||
|
-- START(N)
|
||||||
|
|
||||||
|
-- vide.step(1/60)
|
||||||
|
|
||||||
|
-- return nil
|
||||||
|
-- end)
|
||||||
|
-- end)
|
||||||
|
|
||||||
return nil
|
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? }
|
local proxies = {} :: { [Data]: userdata? }
|
||||||
setmetatable(proxies :: any, { __mode = "v" })
|
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 get_data(userdata: userdata): Data
|
||||||
local function f(userdata: userdata): ProxyMT
|
local function f(userdata: userdata): ProxyMT
|
||||||
return getmetatable(userdata :: any)
|
return getmetatable(userdata :: any)
|
||||||
|
|
@ -97,19 +94,6 @@ local Instance = {} :: any do
|
||||||
return f(userdata).data
|
return f(userdata).data
|
||||||
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
|
|
||||||
|
|
||||||
local function is_instance(value: unknown): boolean
|
local function is_instance(value: unknown): boolean
|
||||||
local mt = getmetatable(value :: any)
|
local mt = getmetatable(value :: any)
|
||||||
return mt and mt.data and mt.data.type == "Instance"
|
return mt and mt.data and mt.data.type == "Instance"
|
||||||
|
|
@ -117,16 +101,16 @@ local Instance = {} :: any do
|
||||||
|
|
||||||
local methods = {}
|
local methods = {}
|
||||||
|
|
||||||
__index = function(userdata: userdata, property: string): ()
|
local function __index(userdata: userdata, property: string): ()
|
||||||
local data = get_data(userdata)
|
local data = get_data(userdata)
|
||||||
return if methods[property] then methods[property]
|
return if methods[property] then methods[property]
|
||||||
elseif property == "Name" then data.name
|
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
|
elseif property == "Destroying" then data.destroying
|
||||||
else data.properties[property]
|
else data.properties[property]
|
||||||
end
|
end
|
||||||
|
|
||||||
__newindex = function(userdata: userdata, property: string, value: unknown)
|
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||||
local data = get_data(userdata)
|
local data = get_data(userdata)
|
||||||
if property == "Name" then
|
if property == "Name" then
|
||||||
if type(value) ~= "string" then error("name must be a string", 2) end
|
if type(value) ~= "string" then error("name must be a string", 2) end
|
||||||
|
|
@ -151,6 +135,19 @@ local Instance = {} :: any do
|
||||||
end
|
end
|
||||||
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
|
function Instance.new(class: string): Instance
|
||||||
local data = {
|
local data = {
|
||||||
name = "UNNAMED",
|
name = "UNNAMED",
|
||||||
|
|
@ -260,6 +257,40 @@ local Vector2 = { __type = "Vector2" } :: any do
|
||||||
end
|
end
|
||||||
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
|
local UDim2 = { __type = "UDim2" } :: any do
|
||||||
function UDim2.new(sx, ox, sy, oy)
|
function UDim2.new(sx, ox, sy, oy)
|
||||||
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
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),
|
Instance = Instance :: typeof(Instance),
|
||||||
Color3 = Color3 :: typeof(Color3),
|
Color3 = Color3 :: typeof(Color3),
|
||||||
Vector2 = Vector2 :: typeof(Vector2),
|
Vector2 = Vector2 :: typeof(Vector2),
|
||||||
|
Vector3 = Vector3 :: typeof(Vector3),
|
||||||
UDim2 = UDim2 :: typeof(UDim2),
|
UDim2 = UDim2 :: typeof(UDim2),
|
||||||
Enum = Enum :: typeof(Enum),
|
Enum = Enum :: typeof(Enum),
|
||||||
typeof = typeof :: typeof(typeof)
|
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 program_time = os.clock()
|
||||||
local MAX = 40
|
|
||||||
local MIN = 10
|
|
||||||
|
|
||||||
local _, input, output = vide.root(function()
|
local function step(): number
|
||||||
local input = vide.source(MAX)
|
local FPS = 60
|
||||||
local output = vide.spring(input, 1, .3)
|
local DT = 1/FPS
|
||||||
return input, output
|
|
||||||
end)
|
|
||||||
|
|
||||||
local T = 10
|
repeat until os.clock() - program_time >= DT
|
||||||
local t = 0
|
program_time += DT
|
||||||
return function(dt)
|
return DT
|
||||||
t += dt
|
|
||||||
if t >= T then
|
|
||||||
t -= T
|
|
||||||
input(input() == MAX and MIN or MAX)
|
|
||||||
end
|
|
||||||
|
|
||||||
vide.step(dt)
|
|
||||||
|
|
||||||
return output()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
local function main()
|
||||||
|
local TERMINAL_HEIGHT = 73 --* REDUCE IF BAR DOES NOT FIT IN TERMINAL
|
||||||
|
local MIN_ALPHA = 0.3
|
||||||
|
local MAX_ALPHA = 0.7
|
||||||
|
|
||||||
local function redraw_block(h: number)
|
local MIN = TERMINAL_HEIGHT * MIN_ALPHA
|
||||||
local OFFSET = 70
|
local MAX = TERMINAL_HEIGHT * MAX_ALPHA
|
||||||
|
local OFFSET = TERMINAL_HEIGHT - MAX
|
||||||
|
|
||||||
local BLOCK = "█"
|
local BLOCK = "█"
|
||||||
|
|
||||||
|
|
@ -44,35 +35,37 @@ local function redraw_block(h: number)
|
||||||
else "▁"
|
else "▁"
|
||||||
end
|
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 reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||||
local offset = string.rep("\n", OFFSET - h_f)
|
local offset = string.rep("\n", MAX - fv + OFFSET)
|
||||||
local bar = remainder_to_block(h - h_f) .. "\n" .. string.rep(BLOCK .. "\n", h_f)
|
local bar = testkit.color.gray(remainder_to_block(v - fv) .. "\n" .. string.rep(BLOCK .. "\n", fv))
|
||||||
--print(reset .. offset .. bar .. "\n" .. string.format("%.1f", h))
|
print(reset .. offset .. bar .. "\n" .. v)
|
||||||
print(reset .. offset .. bar .. "\n" .. h)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
local program_time = os.clock()
|
local T = 3
|
||||||
|
local elapsed = T/1.2
|
||||||
|
repeat local dt = step()
|
||||||
|
vide.step(dt)
|
||||||
|
|
||||||
local function step(): number
|
elapsed += dt
|
||||||
local FPS = 30
|
while elapsed >= T do
|
||||||
local DT = 1/FPS
|
elapsed -= T
|
||||||
|
value(value() == MAX and MIN or MAX)
|
||||||
repeat until os.clock() - program_time >= DT
|
|
||||||
program_time += DT
|
|
||||||
return DT
|
|
||||||
end
|
|
||||||
|
|
||||||
local function loop()
|
|
||||||
local callback = system()
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local dt = step()
|
|
||||||
local x = callback(dt)
|
|
||||||
redraw_block(x)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
until false
|
||||||
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
|
|
||||||
918
test/tests.luau
918
test/tests.luau
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
||||||
name = "centau/vide"
|
name = "centau/vide"
|
||||||
description = "A reactive Luau library for creating UI. "
|
description = "A reactive Luau library for creating UI. "
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "0.4.1"
|
version = "0.3.0"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
include = ["default.project.json", "LICENSE", "src"]
|
include = ["default.project.json", "LICENSE", "src"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue