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.1" have entirely different histories.
93 changed files with 2811 additions and 5395 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.luau linguist-language=Lua
|
||||||
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
|
|
@ -1,33 +0,0 @@
|
||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: setup rokit
|
|
||||||
uses: CompeyDev/setup-rokit@v0.1.2
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
run: rojo build default.project.json -o build.rbxm
|
|
||||||
|
|
||||||
- name: release
|
|
||||||
run: gh release upload ${{github.event.release.tag_name}} build.rbxm
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
.github/workflows/deploy.yml
vendored
13
.github/workflows/deploy.yml
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
name: site-deploy
|
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"
|
latest: true
|
||||||
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
|
|
||||||
|
|
|
||||||
117
CHANGELOG.md
117
CHANGELOG.md
|
|
@ -4,122 +4,9 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
## Unreleased
|
||||||
|
|
||||||
## [0.4.1] - 2026-07-11
|
---
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `branch()` is now allowed to be used within a reactive scope.
|
|
||||||
|
|
||||||
### Deprecated
|
|
||||||
|
|
||||||
- `create()` overloads. Supported is now only `create(class)(props)`.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `create()` types in the new solver should now work without `::`.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.4.0] - 2026-01-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `create("ClassName", { props })` and `create(Instance, { props })` syntax.
|
|
||||||
- `cleanup()` now accepts `thread` types.
|
|
||||||
- Implicit effects to set children can now recursively create more implicit
|
|
||||||
effects to set children.
|
|
||||||
- `spring()` returns a second value, a setter to set position, velocity and
|
|
||||||
impulse.
|
|
||||||
- Improved `spring()` updating and unscheduling.
|
|
||||||
- `show()` now receives a source to its callback returning the current value
|
|
||||||
of the condition.
|
|
||||||
- Ignore `false` passed as a child.
|
|
||||||
- Flag `vide.defaults` to disable the setting of default properties.
|
|
||||||
- Delayed scope destruction for control flow functions: `show()` `switch()` `indexes()` `values()`.
|
|
||||||
- Better `create()` types for the new type solver.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- A scope can no longer be destroyed while it is active. Strict mode will check
|
|
||||||
for this.
|
|
||||||
- Implicit effects to set children now unparent all children when the effect is
|
|
||||||
destroyed.
|
|
||||||
- Error reporting should be improved with better formatting when effects invoke
|
|
||||||
other effects and no more loss of stack traces.
|
|
||||||
- Nesting parent properties now work, and they are now also checked for
|
|
||||||
duplicates like other properties.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Aggregate construction when setting properties with `create()`.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.3.1] - 2024-10-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Context functions now also return results.
|
|
||||||
- `version` table with current version.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.3.0] - 2024-10-06
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `context()`.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `root()` now returns its destructor as the first value by default.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Error stack traces being lost.
|
|
||||||
- `root()` now destroys the scope automatically if an error occurs during call.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.2.0] - 2023-11-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Batched updates with `batch()`.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improved graph updating algorithm.
|
|
||||||
- Graph nodes when destroyed no longer destroy children; only owned.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Graph edge case where a destroyed node can be readded if it was queued for
|
|
||||||
rerun before being destroyed.
|
|
||||||
- Some properties not being applied when `create()` is used recursively.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.1.1] - 2023-09-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `cleanup()` accepts objects with a `Destroy()` or `Disconnect()` interface.
|
|
||||||
- `read()` as a utility to read sources or passthrough a non-source value.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Reactive scopes created within reactive scopes are now destroyed on rerun.
|
|
||||||
- `untrack()` can be called outside of reactive scopes.
|
|
||||||
- `changed()` will also run its callback with the initial property value.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `show()` and `switch()` not updating when in strict mode.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## [0.1.0] - 2023-09-20
|
## [0.1.0] - 2023-09-20
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
//import { defineConfig } from "vitepress"
|
import { defineConfig } from "vitepress"
|
||||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
|
||||||
|
|
||||||
// https://vitepress.dev/reference/site-config
|
// https://vitepress.dev/reference/site-config
|
||||||
export default withMermaid({
|
export default defineConfig({
|
||||||
title: "Vide",
|
title: "Vide",
|
||||||
titleTemplate: ":title - A reactive UI library for Luau",
|
titleTemplate: ":title - A reactive UI library for Luau",
|
||||||
description: "A reactive UI library for Luau.",
|
description: "A reactive UI library for Luau.",
|
||||||
|
|
@ -12,14 +11,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 +25,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" },
|
||||||
|
|
@ -51,26 +42,19 @@ export default withMermaid({
|
||||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||||
{ 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: "Stateful Components", link: "/tut/crash-course/6-stateful-component" },
|
||||||
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
|
{ text: "Property Binding", link: "/tut/crash-course/7-property-binding" },
|
||||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
{ text: "Cleanup", link: "/tut/crash-course/8-cleanup" },
|
||||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
{ text: "Control Flow", link: "/tut/crash-course/9-control-flow" },
|
||||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
{ text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" },
|
||||||
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
|
{ text: "Actions", link: "/tut/crash-course/11-actions" },
|
||||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
{ text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" },
|
||||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
|
||||||
{ 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: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
.home > * > .VPNavBar:not(.top) {
|
|
||||||
backdrop-filter: blur(0.5rem);
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home > * > .VPNavBar > .divider {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home > .VPContent {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.VPHome {
|
|
||||||
margin-top: auto !important;
|
|
||||||
margin-bottom: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.VPBadge a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
// .vitepress/theme/index.js
|
// .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.
|
The output source value is updated every step 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,38 @@
|
||||||
# Element Creation
|
# Element Creation API
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## mount()
|
||||||
|
|
||||||
|
Runs a function and applies its result to a target instance.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
The result of the function is applies to the target in the same way
|
||||||
|
properties are using `create()`.
|
||||||
|
|
||||||
|
The function is ran in a new reactive scope, just like
|
||||||
|
[root()](reactivity-core.md#root).
|
||||||
|
|
||||||
|
Returns a function that when called will destroy the reactive scope.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function App()
|
||||||
|
return create "ScreenGui" {
|
||||||
|
create "TextLabel" { Text = "Vide" }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
|
```
|
||||||
|
|
||||||
## create()
|
## create()
|
||||||
|
|
||||||
|
|
@ -6,11 +40,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**
|
||||||
|
|
@ -26,68 +60,86 @@ Creates a new UI element, applying any given properties.
|
||||||
|
|
||||||
- **Property setting rules**
|
- **Property setting rules**
|
||||||
|
|
||||||
- **index is string:**
|
- If a table index is a string:
|
||||||
- **value is function:**
|
- If its value is a function then it will either bind that property to
|
||||||
- **property is event:** connect function as callback
|
the function or connect it if the property type is a `RBXScriptSignal`.
|
||||||
- **property is not event:** create effect to update property
|
- If the value is not a function then the property will be set to that
|
||||||
- **value is not function:** set property to value
|
value.
|
||||||
- **index is number:**
|
- If a table index is a number:
|
||||||
- **value is action:** run action
|
- If its value is an action then that action will be queued to run after
|
||||||
- **value is table:** recurse table
|
properties are set.
|
||||||
- **value is function:** create effect to update children
|
- If its value is a table then that table will be recursively
|
||||||
- **value is instance:** set instance as child
|
processed just like the outer table.
|
||||||
|
- If its value is a function then it will bind the instances children to
|
||||||
|
that function.
|
||||||
|
- If its value is an instance then it will be parented to the instance.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
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()
|
instance:GetPropertyChangedSignal("property"):Connect(function()
|
||||||
fn(instance[property])
|
callback(instance[property])
|
||||||
end)
|
|
||||||
|
|
||||||
-- disconnect on scope destruction to allow gc of instance
|
|
||||||
cleanup(function()
|
|
||||||
cn:Disconnect()
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -95,58 +147,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)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## changed()
|
|
||||||
|
|
||||||
A wrapper for `action()` to listen for property changes.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
function changed(property: string, fn: (unknown) -> ()): Action
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Details**
|
|
||||||
|
|
||||||
Will run the given function immediately and whenever the property updates.
|
|
||||||
|
|
||||||
The function is called with the updated property value.
|
|
||||||
|
|
||||||
Runs with an action priority of 1.
|
|
||||||
|
|
||||||
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
|
||||||
|
|
||||||
Runs a function in a new stable scope and optionally applies its result to a
|
|
||||||
target instance.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Details**
|
|
||||||
|
|
||||||
This is a utility for `root()` when parenting a component to an existing
|
|
||||||
instance.
|
|
||||||
|
|
||||||
The result of the function is applied to a target in the same way
|
|
||||||
properties are using `create()`.
|
|
||||||
|
|
||||||
Returns a function that when called will destroy the stable scope.
|
|
||||||
|
|
||||||
- **Example**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local function App()
|
|
||||||
return create "ScreenGui" {
|
|
||||||
create "TextLabel" { Text = "Vide" }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local destroy = mount(App, game.StarterGui)
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -1,155 +1,132 @@
|
||||||
# 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>.
|
## root()
|
||||||
|
|
||||||
- Reactive scopes rerun if a source read within updates.
|
Creates and runs a function in a new reactive scope.
|
||||||
- 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
|
|
||||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
|
||||||
for this.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
|
||||||
|
|
||||||
Runs a function in a new stable scope.
|
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||||
|
|
||||||
type Destructor = () -> ()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Returns a destructor and any values returned by the callback.
|
Creates a new root reactive scope, where creation and derivations of sources
|
||||||
|
can be tracked and properly disposed of.
|
||||||
|
|
||||||
|
Returns the result of the given function.
|
||||||
|
|
||||||
|
A function to destroy the root is passed into the callback, which will run
|
||||||
|
any cleanups and allow derived sources created to garbage collect.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
`fn()` cannot yield.
|
||||||
|
:::
|
||||||
|
|
||||||
## source()
|
## 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): (T?) -> T
|
||||||
|
|
||||||
type Source<T> =
|
|
||||||
() -> T -- get
|
|
||||||
& (T) -> () -- set
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **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.
|
||||||
|
|
||||||
|
Reading from the source from within a reactive scope will cause changes
|
||||||
|
to that source to be tracked and anything depending on it to update.
|
||||||
|
|
||||||
- **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 on source update.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function effect(fn: () -> ())
|
function effect(callback: () -> ())
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
The function is ran once immediately.
|
The callback is ran immediately.
|
||||||
|
|
||||||
|
Any time a source referenced in the callback is changed, the callback will
|
||||||
|
be reran.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
`callback()` cannot yield.
|
||||||
|
:::
|
||||||
|
|
||||||
- **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 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.
|
||||||
|
|
||||||
|
Takes a callback that is immediately run to determine what sources are being
|
||||||
|
referenced.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
`source()` cannot yield.
|
||||||
|
:::
|
||||||
|
|
||||||
- **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 reactive scope. If it changes from a
|
||||||
|
truthy to falsey value, the reactive scope the component was created in will
|
||||||
|
be destroyed, and the returned source will output `nil`, or a fallback
|
||||||
|
component if given.
|
||||||
|
|
||||||
|
The fallback component is also ran under a new reactive scope, and destroyed
|
||||||
|
when the input source switches back to truthy.
|
||||||
|
|
||||||
|
## switch()
|
||||||
|
|
||||||
|
Shows one of a set of components depending on an input source and a mapping table.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an instance of the currently shown component.
|
||||||
|
|
||||||
|
When the input source changes, the new value will be used to lookup a given
|
||||||
|
mapping table to get a component, which will be ran under a new reactive
|
||||||
|
scope. If the input source changes, the reactive scope the component was
|
||||||
|
created in will be destroyed, and a new component created under a new
|
||||||
|
reactive scope. If no component is found for an input value, the switch will
|
||||||
|
output `nil`.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local logged = source(false)
|
||||||
|
|
||||||
|
local button = switch(logged) {
|
||||||
|
[true] = function()
|
||||||
|
return Button { Text = "Log out", Toggle = logged }
|
||||||
|
end,
|
||||||
|
|
||||||
|
[false] = function()
|
||||||
|
return Button { Text = "Log in", Toggle = logged }
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## indexes()
|
||||||
|
|
||||||
|
Maps each index in a table source to an object.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function indexes<KI, VI, VO>(
|
||||||
|
source: () -> Map<KI, VI>,
|
||||||
|
transform: (value: () -> VI, index: KI) -> VO
|
||||||
|
): Array<VO>
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an array of instances currently shown.
|
||||||
|
|
||||||
|
When the input source changes, each *index* in the new table is compared with
|
||||||
|
the last input table.
|
||||||
|
|
||||||
|
- For any new index, the `transform` function is ran under a new reactive
|
||||||
|
scope to produce a new instance.
|
||||||
|
- For any removed index, the reactive scope for that index is destroyed.
|
||||||
|
- Unchanged indexes are untouched.
|
||||||
|
|
||||||
|
The transform function is called only ever *once* for each index in the
|
||||||
|
source table.
|
||||||
|
|
||||||
|
1. First argument is a *source containing the index's value*.
|
||||||
|
2. Second argument is the *index itself*.
|
||||||
|
|
||||||
|
Anytime an existing index's value changes, the transform function is not
|
||||||
|
rerun, instead the source value for that index will update, causing anything
|
||||||
|
depending on it to update too.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
The intended purpose of this function is to map each index in a table to
|
||||||
|
a UI element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Item = {
|
||||||
|
name: string,
|
||||||
|
icon: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local items = source {} :: () -> Array<Item>
|
||||||
|
|
||||||
|
local displays = indexes(items, function(item, i)
|
||||||
|
return ItemDisplay {
|
||||||
|
Name = function()
|
||||||
|
return i .. ": " .. item().name
|
||||||
|
end,
|
||||||
|
|
||||||
|
Image = function()
|
||||||
|
return "rbxassetid://" .. item().icon
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## values()
|
||||||
|
|
||||||
|
Maps each value in a table source to an object.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function values<KI, VI, VO>(
|
||||||
|
source: () -> Map<KI, VI>,
|
||||||
|
transform: (value: VI, index: () -> KI) -> VO
|
||||||
|
): Array<VO>
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Returns a source holding an array of instances currently shown.
|
||||||
|
|
||||||
|
When the input source changes, each *value* in the new table is compared with
|
||||||
|
the last input table. Similar to `indexes()` but for values instead of indexes.
|
||||||
|
|
||||||
|
- For any new value, the `transform` function is ran under a new reactive
|
||||||
|
scope to produce a new instance.
|
||||||
|
- For any removed value, the reactive scope for that value is destroyed.
|
||||||
|
- Unchanged values are untouched.
|
||||||
|
|
||||||
|
The transform function is only ever called *once* for each value in the
|
||||||
|
source table.
|
||||||
|
|
||||||
|
1. First argument is the *value itself*.
|
||||||
|
2. Second argument is a *source containing the value's index*.
|
||||||
|
|
||||||
|
Anytime an existing value's index changes, the transform function is not
|
||||||
|
rerun, instead the source index for that value will update, causing anything
|
||||||
|
depending on it to update too.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Having primitive values in the input source table can cause unexpected
|
||||||
|
behavior, as duplicate values can result in multiple tranforms being ran for
|
||||||
|
a single value, meaning there can be multiple source indexes bound to the
|
||||||
|
same UI element. Strict mode has checks for this.
|
||||||
|
:::
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
The intended purpose of this function is to map each value in a table to
|
||||||
|
a UI element.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Item = {
|
||||||
|
name: string,
|
||||||
|
icon: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local items = source {} :: () -> Array<Item>
|
||||||
|
|
||||||
|
local displays = values(items, function(item, i)
|
||||||
|
return ItemDisplay {
|
||||||
|
Name = function()
|
||||||
|
return i() .. ": " .. item.Name
|
||||||
|
end
|
||||||
|
|
||||||
|
Image = "rbxassetid://" .. item.icon,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Extra**
|
||||||
|
|
||||||
|
When should you use `indexes()` and `values()`?
|
||||||
|
|
||||||
|
`values()` should be used when you have a fixed set of objects where the
|
||||||
|
same objects can be re-arranged in the source table. It maps a value to a
|
||||||
|
UI element.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
- List of all players.
|
||||||
|
- Inventory of items.
|
||||||
|
- Chat message history.
|
||||||
|
- Toast notifications.
|
||||||
|
|
||||||
|
`indexes()` should be used in other cases, especially when your source table
|
||||||
|
has primitive value. It maps an index to a UI element.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
- List of character or weapon stats.
|
||||||
|
|
||||||
|
In most cases, both functions will appear to have the same behavior.
|
||||||
|
The main difference is performance, picking the right function to use can
|
||||||
|
result in less property updates and less re-renders. One case to note is
|
||||||
|
that `values()` works nicely when animating re-ordering of instances, since
|
||||||
|
the value is not destroyed when indexes are changed, and the source index
|
||||||
|
can easily be put through a spring.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
@ -1,58 +1,47 @@
|
||||||
# Reactivity: Utility
|
# Reactivity API: Utility
|
||||||
|
|
||||||
## cleanup()
|
## cleanup()
|
||||||
|
|
||||||
Queues a callback to run when a scope is reran or destroyed.
|
Runs a callback anytime a reactive scope is re-ran.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function cleanup(v: Function | Disconnectable | Destroyable | thread)
|
function cleanup(callback: () -> ())
|
||||||
|
|
||||||
type Function = () -> ()
|
|
||||||
type Destroyable = { destroy: () -> () }
|
|
||||||
type Disconnectable = { disconnect: () -> () }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local data = source(1)
|
||||||
|
|
||||||
local destroy = root(function()
|
effect(function()
|
||||||
effect(function()
|
local label = create "TextLabel" { Text = data() }
|
||||||
count()
|
|
||||||
|
|
||||||
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 where any sources read will not track its reactive scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function untrack<T>(source: () -> T): T
|
function untrack<T>(source: () -> T): T
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Can be used inside a reactive scope to read from sources you do not want
|
Updates made to a source passed to `untrack()` will not cause updates to
|
||||||
tracked by the reactive scope.
|
anything depending on that source.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local a = source(0)
|
local a = source(0)
|
||||||
local b = source(0)
|
local b = source(0)
|
||||||
|
|
||||||
|
|
@ -61,9 +50,9 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -73,86 +62,8 @@ Utility used to read a value that is either a primitive or a source.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
function read<T>(value: T | () -> T): T
|
function read<T>(value: T | () -> T): T
|
||||||
```
|
```
|
||||||
|
|
||||||
## batch()
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
Runs a function where any source updates made within the function do not
|
|
||||||
trigger effects until after the function ends.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
function batch(fn: () -> ())
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Details**
|
|
||||||
|
|
||||||
Improves performance when an effect depends on multiple sources, and those
|
|
||||||
sources need to be updated.
|
|
||||||
|
|
||||||
- **Example**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local a = source(0)
|
|
||||||
local b = source(0)
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
print(a() + b())
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- prints "0"
|
|
||||||
|
|
||||||
batch(function()
|
|
||||||
a(1) -- no print
|
|
||||||
b(2) -- no print
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- prints "3"
|
|
||||||
```
|
|
||||||
|
|
||||||
## context() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
|
||||||
|
|
||||||
Creates a new context.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
function context<T>(default: T): Context<T>
|
|
||||||
|
|
||||||
type Context<T> =
|
|
||||||
() -> T -- get
|
|
||||||
& <U>(T, () -> U) -> U -- set
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Details**
|
|
||||||
|
|
||||||
Calling `context()` returns a new context function.
|
|
||||||
Call this function with no arguments to get the context value.
|
|
||||||
Call this function with a value and a function to create a new context with
|
|
||||||
the given value.
|
|
||||||
|
|
||||||
The new context is run under a stable scope.
|
|
||||||
|
|
||||||
- **Example**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local theme = context()
|
|
||||||
|
|
||||||
local function Button()
|
|
||||||
print(theme())
|
|
||||||
end
|
|
||||||
|
|
||||||
root(function()
|
|
||||||
theme("light", function()
|
|
||||||
Button() -- prints "light"
|
|
||||||
|
|
||||||
theme("dark", function()
|
|
||||||
Button() -- prints "dark"
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,24 @@ 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 sources and effects, any side-effects are made more apparent.
|
||||||
computations are pure, and that any cleanup is done correctly.
|
This also helps ensure that cleanups are being handled 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 can 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 recommend to develop UI with strict mode and to disable it when pushing to
|
||||||
production. In Roblox, production code compiles at O2 by default, so you 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,13 @@
|
||||||
{
|
{
|
||||||
"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.4"
|
||||||
"vitepress-plugin-mermaid": "2.0.17"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
61
docs/tut/advanced/reactive-scoping.md
Normal file
61
docs/tut/advanced/reactive-scoping.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Reactive Scoping
|
||||||
|
|
||||||
|
This is a brief document designed to give the user more insight into how Vide's
|
||||||
|
reactive graph works.
|
||||||
|
|
||||||
|
Each time you create and derive sources, a new node representing that source is
|
||||||
|
created and added to the reactive graph. Each node stores a value and a
|
||||||
|
side-effect function. Each node also keeps track of its parents and children,
|
||||||
|
as well as any cleanups registered.
|
||||||
|
|
||||||
|
Any time a node is updated, Vide will traverse and update that node's children,
|
||||||
|
its children's children, etc, until all nodes descending from that node has been
|
||||||
|
updated. Traversal will stop at a node if that node's cached value does not
|
||||||
|
change after an update.
|
||||||
|
|
||||||
|
For every node that is updated, a scope is opened for that node. These scopes
|
||||||
|
are referred to as "reactive scopes". Any source read from within a node's scope
|
||||||
|
will that node as a child. This is similar to cleanups, anytime a cleanup is
|
||||||
|
registered, it is added to the node of the currently active scope.
|
||||||
|
|
||||||
|
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
|
||||||
|
active reactive scope is the node at the top of this stack.
|
||||||
|
|
||||||
|
When destroying a node, its descendents are traversed and also destroyed.
|
||||||
|
When being destroyed, a node's connections (parents and children) are cleared,
|
||||||
|
and any pending cleanup functions are ran.
|
||||||
|
|
||||||
|
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
||||||
|
the root node which will track any node created or derived inside its scope, or
|
||||||
|
any cleanups registered. Without it, nodes could be garbage collected without a
|
||||||
|
chance to run pending cleanups which can cause memory leakage.
|
||||||
|
|
||||||
|
Control flow functions in Vide are special, as they can dynamically create and
|
||||||
|
destroy new root scopes.
|
||||||
|
|
||||||
|
It is the combination of the above which allows us to write components like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local connection = stepped:Connect(function() count(count() + 1) end)
|
||||||
|
|
||||||
|
cleanup(function() connection:Disconnect() end)
|
||||||
|
effect(function() print(count()) end)
|
||||||
|
|
||||||
|
return create "TextLabel" { Text = count }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
|
||||||
|
just sees this as a function that creates nodes in the reactive graph.
|
||||||
|
|
||||||
|
Whenever the reactive scope that calls this function is destroyed, like by a
|
||||||
|
control flow function, the registered cleanup will be called, and the effect
|
||||||
|
(which is just a node on the reactive graph) is destroyed. The returned instance
|
||||||
|
and the bound `count` source is just considered to be a side-effect, and with
|
||||||
|
the reactive scope from which the side-effects stem from destroyed, the instance
|
||||||
|
can be garbage collected - everything is nicely cleaned up.
|
||||||
|
|
||||||
|
> todo: add graphics
|
||||||
0
docs/tut/control-flow/1-intro.md
Normal file
0
docs/tut/control-flow/1-intro.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# show()
|
||||||
1
docs/tut/control-flow/3-switch.md
Normal file
1
docs/tut/control-flow/3-switch.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# switch()
|
||||||
1
docs/tut/control-flow/4-indexes.md
Normal file
1
docs/tut/control-flow/4-indexes.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# indexes()
|
||||||
1
docs/tut/control-flow/5-values.md
Normal file
1
docs/tut/control-flow/5-values.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# values()
|
||||||
|
|
@ -1,21 +1,42 @@
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
This is a tutorial that introduces the concepts and usage of Vide.
|
This is a brief tutorial designed to give you a quick run through the usage of
|
||||||
|
Vide.
|
||||||
|
|
||||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
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
|
Creating UI is a slow and tedious process. The purpose of Vide is to make UI
|
||||||
possible, with a strong focus on how data flows through your application.
|
declarative and concise, making it faster to create and more importantly easier
|
||||||
|
to maintain. Vide achieves this using a reactive style of programming which
|
||||||
|
allows you to focus on the flow of data through your application without
|
||||||
|
worrying about manually updating UI instances.
|
||||||
|
|
||||||
Some of Vide's main design choices:
|
Some of the main focuses behind Vide's design choices:
|
||||||
|
|
||||||
- Syntax minimal.
|
- Concise syntax to reduce verbosity as much as possible.
|
||||||
- Data oriented.
|
- Being completely typecheckable.
|
||||||
- Typechecking compatible.
|
- Independence from instance lifetimes.
|
||||||
- Instance independent.
|
- A powerful reactive system that can update specific properties as a result of
|
||||||
|
state changes, updates are immediate with no diffing needed.
|
||||||
|
|
||||||
Vide's reactivity operates with the concept
|
## Structure Of A Vide App
|
||||||
of scopes which carries a learning curve, though is what makes Vide's minimal
|
|
||||||
syntax possible. The crash course will introduce these concepts gradually.
|
The entry point for all Vide apps is the `mount()` function. This function
|
||||||
|
sets up Vide's reactivity system. It takes and calls a function that should
|
||||||
|
create your entire app, and will apply its result to a target.
|
||||||
|
|
||||||
|
In Vide, your app should be composed of functions, each function creates a
|
||||||
|
specific part of your app, and can be reused if needed. These functions are
|
||||||
|
called *components*.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function App()
|
||||||
|
return create "ScreenGui" {
|
||||||
|
create "TextLabel" { Text = "hi" }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
# Cleanup
|
|
||||||
|
|
||||||
Sometimes you may need to do some cleanup when destroying a component or after
|
|
||||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
|
||||||
is used to queue a callback for the next time a reactive scope is rerun or
|
|
||||||
destroyed, or when a stable scope is destroyed.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local root = vide.root
|
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
|
||||||
local cleanup = vide.cleanup
|
|
||||||
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local destroy = root(function()
|
|
||||||
effect(function()
|
|
||||||
local x = count()
|
|
||||||
cleanup(function() print(x) end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
cleanup(function() print "root destroyed" end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
count(1) -- prints "0"
|
|
||||||
count(2) -- prints "1"
|
|
||||||
destroy() -- prints "2" and "root destroyed"
|
|
||||||
```
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
Roblox instances do not need to be explicitly destroyed for their
|
|
||||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
|
||||||
need to use `cleanup()` to destroy instances. However, be wary of connecting
|
|
||||||
a function that references an instance to an event from the same instance,
|
|
||||||
this causes the instance to reference itself and never be freed. In such a case
|
|
||||||
you would need to use `cleanup()` to disconnect this connection or to explicitly
|
|
||||||
destroy the instance.
|
|
||||||
:::
|
|
||||||
120
docs/tut/crash-course/10-property-nesting.md
Normal file
120
docs/tut/crash-course/10-property-nesting.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Nested Properties
|
||||||
|
|
||||||
|
Often when creating components from existing components, you can find yourself
|
||||||
|
repetitively passing through properties such as size or position.
|
||||||
|
|
||||||
|
Example below:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function Background(props: {
|
||||||
|
Color: Color3,
|
||||||
|
AnchorPoint: UDim2,
|
||||||
|
Position: UDim2,
|
||||||
|
Size: UDim2
|
||||||
|
})
|
||||||
|
return create "Frame" {
|
||||||
|
Color = props.Color
|
||||||
|
AnchorPoint = props.AnchorPoint,
|
||||||
|
Position = props.Position,
|
||||||
|
Size = props.Size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu(props: {
|
||||||
|
Color = props.Color
|
||||||
|
AnchorPoint: UDim2,
|
||||||
|
Position: UDim2,
|
||||||
|
Size: UDim2
|
||||||
|
})
|
||||||
|
return Background {
|
||||||
|
Color = props.COlor,
|
||||||
|
AnchorPoint = props.AnchorPoint,
|
||||||
|
Position = props.Position,
|
||||||
|
Size = props.Size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
One way this can be avoided is by using *property nesting*. In Vide, passing a
|
||||||
|
table value inside `props` has special semantics. Any key with a table value is
|
||||||
|
not assigned like a property, instead the table is iterated and processed just
|
||||||
|
like the outer table is. Any properties in the nested table will be assigned
|
||||||
|
to the instance just the same.
|
||||||
|
|
||||||
|
Below is an example of how you can use this to pass groups of similar properties
|
||||||
|
together such as position and size, while also using typechecking.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Layout = {
|
||||||
|
Layout = {
|
||||||
|
Position: UDim2?,
|
||||||
|
Size: UDim2?,
|
||||||
|
AnchorPoint: Vector2?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Background(props: Layout & { Color: Color3 })
|
||||||
|
return create "Frame" {
|
||||||
|
Color = props.Color,
|
||||||
|
props.Layout
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu(props: Layout & { Color: Color3 })
|
||||||
|
return Background {
|
||||||
|
Color = props.Color,
|
||||||
|
Layout = props.Layout
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we created a nested group with the key `Layout` that can accept
|
||||||
|
layout-related properties. Any name could be chosen for the key.
|
||||||
|
This allows us to write much more concise syntax that is also typecheckable.
|
||||||
|
|
||||||
|
In another example we use a key named `Children` to pass arrays of instances to
|
||||||
|
be parented.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Children = {
|
||||||
|
-- allows us to also optionally pass a source that returns an array of children instead
|
||||||
|
Children = Array<Instance> | () -> Array<Instance>
|
||||||
|
}
|
||||||
|
|
||||||
|
local function List(props: Children & Layout)
|
||||||
|
return create "Frame" {
|
||||||
|
props.Children,
|
||||||
|
props.Layout,
|
||||||
|
create "UIListLayout" {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
List {
|
||||||
|
Layout = {
|
||||||
|
Position = UDim2.new()
|
||||||
|
},
|
||||||
|
|
||||||
|
Children = {
|
||||||
|
create "TextLabel" { Text = "1" },
|
||||||
|
create "TextLabel" { Text = "2" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Deeper nested properties are guaranteed to be set after shallower nested
|
||||||
|
properties, this can be used to create overridable default properties.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function List(props: Children & Layout)
|
||||||
|
return create "Frame" {
|
||||||
|
props.Children,
|
||||||
|
props.Layout,
|
||||||
|
|
||||||
|
-- can be overriden by `props.Layout`
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
|
Position = UDim2.fromScale(0.5, 0),
|
||||||
|
|
||||||
|
create "UIListLayout" {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
# 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.
|
which will be called when those properties are being processed with the instance
|
||||||
|
being assigned to, allowing you to run custom code.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "test",
|
Text = "test",
|
||||||
|
|
||||||
|
|
@ -17,24 +20,24 @@ create "TextLabel" {
|
||||||
-- will print "test"
|
-- will print "test"
|
||||||
```
|
```
|
||||||
|
|
||||||
Actions can be wrapped with functions for reuse. Below is an example of an
|
Actions can be wrapped with functions to re-use specific behaviors. Below is
|
||||||
action used to listen for property changes:
|
an example of an 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 cleanup = vide.cleanup
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
local function changed(property: string, callback: (new) -> ())
|
local function changed(property: string, callback: (new) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
|
local con = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||||
callback(instance[property])
|
callback(instance[property])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- remember to clean up the connection when the reactive scope the action
|
-- remember to clean up the connection when the reactive scope the action
|
||||||
-- is ran in is destroyed, so the instance can be garbage collected
|
-- is ran in is destroyed, so the instance can be garbage collected
|
||||||
cleanup(connection)
|
cleanup(function()
|
||||||
|
con:Disconnect()
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -44,11 +47,9 @@ local instance = create "TextBox" {
|
||||||
changed("Text", output)
|
changed("Text", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
effect(function()
|
instance.Text = "foo"
|
||||||
print(output())
|
|
||||||
end)
|
|
||||||
|
|
||||||
instance.Text = "foo" -- "foo" will be printed by the effect
|
print(output()) -- "foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
The source `output` will be updated with the new property value any time it is
|
The source `output` will be updated with the new property value any time it is
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -5,15 +5,11 @@ be set with `vide.strict = true` once when you first require Vide. Strict mode
|
||||||
will add extra safety checks and emit better error traces, particularly when
|
will add extra safety checks and emit better error traces, particularly when
|
||||||
errors occur in property bindings.
|
errors occur in property bindings.
|
||||||
|
|
||||||
Strict mode is automatically enabled when Vide is required in O0 or O1
|
|
||||||
optimization (default studio level). You can `vide.strict = false` if you do not
|
|
||||||
want this.
|
|
||||||
|
|
||||||
Strict mode will run derived sources and effects twice each time they update.
|
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 identify improper cleanup of side-effects and ensure that pure
|
||||||
cleanups made in derived sources or effects are done properly.
|
computations are actually pure.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -23,7 +19,6 @@ local count = source(0)
|
||||||
|
|
||||||
local ran = 0
|
local ran = 0
|
||||||
effect(function()
|
effect(function()
|
||||||
count()
|
|
||||||
ran += 1
|
ran += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
# Concepts Summary
|
|
||||||
|
|
||||||
A summary of all the concepts covered during the crash course.
|
|
||||||
|
|
||||||
## Source
|
|
||||||
|
|
||||||
A source of data.
|
|
||||||
|
|
||||||
Stores a single value that can be updated.
|
|
||||||
|
|
||||||
Created with `source()`.
|
|
||||||
|
|
||||||
## Derived Source
|
|
||||||
|
|
||||||
A new source composed of other sources.
|
|
||||||
|
|
||||||
Created with a plain function or with `derive()`.
|
|
||||||
|
|
||||||
## Effect
|
|
||||||
|
|
||||||
Anything that happens in response to a source update.
|
|
||||||
|
|
||||||
Created with `effect()`.
|
|
||||||
|
|
||||||
## Stable Scope
|
|
||||||
|
|
||||||
One of the two types of Vide scopes.
|
|
||||||
|
|
||||||
Created by:
|
|
||||||
|
|
||||||
- `root()`
|
|
||||||
- `untrack()`
|
|
||||||
- `show()`
|
|
||||||
- `indexes()`
|
|
||||||
|
|
||||||
Stable scopes do not track sources and never rerun.
|
|
||||||
|
|
||||||
New stable or reactive scopes can be created within a stable scope.
|
|
||||||
|
|
||||||
## Reactive Scope
|
|
||||||
|
|
||||||
Created by:
|
|
||||||
|
|
||||||
- `effect()`
|
|
||||||
- `derive()`
|
|
||||||
|
|
||||||
Reactive scopes do track sources and will rerun when those sources update.
|
|
||||||
|
|
||||||
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
|
||||||
can be created within a reactive scope.
|
|
||||||
|
|
||||||
## Scope Cleanup
|
|
||||||
|
|
||||||
When a scope is rerun or destroyed, all scopes created within it are
|
|
||||||
automatically destroyed.
|
|
||||||
|
|
||||||
Any functions queued by `cleanup()` are also ran.
|
|
||||||
|
|
||||||
## Reactive Graph
|
|
||||||
|
|
||||||
The combination of stable and reactive scopes can viewed graphically, called a
|
|
||||||
*reactive graph*. This can be a more intuitive way to think of the
|
|
||||||
relationships between effects and the sources they depend on.
|
|
||||||
|
|
||||||
### Code
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
root(function()
|
|
||||||
local text = derive(function()
|
|
||||||
return "count: " .. count()
|
|
||||||
end)
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
print(text())
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Graph resulting from code
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%%{init: {
|
|
||||||
"theme": "base",
|
|
||||||
"themeVariables": {
|
|
||||||
"primaryColor": "#111720",
|
|
||||||
"primaryTextColor": "#fff",
|
|
||||||
"primaryBorderColor": "#111720",
|
|
||||||
"lineColor": "#79B8FF",
|
|
||||||
"tertiaryColor": "#0d131b",
|
|
||||||
"tertiaryBorderColor": "#202530"
|
|
||||||
}
|
|
||||||
}}%%
|
|
||||||
|
|
||||||
graph LR
|
|
||||||
|
|
||||||
subgraph root
|
|
||||||
text --> effect
|
|
||||||
end
|
|
||||||
|
|
||||||
count --> text
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Since `count` is a source, not an effect, it can exist
|
|
||||||
outside of scopes.
|
|
||||||
- An update to `count` will cause `text` to rerun, which
|
|
||||||
then causes `effect` to rerun.
|
|
||||||
- When the root scope is destroyed, `text` and
|
|
||||||
`effect` will be destroyed alongside it, since they were created within it.
|
|
||||||
`count` will be untouched and future updates to `count` will have no effect.
|
|
||||||
|
|
@ -2,37 +2,62 @@
|
||||||
|
|
||||||
Instances are created using `create()`.
|
Instances are created using `create()`.
|
||||||
|
|
||||||
Parentheses `()` can be omitted when calling functions with string or
|
`create()` returns a constructor for a class which then takes a table of
|
||||||
table literals for brevity.
|
properties to assign when creating a new instance for that class.
|
||||||
|
|
||||||
```luau
|
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||||
|
table literals which Vide takes advantage of for brevity.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local mount = vide.mount
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
return create "ScreenGui" {
|
local function App()
|
||||||
create "Frame" {
|
return create "ScreenGui" {
|
||||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
create "Frame" {
|
||||||
Position = UDim2.fromScale(0.5, 0.5),
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
Size = UDim2.fromScale(0.4, 0.7),
|
Position = UDim2.fromScale(0.5, 0.5),
|
||||||
|
Size = UDim2.fromScale(0.4, 0.7),
|
||||||
|
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "hi"
|
Text = "hi"
|
||||||
},
|
},
|
||||||
|
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "bye"
|
Text = "bye"
|
||||||
},
|
},
|
||||||
|
|
||||||
create "TextButton" {
|
create "TextButton" {
|
||||||
Text = "click me",
|
Text = "click me",
|
||||||
|
|
||||||
Activated = function()
|
Activated = function()
|
||||||
print "clicked!"
|
print "clicked!"
|
||||||
end
|
end
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
```
|
```
|
||||||
|
|
||||||
Assign a value to a string key to set a property, and assign a value to a
|
Assign a value to a string key to set a property, and assign a value to a
|
||||||
number key to set a child. Events can be connected to by assigning a function
|
number key to set a child. Events can be connected to by assigning a function
|
||||||
to a string key.
|
to a string key.
|
||||||
|
|
||||||
|
You can also use a shorthand to create datatypes instead of explicitly typing
|
||||||
|
out the class name and constructor. The table will be unpacked into the `.new()`
|
||||||
|
constructor of the property's type.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
create "Frame" {
|
||||||
|
AnchorPoint = { 0.5, 1 },
|
||||||
|
UDim2 = { 0.5, 0, 0.5, 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When creating an instance with no properties, it is important to not forget to
|
||||||
|
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||||
|
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||||
|
that class, not an instance of that class. This would result in you attempting
|
||||||
|
to parent a function instead of an instance which is not the correct behavior.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
# Components
|
# Components
|
||||||
|
|
||||||
Vide encourages separating different parts of your UI into functions called
|
Components are custom-made reusable pieces of UI made from other pieces of UI.
|
||||||
*components*.
|
|
||||||
|
|
||||||
A component is a function that creates and returns a piece of UI.
|
By using components you can make your application more modular and better
|
||||||
|
organized.
|
||||||
|
|
||||||
This is a way to separate your UI into small chunks that you can reuse and put
|
```lua [Button.luau]
|
||||||
together.
|
|
||||||
|
|
||||||
::: code-group
|
|
||||||
|
|
||||||
```luau [Button.luau]
|
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local function Button(props: {
|
local function Button(props: {
|
||||||
|
|
@ -20,46 +15,50 @@ local function Button(props: {
|
||||||
})
|
})
|
||||||
return create "TextButton" {
|
return create "TextButton" {
|
||||||
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
|
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
|
||||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
|
||||||
Size = UDim2.fromOffset(200, 150),
|
Size = UDim2.fromOffset(200, 150),
|
||||||
|
|
||||||
Position = props.Position,
|
Position = props.Position,
|
||||||
Text = props.Text,
|
Text = props.Text,
|
||||||
Activated = props.Activated,
|
Activated = props.Activated
|
||||||
|
|
||||||
create "UICorner" {}
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return Button
|
return Button
|
||||||
```
|
```
|
||||||
|
|
||||||
```luau [Menu.luau]
|
```lua [App.luau]
|
||||||
|
local mount = vide.mount
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local Button = require(Button)
|
local Button = require(Button)
|
||||||
|
|
||||||
local function Menu()
|
local function App()
|
||||||
return create "ScreenGui" {
|
return create "ScreenGui" {
|
||||||
Button {
|
Button {
|
||||||
Position = UDim2.fromOffset(200, 200),
|
Position = UDim2.fromOffset(200, 200),
|
||||||
Text = "back",
|
Text = "click me!",
|
||||||
Activated = function()
|
|
||||||
print "go to previous page"
|
|
||||||
end
|
|
||||||
},
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Position = UDim2.fromOffset(400, 200),
|
|
||||||
Text = "next",
|
|
||||||
Activated = function()
|
Activated = function()
|
||||||
print "go to next page"
|
print "clicked"
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
```
|
```
|
||||||
|
|
||||||
:::
|
Above is a simple example of a button component with a set color and size,
|
||||||
|
being reused across files.
|
||||||
|
|
||||||
A single parameter `props` is used to pass properties to the component.
|
A single parameter `props` is used to pass properties to the component.
|
||||||
|
|
||||||
|
Components allow you to *encapsulate* behavior. You can only modify the
|
||||||
|
component in ways that you allow in the component, through the `props` parameter.
|
||||||
|
|
||||||
|
To create a new button all you must do is call the `Button` function, passing in
|
||||||
|
values through props. This saves having to create and set every property each
|
||||||
|
time. Also, when updating the button component in future, any changes to the
|
||||||
|
button file will be seen anywhere the button is used in your app.
|
||||||
|
|
||||||
|
This can be extended to much more complicated UI.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
# Sources
|
# Source
|
||||||
|
|
||||||
Sources are special objects that store a single value and are the core of
|
*Sources* in Vide are special objects that store a single value. They are the
|
||||||
Vide's reactivity.
|
core of reactivity in Vide. Each source represents a source of data, and they
|
||||||
|
can be composed and derived to create new sources of data.
|
||||||
|
|
||||||
A source can be created using `source()`.
|
A source in Vide 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 +17,14 @@ The value passed to `source()` is the initial value of the source.
|
||||||
The value of a source can be set by calling it with an argument, and can be read
|
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. A wrapped source
|
||||||
|
effectively becomes a new source.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
local text = function()
|
local text = function()
|
||||||
|
|
@ -34,5 +36,8 @@ count(1)
|
||||||
print(text()) -- "count: 1"
|
print(text()) -- "count: 1"
|
||||||
```
|
```
|
||||||
|
|
||||||
While the above can be achieved with plain variables, the use for sources will
|
You may be wondering why we are using sources instead of plain variables to do
|
||||||
be obvious in the next part.
|
this. The reason is that Vide has an entire reactive system based on sources.
|
||||||
|
You can write functions to automatically run each time a source is updated. This
|
||||||
|
can be to update properties, create new instances, print to the terminal, etc.
|
||||||
|
How this is done will be covered next.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Effects
|
# Effect
|
||||||
|
|
||||||
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.
|
alled effects because they cause *side-effects* when reacting to source updates.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -20,16 +20,17 @@ count(1)
|
||||||
-- "count: 1" printed
|
-- "count: 1" printed
|
||||||
```
|
```
|
||||||
|
|
||||||
Any source read inside an effect is tracked and will rerun the effect when
|
The callback given to `effect()` is ran in a *reactive-scope*. Any source read
|
||||||
that source is updated.
|
from inside a reactive scope will be tracked, so that if any of those sources
|
||||||
|
update, the effect will be re-ran too.
|
||||||
|
|
||||||
The effect runs its callback once immediately to initially figure out what
|
The callback is first ran immediately inside the `effect()` call to initially
|
||||||
sources are being read.
|
figure out what sources are being used.
|
||||||
|
|
||||||
Derived sources are also tracked, it does not matter how deeply nested
|
Effects also work with derived sources, it doesn't matter how deeply nested a
|
||||||
inside a function a source is.
|
source is.
|
||||||
|
|
||||||
```luau
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
|
|
@ -48,24 +49,27 @@ count(2)
|
||||||
-- "doubled count: 4" printed
|
-- "doubled count: 4" printed
|
||||||
```
|
```
|
||||||
|
|
||||||
If a source is updated with the same value it already had, it will not rerun
|
Derived sources should be a *pure computation*. A pure computation is one where
|
||||||
effects depending on it.
|
the same input will always produce the same output.
|
||||||
|
|
||||||
You can also read from a source within an effect without the effect tracking it.
|
All observable changes to the user are considered to be side-effects of pure
|
||||||
|
computations.
|
||||||
|
|
||||||
```luau
|
Sources, derived sources, and effects form what is called a *reactive graph*.
|
||||||
local source = vide.source
|
In the above example a graph `count -> doubled -> effect` is formed. Anywhere
|
||||||
local effect = vide.effect
|
an update occures, everything further down the graph is updated.
|
||||||
local untrack = vide.untrack
|
|
||||||
|
|
||||||
local a = source(0)
|
You should not update other sources using an effect. Improper usage can lead to
|
||||||
local b = source(0)
|
a cyclic loop in the graph, causing an infinite loop when it tries to update.
|
||||||
|
Sources should be derived instead.
|
||||||
|
|
||||||
effect(function()
|
## Root Reactive Scopes
|
||||||
print(`a: {a()} b: {untrack(b)}`)
|
|
||||||
end)
|
|
||||||
|
|
||||||
a(1) -- prints "a: 1 b: 0"
|
Effects must be created within another reactive scope. This is so that the
|
||||||
b(1) -- prints nothing
|
effect itself can be tracked and later freed when the parent reactive scope is
|
||||||
a(2) -- prints "a: 2 b: 1"
|
destroyed, such as from unmounting an app. The example code above will not
|
||||||
```
|
actually work unless it is ran inside a root reactive scope, such as one created
|
||||||
|
by `vide.mount(function)`. This generally isn't a concern since you can assume
|
||||||
|
that all your components will be created within a single `mount()` call, which
|
||||||
|
happens only once at the top level, where you put together your UI and parent it
|
||||||
|
to a ScreenGUI.
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
# Scopes
|
|
||||||
|
|
||||||
Just like how a signal's connection may need to be disconnected, a source's
|
|
||||||
effect also may need to be disconnected.
|
|
||||||
|
|
||||||
But the disconnecting of many signals and connections is tedious and verbose.
|
|
||||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
|
||||||
API, given that you follow a few rules.
|
|
||||||
|
|
||||||
There are two types of scopes: stable and reactive.
|
|
||||||
|
|
||||||
- A scope must be created within another scope.
|
|
||||||
- Stable scopes never rerun.
|
|
||||||
- Reactive scopes can rerun.
|
|
||||||
- A reactive scope cannot be created within another reactive scope, only within
|
|
||||||
a stable scope.
|
|
||||||
|
|
||||||
An exception to the first rule is `root()`, which creates the initial scope that
|
|
||||||
you destroy manually with a destructor function it returns.
|
|
||||||
|
|
||||||
`root()` creates a stable scope.
|
|
||||||
`effect()` creates a reactive scope.
|
|
||||||
|
|
||||||
Whenever a scope is destroyed, any scope created within that scope is also
|
|
||||||
destroyed, and so on.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local root = vide.root
|
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
|
||||||
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local function setup()
|
|
||||||
effect(function()
|
|
||||||
print(count())
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
setup() -- error, effect() tried to create a reactive scope with no stable scope
|
|
||||||
|
|
||||||
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
|
||||||
|
|
||||||
count(1) -- prints "1"
|
|
||||||
count(2) -- prints "2"
|
|
||||||
|
|
||||||
destroy()
|
|
||||||
|
|
||||||
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
|
|
||||||
```
|
|
||||||
|
|
||||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
|
||||||
|
|
||||||
The reactive graph for the above example looks like so:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%%{init: {
|
|
||||||
"theme": "base",
|
|
||||||
"themeVariables": {
|
|
||||||
"primaryColor": "#111720",
|
|
||||||
"primaryTextColor": "#fff",
|
|
||||||
"primaryBorderColor": "#111720",
|
|
||||||
"lineColor": "#79B8FF",
|
|
||||||
"tertiaryColor": "#0d131b",
|
|
||||||
"tertiaryBorderColor": "#0d131b"
|
|
||||||
}
|
|
||||||
}}%%
|
|
||||||
|
|
||||||
graph
|
|
||||||
|
|
||||||
subgraph root
|
|
||||||
direction LR
|
|
||||||
count --> effect
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
When the stable `root()` scope is destroyed, the reactive `effect()`
|
|
||||||
scope will also be destroyed since it was created within it.
|
|
||||||
|
|
||||||
This is important because you may have an effect that updates the property of a
|
|
||||||
UI instance, meaning the effect is referencing and holding that instance in
|
|
||||||
memory. The effect being destroyed will remove this reference, allowing the
|
|
||||||
instance to be garbage collected.
|
|
||||||
|
|
||||||
You don't need to worry about ensuring all your effects are created within a
|
|
||||||
stable scope, since you should be creating all your UI and effects within a
|
|
||||||
single top-level `root()` call that puts all your UI together, making it safe to
|
|
||||||
assume any effect created will be created under this stable scope.
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
# Reactive Components
|
# Stateful Component
|
||||||
|
|
||||||
Reactive components in Vide are created using sources and effects - sources to
|
A stateful component is a component that stores and displays some data.
|
||||||
|
|
||||||
|
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
|
```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
|
||||||
|
|
@ -21,7 +23,7 @@ local function Counter()
|
||||||
instance.Text = "count: " .. count()
|
instance.Text = "count: " .. count()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return instance
|
return count
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -29,32 +31,36 @@ Above is an example of a counter component, that when clicked, will increment
|
||||||
its internal count, and automatically update its text to reflect that count.
|
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 scope of the component.
|
||||||
|
|
||||||
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()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return instance
|
return count
|
||||||
end
|
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
|
||||||
restrictions on how they are used as long as the effect using it is created
|
restrictions on how they are used as long as the effect is created within a
|
||||||
within a stable scope.
|
reactive scope so that it can be tracked.
|
||||||
64
docs/tut/crash-course/7-property-binding.md
Normal file
64
docs/tut/crash-course/7-property-binding.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Property Binding
|
||||||
|
|
||||||
|
Explicitly creating effects to update properties can become verbose when there
|
||||||
|
are a lot of properties to update. Vide provides a way to *implicitly* create
|
||||||
|
an effect to update properties on source update. This is also known as
|
||||||
|
*property binding*, as a property is binded to reflect some data.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local create = vide.create
|
||||||
|
local source = vide.source
|
||||||
|
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
return create "TextButton" {
|
||||||
|
Text = function()
|
||||||
|
return "count: " .. count()
|
||||||
|
end,
|
||||||
|
|
||||||
|
Activated = function()
|
||||||
|
count(count() + 1)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example is equivalent to the example seen on the previous page.
|
||||||
|
|
||||||
|
Instead of explicitly creating an effect, assigning a (non-event) property
|
||||||
|
a function will implicitly create a side-effect to update that property anytime
|
||||||
|
a dependent source is updated.
|
||||||
|
|
||||||
|
Just like effects, the function is ran immediately in a reactive-scope to set
|
||||||
|
the property initially and determine what sources are being depended on.
|
||||||
|
|
||||||
|
This allows you as the programmer to not need to manually update UI as the state
|
||||||
|
of your program changes. You just define how the data maps to UI, and Vide's
|
||||||
|
reactive system will automatically update any properties depending on sources
|
||||||
|
that are updated.
|
||||||
|
|
||||||
|
## Children Binding
|
||||||
|
|
||||||
|
Children can also be set in a similar manner.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local items = source {
|
||||||
|
create "TextLabel" { Text = "A" }
|
||||||
|
}
|
||||||
|
|
||||||
|
local function List(props: { children: () -> { Instance } })
|
||||||
|
return create "Frame" {
|
||||||
|
create "UIListLayout" {},
|
||||||
|
props.children
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local list = List { children = items } -- creates a list with a single text label "A"
|
||||||
|
|
||||||
|
items {
|
||||||
|
create "TextLabel" { Text = "B" },
|
||||||
|
create "TextLabel" { Text = "C" }
|
||||||
|
}
|
||||||
|
|
||||||
|
-- this will automatically unparent the text label "A", and parent the labels "B" and "C".
|
||||||
|
```
|
||||||
48
docs/tut/crash-course/8-cleanup.md
Normal file
48
docs/tut/crash-course/8-cleanup.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Cleanup
|
||||||
|
|
||||||
|
Sometimes you may need to do some cleanup when destroying a component or after
|
||||||
|
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||||
|
is used to register a cleanup callback for the next time the reactive scope
|
||||||
|
it is called in re-runs.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
locla mount = vide.mount
|
||||||
|
local source = vide.source
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
|
local function Timer()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||||
|
count(count() + dt)
|
||||||
|
end)
|
||||||
|
|
||||||
|
cleanup(function()
|
||||||
|
con:Disconnect()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return create "TextButton" {
|
||||||
|
Position = UDim2.fromOffset(300, 300),
|
||||||
|
Size = UDim2.fromOffset(200, 50),
|
||||||
|
|
||||||
|
Text = function()
|
||||||
|
return "seconds: " .. math.floor(count())
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local unmount = mount(Timer)
|
||||||
|
|
||||||
|
unmount() -- all registered cleanups are ran, heartbeat connection stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, this allows us to disconnect the heartbeat connection
|
||||||
|
when the timer component is destroyed, whether that is from unmounting the app
|
||||||
|
or if it is dynamically created by a control-flow function, which will be
|
||||||
|
covered next.
|
||||||
|
|
||||||
|
On a related note: the reason why `mount()` is used to create your app, is so
|
||||||
|
that any top-level components that need to be cleaned up, can be cleaned up
|
||||||
|
when the app is later unmounted, since `mount()` runs in a reactive-scope to
|
||||||
|
track `cleanup()` calls. Vide's entire reactive system is independent from the
|
||||||
|
life-time of instances; instances are just a side-effect of the reactive system.
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
# Implicit Effects
|
|
||||||
|
|
||||||
Explicitly creating effects to update properties is tedious. You can
|
|
||||||
*implicitly* create an effect to update properties instead.
|
|
||||||
|
|
||||||
::: code-group
|
|
||||||
|
|
||||||
```luau [Implicit Effect]
|
|
||||||
local create = vide.create
|
|
||||||
local source = vide.source
|
|
||||||
|
|
||||||
local function Counter()
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
return create "TextButton" {
|
|
||||||
Activated = function()
|
|
||||||
count(count() + 1)
|
|
||||||
end,
|
|
||||||
|
|
||||||
Text = function()
|
|
||||||
return "count: " .. count()
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```luau [Explicit Effect]
|
|
||||||
local create = vide.create
|
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
|
||||||
|
|
||||||
local function Counter()
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local instance = create "TextButton" {
|
|
||||||
Activated = function()
|
|
||||||
count(count() + 1)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
instance.Text = "count: " .. count()
|
|
||||||
end)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
This example is equivalent to the example seen on the previous page.
|
|
||||||
|
|
||||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
|
||||||
function will implicitly create an effect to update that property.
|
|
||||||
|
|
||||||
## Children
|
|
||||||
|
|
||||||
Children can also be set in a similar manner. A source passed as a child (passed
|
|
||||||
with a number key instead of string key) can return an instance or an array of
|
|
||||||
instances. An effect is automatically created to unparent removed instances and
|
|
||||||
parent new instances on source update.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local items = source {
|
|
||||||
create "TextLabel" { Text = "A" }
|
|
||||||
}
|
|
||||||
|
|
||||||
local function List(props: { children: () -> { Instance } })
|
|
||||||
return create "Frame" {
|
|
||||||
create "UIListLayout" {},
|
|
||||||
props.children
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local list = List { children = items } -- creates a list with text label "A"
|
|
||||||
|
|
||||||
items {
|
|
||||||
create "TextLabel" { Text = "B" },
|
|
||||||
create "TextLabel" { Text = "C" }
|
|
||||||
}
|
|
||||||
|
|
||||||
-- this will automatically unparent text label "A", and parent labels "B" and "C"
|
|
||||||
```
|
|
||||||
157
docs/tut/crash-course/9-control-flow.md
Normal file
157
docs/tut/crash-course/9-control-flow.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# Control Flow
|
||||||
|
|
||||||
|
Eventually you will need a way to dynamically create and destroy UI elements
|
||||||
|
resulting from source updates. Vide provides functions to help you do this,
|
||||||
|
known as *control flow* functions.
|
||||||
|
|
||||||
|
These functions return new sources, which hold the instances to be displayed.
|
||||||
|
These sources can be assigned as children, meaning the displayed children
|
||||||
|
will update when the input source updates.
|
||||||
|
|
||||||
|
Control flow functions are special, because they run their components in a new
|
||||||
|
reactive scope, which can be destroyed independently of the reactive scope that
|
||||||
|
called the control flow function itself. This means that parts of your app can
|
||||||
|
be independently created then destroyed and cleaned.
|
||||||
|
|
||||||
|
## show()
|
||||||
|
|
||||||
|
The most basic control flow function is `show()`, which is used to conditionally
|
||||||
|
show a component.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local source = vide.source
|
||||||
|
local show = vide.show
|
||||||
|
|
||||||
|
local function JoinMenu()
|
||||||
|
local joined = source(false)
|
||||||
|
|
||||||
|
local function JoinButton()
|
||||||
|
return Button {
|
||||||
|
Activated = function() joined(true) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return create "Frame" {
|
||||||
|
show(function() return not joined() end, JoinButton)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This will make a button to join if you have not joined already.
|
||||||
|
|
||||||
|
You can also pass a third argument, a fallback to show if the condition is falsey.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function JoinMenu()
|
||||||
|
local joined = source(false)
|
||||||
|
|
||||||
|
local function JoinButton()
|
||||||
|
return Button {
|
||||||
|
Activated = function() joined(true) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function LeaveButton()
|
||||||
|
return Button {
|
||||||
|
Activated = function() joined(false) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return create "Frame" {
|
||||||
|
show(joined, LeaveButton, JoinButton)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## switch()
|
||||||
|
|
||||||
|
Similar to `show()`, `switch()`, also condtionally displays one instance at a
|
||||||
|
time. It is more flexible since it can show one of many components, based on a
|
||||||
|
table used to map a source value to a component.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local source = vide.source
|
||||||
|
local switch = vide.switch
|
||||||
|
|
||||||
|
local function JoinMenu()
|
||||||
|
local joined = source(false)
|
||||||
|
|
||||||
|
local function JoinButton()
|
||||||
|
return Button {
|
||||||
|
Activated = function() joined(true) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function LeaveButton()
|
||||||
|
return Button {
|
||||||
|
Activated = function() joined(false) end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return create "Frame" {
|
||||||
|
switch(joined) {
|
||||||
|
[true] = LeaveButton,
|
||||||
|
[false] = JoinButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This example is equivalent to the previous one.
|
||||||
|
|
||||||
|
The switch can map any value to any component.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type ActiveMenu = "none" | "inventory" | "shop" | "settings"
|
||||||
|
|
||||||
|
local menu = source "none"
|
||||||
|
|
||||||
|
switch(menu) {
|
||||||
|
inventory = InventoryMenu,
|
||||||
|
shop = ShopMenu.
|
||||||
|
settings = SettingsMenu
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## indexes()
|
||||||
|
|
||||||
|
Often, you will have a table of values that will be displayed in a similar
|
||||||
|
manner. Rather than manually looping over each value to generate a corresponding
|
||||||
|
UI element, `indexes()` allows you to create an instance for each table index,
|
||||||
|
to display the value at that index.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local todoList = {
|
||||||
|
"finish the crash course",
|
||||||
|
"star vide's GitHub"
|
||||||
|
}
|
||||||
|
|
||||||
|
local function TodoList(props: { list: () -> Array<string> })
|
||||||
|
return create "Frame" {
|
||||||
|
create "UIListLayout" {},
|
||||||
|
|
||||||
|
indexes(todoList, function(todo, i)
|
||||||
|
return create "TextLabel" {
|
||||||
|
Text = function()
|
||||||
|
return i .. ": " .. todo()
|
||||||
|
end,
|
||||||
|
|
||||||
|
LayoutOrder = i
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
TodoList { list = todoList }
|
||||||
|
```
|
||||||
|
|
||||||
|
For each unique index in the passed table, the transform function will be called
|
||||||
|
with 1. a source containing the value of the index, 2. the index itself.
|
||||||
|
|
||||||
|
When the value at an index is changed, the function is not reran. Instead, the
|
||||||
|
given source for that index is updated.
|
||||||
|
|
||||||
|
An element is only destroyed if the value of an index is set to `nil`.
|
||||||
|
|
||||||
|
Together, these control flow functions cover the majority of cases where you
|
||||||
|
need to dynamically create and destroy parts of your UI.
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# Derived Sources
|
|
||||||
|
|
||||||
We have seen the basic way to derive a source:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local text = function()
|
|
||||||
return "count: " .. tostring(count())
|
|
||||||
end
|
|
||||||
|
|
||||||
print(text()) -- "count: 0"
|
|
||||||
count(1)
|
|
||||||
print(text()) -- "count: 1"
|
|
||||||
```
|
|
||||||
|
|
||||||
However, in some cases where this source could be used by multiple effects at
|
|
||||||
the same time, the function wrapping the source will needlessly rerun to convert
|
|
||||||
the count into a string for each effect using it.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
|
||||||
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local text = function()
|
|
||||||
print "ran"
|
|
||||||
return "count: " .. tostring(count())
|
|
||||||
end
|
|
||||||
|
|
||||||
effect(function() text() end)
|
|
||||||
effect(function() text() end)
|
|
||||||
|
|
||||||
count(1) -- prints "ran" x2
|
|
||||||
```
|
|
||||||
|
|
||||||
To avoid this, you can use `derive()` to derive a new source instead. This will
|
|
||||||
run a function in a reactive scope only when a source used inside updated.
|
|
||||||
Reading this derived source multiple times will just return a cached result.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local source = vide.source
|
|
||||||
local effect = vide.effect
|
|
||||||
local derive = vide.derive
|
|
||||||
|
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
local text = derive(function()
|
|
||||||
print "ran"
|
|
||||||
return "count: " .. tostring(count())
|
|
||||||
end)
|
|
||||||
|
|
||||||
effect(function() text() end)
|
|
||||||
effect(function() text() end)
|
|
||||||
|
|
||||||
count(1) -- prints "ran" x1
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `derive()` creates a reactive scope, it must be called within a stable
|
|
||||||
scope, just like `effect()`.
|
|
||||||
|
|
||||||
If the recalculated value is the same as the old value, the derived source will
|
|
||||||
not rerun the effects using it.
|
|
||||||
|
|
||||||
The reactive graph for the above example:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%%{init: {
|
|
||||||
"theme": "base",
|
|
||||||
"themeVariables": {
|
|
||||||
"primaryColor": "#111720",
|
|
||||||
"primaryTextColor": "#fff",
|
|
||||||
"primaryBorderColor": "#111720",
|
|
||||||
"lineColor": "#79B8FF",
|
|
||||||
"tertiaryColor": "#0d131b",
|
|
||||||
"tertiaryBorderColor": "#0d131b"
|
|
||||||
}
|
|
||||||
}}%%
|
|
||||||
|
|
||||||
graph
|
|
||||||
|
|
||||||
subgraph root
|
|
||||||
direction LR
|
|
||||||
count --> text --> effect1 & effect2
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Deriving a source in this manner is similar to creating an effect to update
|
|
||||||
another source. You should avoid doing this using an effect however.
|
|
||||||
Improper usage could accidently create infinite loops in the reactive graph.
|
|
||||||
Always favour deriving when you need one source to update based on another
|
|
||||||
source.
|
|
||||||
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"
|
|
||||||
|
|
@ -10,14 +10,14 @@ local function is_action(v: any)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action(callback: (Instance) -> (), priority: number?): Action
|
local function action(callback: (Instance) -> (), priority: number?): Action
|
||||||
local a = {
|
local t = {
|
||||||
priority = priority or 1,
|
priority = priority or 1,
|
||||||
callback = callback
|
callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(a :: any, ActionMT)
|
setmetatable(t :: any, ActionMT)
|
||||||
|
|
||||||
return table.freeze(a)
|
return table.freeze(t)
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
|
||||||
234
src/apply.luau
234
src/apply.luau
|
|
@ -1,161 +1,161 @@
|
||||||
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 Map<K, V> = { [K]: V }
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
type Cache = {
|
-- buffer of event -> callback to connect after properties are set
|
||||||
-- event listeners to connect after properties are set
|
local event_buffer = {} :: Map<string, () -> ()>
|
||||||
events: Array<
|
|
||||||
| string -- 1. event name
|
|
||||||
| () -> () -- 2. listener
|
|
||||||
>,
|
|
||||||
|
|
||||||
-- actions to run after events are connected
|
-- buffer of priority -> callback to run after events are connected
|
||||||
actions: Map<
|
local action_buffers = {} :: Map<number, Array<(Instance) -> ()>>
|
||||||
number, -- priority
|
|
||||||
Array<(Instance) -> ()> -- action callbacks
|
|
||||||
>,
|
|
||||||
|
|
||||||
-- what to parent the instance to after running actions
|
-- lazily create buffers on nil index
|
||||||
parent: unknown,
|
setmetatable(action_buffers :: any, {
|
||||||
|
__index = function(_, i: number)
|
||||||
-- cache to detect duplicate property setting at same nesting depth
|
action_buffers[i] = {}
|
||||||
nested_debug: Map<
|
return action_buffers[i]
|
||||||
number, -- depth
|
|
||||||
Map<string, true> -- set of property names
|
|
||||||
>,
|
|
||||||
|
|
||||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
|
||||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
|
||||||
nested_stack: { {} | number }
|
|
||||||
}
|
|
||||||
|
|
||||||
local free_cache: Cache?
|
|
||||||
|
|
||||||
local function borrow_cache(): Cache
|
|
||||||
if free_cache then
|
|
||||||
local cache = free_cache
|
|
||||||
free_cache = nil
|
|
||||||
return cache
|
|
||||||
else
|
|
||||||
return {
|
|
||||||
events = {},
|
|
||||||
actions = setmetatable({} :: any, { -- lazy init
|
|
||||||
__index = function(self, i) self[i] = {}; return self[i] end
|
|
||||||
}),
|
|
||||||
parent = nil,
|
|
||||||
nested_debug = setmetatable({} :: any, {
|
|
||||||
__index = function(self, i: number) self[i] = {}; return self[i] end
|
|
||||||
}),
|
|
||||||
nested_stack = {}
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- cache in strict mode to detect duplicate property set at same nesting level
|
||||||
|
local nested_debug_cache = {} :: Map<number, Map<string, true>>
|
||||||
|
|
||||||
|
setmetatable(nested_debug_cache :: any, {
|
||||||
|
__index = function(_, i: number)
|
||||||
|
nested_debug_cache[i] = {}
|
||||||
|
return nested_debug_cache[i]
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- use stack instead of recursive function to process nested layers one at time
|
||||||
|
-- deeper-nested properties take precedence over shallower-nested ones
|
||||||
|
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||||
|
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||||
|
local nested_stack = {} :: { {} | number }
|
||||||
|
|
||||||
|
-- todo: solution without manual updating of this table
|
||||||
|
-- map of datatype names to class default constructor for aggregate init
|
||||||
|
local aggregates = {}
|
||||||
|
|
||||||
|
for i, v in next, {
|
||||||
|
CFrame = CFrame,
|
||||||
|
Color3 = Color3,
|
||||||
|
UDim = UDim,
|
||||||
|
UDim2 = UDim2,
|
||||||
|
Vector2 = Vector2,
|
||||||
|
Vector3 = Vector3,
|
||||||
|
Rect = Rect
|
||||||
|
} do
|
||||||
|
aggregates[i] = v.new
|
||||||
end
|
end
|
||||||
|
|
||||||
local function return_cache(cache: Cache )
|
-- processes a potentially nested table of values to assign to an instance
|
||||||
free_cache = cache
|
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
|
||||||
end
|
local strict = flags.strict
|
||||||
|
|
||||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
table.clear(nested_stack)
|
||||||
for property, value in properties do
|
if strict then table.clear(nested_debug_cache) end
|
||||||
if type(property) == "string" then
|
|
||||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
local depth = 1
|
||||||
if cache.nested_debug[depth][property] then
|
|
||||||
error(`duplicate property {property} at depth {depth}`, 0)
|
repeat
|
||||||
|
for property, value in properties do
|
||||||
|
if type(property) == "string" then
|
||||||
|
if strict then -- check for duplicate prop assignment at nesting layer
|
||||||
|
if nested_debug_cache[depth][property] then
|
||||||
|
throw(`duplicate property {property} at depth {depth}`)
|
||||||
|
end
|
||||||
|
nested_debug_cache[depth][property] = true
|
||||||
end
|
end
|
||||||
cache.nested_debug[depth][property] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if property == "Parent" then
|
if type(value) == "table" then -- attempt aggregate init
|
||||||
cache.parent = value
|
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||||
continue
|
if ctor == nil then
|
||||||
end
|
throw(`cannot aggregate type {typeof(value)} for property {property}`)
|
||||||
|
end
|
||||||
if type(value) == "function" then
|
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
elseif type(value) == "function" then
|
||||||
table.insert(cache.events, property) -- add event name to buffer
|
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||||
table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
|
event_buffer[property] = value :: () -> () -- add event to buffer
|
||||||
|
else
|
||||||
|
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||||
|
end
|
||||||
else
|
else
|
||||||
implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property
|
(instance :: any)[property] = value -- set property
|
||||||
end
|
end
|
||||||
else
|
elseif type(property) == "number" then
|
||||||
(instance :: any)[property] = value -- set property
|
if type(value) == "function" then
|
||||||
end
|
bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children
|
||||||
elseif type(property) == "number" then
|
elseif type(value) == "table" then
|
||||||
if type(value) == "function" then
|
if is_action(value) then
|
||||||
implicit_effect.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||||
elseif type(value) == "table" then
|
else
|
||||||
if is_action(value) then
|
table.insert(nested_stack, value :: {})
|
||||||
table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||||
elseif flags.defer_nested_properties then
|
end
|
||||||
table.insert(cache.nested_stack, value :: {})
|
|
||||||
table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
|
|
||||||
else
|
else
|
||||||
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
|
(value :: Instance).Parent = instance -- parent child
|
||||||
end
|
end
|
||||||
elseif type(value) == "userdata" then
|
|
||||||
(value :: Instance).Parent = instance -- parent child
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
-- pop next nested table off stack
|
||||||
|
depth = table.remove(nested_stack) :: number
|
||||||
|
properties = table.remove(nested_stack) :: {}
|
||||||
|
|
||||||
|
until not properties
|
||||||
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("no properties given, did you forget to call the constructor returned by create()?")
|
||||||
end
|
end
|
||||||
|
|
||||||
local caches = borrow_cache()
|
-- queue parent assignment if any for last
|
||||||
local events = caches.events
|
local parent: unknown = properties.Parent
|
||||||
local actions = caches.actions
|
if parent then properties.Parent = nil end
|
||||||
local nested_debug = caches.nested_debug
|
|
||||||
local nested_stack = caches.nested_stack
|
|
||||||
|
|
||||||
-- process all properties
|
-- reset buffers
|
||||||
local depth = 1
|
table.clear(event_buffer)
|
||||||
repeat
|
for _, buffer in next, action_buffers do
|
||||||
process_properties(properties, instance, caches, depth)
|
table.clear(buffer)
|
||||||
depth = table.remove(nested_stack) :: number
|
|
||||||
properties = table.remove(nested_stack) :: {}
|
|
||||||
until not properties
|
|
||||||
|
|
||||||
for i = 1, #events, 2 do
|
|
||||||
local event_name = events[i]
|
|
||||||
local event_listener = events[i + 1]
|
|
||||||
;(instance :: any)[event_name]:Connect(event_listener)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, queued in actions do
|
-- process all properties for immediate setting or buffering
|
||||||
for _, callback in queued do
|
process_props(instance, properties)
|
||||||
|
|
||||||
|
-- connect buffered events
|
||||||
|
for event, fn in next, event_buffer do
|
||||||
|
(instance :: any)[event]:Connect(fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- run buffered actions
|
||||||
|
for _, buffer in next, action_buffers do
|
||||||
|
for _, callback in next, buffer do
|
||||||
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
|
||||||
|
|
||||||
table.clear(events)
|
|
||||||
for _, queued in actions do table.clear(queued) end
|
|
||||||
caches.parent = nil
|
|
||||||
if flags.strict then table.clear(nested_debug) end
|
|
||||||
table.clear(nested_stack)
|
|
||||||
|
|
||||||
return_cache(caches)
|
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
local flags = require "./flags"
|
|
||||||
local graph = require "./graph"
|
|
||||||
|
|
||||||
local function batch(setter: () -> ())
|
|
||||||
local already_batching = flags.batch
|
|
||||||
local from
|
|
||||||
|
|
||||||
if not already_batching then
|
|
||||||
flags.batch = true
|
|
||||||
from = graph.get_update_queue_length()
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, err: string? = xpcall(setter, debug.traceback)
|
|
||||||
|
|
||||||
if not already_batching then
|
|
||||||
flags.batch = false
|
|
||||||
graph.flush_update_queue(from)
|
|
||||||
end
|
|
||||||
|
|
||||||
if not ok then error(`error occured while batching updates: {err}`, 0) end
|
|
||||||
end
|
|
||||||
|
|
||||||
return batch
|
|
||||||
126
src/bind.luau
Normal file
126
src/bind.luau
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local trace = require(script.Parent.trace)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local get_owning_scope = graph.get_owning_scope
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local set_owner = graph.set_owner
|
||||||
|
|
||||||
|
function create_binding<T>(updater: (T) -> T, binding: T)
|
||||||
|
if flags.strict then
|
||||||
|
-- track bind creation trace
|
||||||
|
local fn = updater
|
||||||
|
local bind_trace = debug.traceback(nil, trace()-1)
|
||||||
|
updater = function(...)
|
||||||
|
local ok, result = xpcall(fn, function(err: string)
|
||||||
|
return err
|
||||||
|
end, ...)
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
local btype =
|
||||||
|
if (binding :: any).property then (binding :: any).property
|
||||||
|
elseif (binding :: any).parent then "Parent"
|
||||||
|
else "children"
|
||||||
|
error(`PROPERTY BINDING ERROR: Property {btype}\n{result}\nBIND CREATION TRACE:\n{bind_trace}`, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
|
local node = create_node(binding, updater)
|
||||||
|
|
||||||
|
set_owner(node, owner)
|
||||||
|
evaluate_node(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
type PropertyBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
property: string,
|
||||||
|
source: () -> unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_property(p: PropertyBinding)
|
||||||
|
(p.instance :: any)[p.property] = p.source()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
type ParentBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
parent: () -> Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_parent(p: ParentBinding)
|
||||||
|
p.instance.Parent = p.parent()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
type ChildrenBinding = {
|
||||||
|
instance: Instance,
|
||||||
|
cur_children_set: { [Instance]: true },
|
||||||
|
new_children_set: { [Instance]: true },
|
||||||
|
children: () -> Instance | { Instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_children(p: ChildrenBinding)
|
||||||
|
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||||
|
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||||
|
|
||||||
|
local new_children = p.children() -- all (and only) children that should be parented after this update
|
||||||
|
|
||||||
|
if type(new_children) ~= "table" then
|
||||||
|
new_children = { new_children }
|
||||||
|
end
|
||||||
|
|
||||||
|
if new_children then
|
||||||
|
for _, child in next, new_children :: { Instance } do
|
||||||
|
new_child_set[child] = true -- record child set from this update
|
||||||
|
if not cur_children_set[child] then
|
||||||
|
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||||
|
else
|
||||||
|
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for child in next, cur_children_set do
|
||||||
|
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||||
|
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
|
||||||
|
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
property = function(instance, property, source)
|
||||||
|
return create_binding(update_property, {
|
||||||
|
instance = instance,
|
||||||
|
property = property,
|
||||||
|
source = source
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
parent = function(instance, parent)
|
||||||
|
return create_binding(update_parent, {
|
||||||
|
instance = instance,
|
||||||
|
parent = parent
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
children = function(instance, children)
|
||||||
|
return create_binding(update_children, {
|
||||||
|
instance = instance,
|
||||||
|
cur_children_set = {},
|
||||||
|
new_children_set = {},
|
||||||
|
children = children
|
||||||
|
})
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
local graph = require "./graph"
|
|
||||||
type Node<T> = graph.Node<T>
|
|
||||||
local create_node = graph.create_node
|
|
||||||
local push_scope = graph.push_scope
|
|
||||||
local pop_scope = graph.pop_scope
|
|
||||||
local destroy = graph.destroy
|
|
||||||
local get_scope = graph.get_scope
|
|
||||||
|
|
||||||
local function branch<T>(fn: () -> T): (() -> (), T)
|
|
||||||
local current = get_scope()
|
|
||||||
if not current then
|
|
||||||
error(`cannot use branch() outside a stable or reactive scope`, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local parent = current.owner
|
|
||||||
if not parent then
|
|
||||||
error(`current scope is not owned by a scope`, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local node = create_node(parent, false, false)
|
|
||||||
|
|
||||||
local destroy = function()
|
|
||||||
destroy(node)
|
|
||||||
end
|
|
||||||
|
|
||||||
push_scope(node)
|
|
||||||
|
|
||||||
local ok, result = xpcall(fn, debug.traceback)
|
|
||||||
|
|
||||||
pop_scope()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
destroy()
|
|
||||||
error(`error while running branch():\n\n{result}`, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return destroy, result
|
|
||||||
end
|
|
||||||
|
|
||||||
return branch
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
local action = require "./action"()
|
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)
|
||||||
|
|
@ -10,8 +12,6 @@ local function changed<T>(property: string, callback: (T) -> ())
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
con:Disconnect()
|
con:Disconnect()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
callback((instance :: any)[property])
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,19 @@
|
||||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
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 add_cleanup = graph.add_cleanup
|
||||||
|
|
||||||
local function helper(obj: any)
|
local function cleanup(callback: () -> ())
|
||||||
return
|
|
||||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
|
||||||
elseif type(obj) == "thread" then function() task.cancel(obj) end
|
|
||||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
|
||||||
elseif obj.destroy then function() obj:destroy() end
|
|
||||||
elseif obj.disconnect then function() obj:disconnect() end
|
|
||||||
elseif obj.Destroy then function() obj:Destroy() end
|
|
||||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
|
||||||
else error "cannot cleanup given object"
|
|
||||||
end
|
|
||||||
|
|
||||||
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 in a non-reactive scope"
|
||||||
end; assert(scope)
|
end; assert(scope)
|
||||||
|
|
||||||
if type(value) == "function" then
|
add_cleanup(scope, callback)
|
||||||
push_cleanup(scope, value :: () -> ())
|
|
||||||
else
|
|
||||||
push_cleanup(scope, helper(value))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
type Destroyable = { destroy: (any) -> () } | { Destroy: (any) -> () }
|
return cleanup
|
||||||
type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
|
||||||
|
|
||||||
return cleanup ::
|
|
||||||
( (callback: () -> ()) -> () ) &
|
|
||||||
( (thread: thread) -> () ) &
|
|
||||||
( (instance: Destroyable) -> () ) &
|
|
||||||
( (connection: Disconnectable) -> () ) &
|
|
||||||
( (instance: Instance) -> () ) &
|
|
||||||
( (connection: RBXScriptConnection) -> () )
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
local graph = require "./graph"
|
|
||||||
type Node<T> = graph.Node<T>
|
|
||||||
local create_node = graph.create_node
|
|
||||||
local get_scope = graph.get_scope
|
|
||||||
local push_scope = graph.push_scope
|
|
||||||
local pop_scope = graph.pop_scope
|
|
||||||
local set_context = graph.set_context
|
|
||||||
|
|
||||||
export type Context<T> = (() -> T) & (<U>(T, () -> U) -> U)
|
|
||||||
|
|
||||||
local nil_symbol = newproxy()
|
|
||||||
local count = 0
|
|
||||||
|
|
||||||
local function context<T>(...: T): Context<T>
|
|
||||||
count += 1
|
|
||||||
local id = count
|
|
||||||
|
|
||||||
local has_default = select("#", ...) > 0
|
|
||||||
local default_value = ...
|
|
||||||
|
|
||||||
return function<T>(...): any -- todo: fix type error
|
|
||||||
local scope: Node<unknown>? | false = get_scope()
|
|
||||||
|
|
||||||
if select("#", ...) == 0 then -- get
|
|
||||||
while scope do
|
|
||||||
local ctx = scope.context
|
|
||||||
|
|
||||||
if not ctx then
|
|
||||||
scope = scope.owner
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
local value = (ctx :: { unknown })[id]
|
|
||||||
|
|
||||||
if value == nil then
|
|
||||||
scope = scope.owner
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
return (if value ~= nil_symbol then value else nil) :: T
|
|
||||||
end
|
|
||||||
|
|
||||||
if has_default ~= nil then
|
|
||||||
return default_value
|
|
||||||
else
|
|
||||||
error("attempt to get context when no context is set and no default context is set", 0)
|
|
||||||
end
|
|
||||||
else -- set
|
|
||||||
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
|
||||||
|
|
||||||
local value, component = ...
|
|
||||||
|
|
||||||
local new_scope = create_node(scope, false, false)
|
|
||||||
set_context(new_scope, id, if value == nil then nil_symbol else value)
|
|
||||||
|
|
||||||
push_scope(new_scope)
|
|
||||||
|
|
||||||
local function efn(err: string) return debug.traceback(err, 3) end
|
|
||||||
local ok, result = xpcall(component, efn)
|
|
||||||
|
|
||||||
pop_scope()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
error(`error while running context:\n\n{result}`, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil :: any
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return context
|
|
||||||
167
src/create.luau
167
src/create.luau
|
|
@ -1,119 +1,74 @@
|
||||||
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 memoize = require(script.Parent.memoize)
|
||||||
|
|
||||||
local function create_constructor_for_class(class: string): ({ [unknown]: unknown }) -> Instance
|
local function create_instance(class: string)
|
||||||
local function constructor(properties: { [unknown]: unknown }): Instance
|
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
||||||
if not ok then error(`invalid class name {class}`, 0) 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 next, default do
|
||||||
for i, v in default do
|
(instance :: any)[i] = v
|
||||||
(instance :: any)[i] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return apply(instance, properties)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return constructor
|
return function(properties: { [any]: unknown }): Instance
|
||||||
|
return apply(instance:Clone(), properties)
|
||||||
|
end
|
||||||
|
end; create_instance = memoize(create_instance) -- always return same constructor for given class
|
||||||
|
|
||||||
|
local function clone_instance(instance: Instance)
|
||||||
|
return function(properties: { [any]: unknown }): Instance
|
||||||
|
local clone = instance:Clone()
|
||||||
|
if not clone then error("Attempt to clone a non-archivable instance", 3) end
|
||||||
|
return apply(clone, properties)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local constructor_cache = {} :: { [string]: ({ [unknown]: unknown }) -> Instance }
|
local function create(class_or_instance: string|Instance)
|
||||||
|
|
||||||
-- todo: remove support for different overloads
|
|
||||||
local function create(class_or_instance: string|Instance, properties: { [unknown]: unknown }?): unknown
|
|
||||||
if type(class_or_instance) ~= "string" and typeof(class_or_instance) ~= "Instance" then
|
|
||||||
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local constructor: ({ [unknown]: unknown }) -> Instance
|
|
||||||
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 apply(clone, props)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
return nil :: never
|
||||||
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,16 +1,22 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
local set_owner = graph.set_owner
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local track = graph.track
|
||||||
|
local get_owning_scope = graph.get_owning_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
|
|
||||||
local function derive<T>(source: () -> T): () -> T
|
local function derive<T>(source: () -> T): () -> T
|
||||||
local node = create_node(assert_stable_scope(), source, false :: any)
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
|
local node = create_node(false :: any, source)
|
||||||
|
|
||||||
|
set_owner(node, owner)
|
||||||
evaluate_node(node)
|
evaluate_node(node)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
push_scope_as_child_of(node)
|
track(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local get_owning_scope = graph.get_owning_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local set_owner = graph.set_owner
|
||||||
|
|
||||||
local function effect<T>(callback: (T) -> T, initial_value: T)
|
local function effect<T>(callback: (T) -> T, initial_value: T)
|
||||||
local node = create_node(assert_stable_scope(), callback, initial_value)
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
|
local node = create_node(initial_value, callback)
|
||||||
|
|
||||||
|
set_owner(node, owner)
|
||||||
evaluate_node(node)
|
evaluate_node(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
strict = not is_O2,
|
|
||||||
defaults = true,
|
|
||||||
defer_nested_properties = true,
|
|
||||||
batch = false,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
331
src/graph.luau
331
src/graph.luau
|
|
@ -1,6 +1,9 @@
|
||||||
local flags = require "./flags"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
export type SourceNode<T> = {
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
|
||||||
|
export type StartNode<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
[number]: Node<T>
|
[number]: Node<T>
|
||||||
}
|
}
|
||||||
|
|
@ -9,81 +12,73 @@ export type Node<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
effect: ((T) -> T) | false,
|
effect: ((T) -> T) | false,
|
||||||
cleanups: { () -> () } | false,
|
cleanups: { () -> () } | false,
|
||||||
|
parents: { owner: StartNode<T>?, [number]: StartNode<T> },
|
||||||
context: { [number]: unknown } | false,
|
[number]: Node<T>
|
||||||
|
|
||||||
owned: { Node<T> } | false,
|
|
||||||
owner: Node<T> | false,
|
|
||||||
|
|
||||||
parents: { SourceNode<T> },
|
|
||||||
[number]: Node<T> -- children
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
-- reactive scope stack
|
||||||
|
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
|
||||||
|
|
||||||
local function efn(err: string)
|
-- runs a given callback in a context that Luau does not allow yielding in
|
||||||
local trace = debug.traceback(err, 2)
|
local check_for_yield: <T...>(fn: (T...) -> (), T...) -> (boolean, string?) do
|
||||||
|
local t = { __mode = "kv" }
|
||||||
|
setmetatable(t, t)
|
||||||
|
|
||||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
check_for_yield = function(fn, ...: any)
|
||||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
local args = { ... }
|
||||||
return "\n "
|
|
||||||
|
t.__unm = function(_)
|
||||||
|
fn(unpack(args))
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err: string? = pcall(function()
|
||||||
|
local _ = -t
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
return ok, if err == "attempt to yield across metamethod/C-call boundary"
|
||||||
|
or err == "thread is not yieldable" then "yield occured"
|
||||||
|
else err
|
||||||
end
|
end
|
||||||
|
|
||||||
trace ..= "\nsource update stacktrace:"
|
|
||||||
return trace
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
|
||||||
|
|
||||||
local thread = coroutine.create(xpcall)
|
|
||||||
--local function efn(err: string) return debug.traceback(err, 3) end
|
|
||||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
|
||||||
|
|
||||||
assert(resume_ok)
|
|
||||||
|
|
||||||
if coroutine.status(thread) ~= "dead" then
|
|
||||||
return false, debug.traceback(thread, "attempt to yield in reactive scope")
|
|
||||||
end
|
|
||||||
|
|
||||||
return run_ok, result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_scope(): Node<unknown>?
|
local function get_scope(): Node<unknown>?
|
||||||
return scopes[scopes.n]
|
return scopes[scopes.n]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function assert_stable_scope(): Node<unknown>
|
local function get_owning_scope(): Node<unknown>
|
||||||
local scope = get_scope()
|
local scope = get_scope()
|
||||||
|
|
||||||
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}() in non-reactive scope, must be used within a root() or mount() callback`)
|
||||||
elseif scope.effect then
|
elseif scope.effect then
|
||||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
throw("owning scope is not stable; are you trying to derive a new source from within a side-effect?")
|
||||||
end
|
end
|
||||||
|
|
||||||
return scope
|
return scope
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_child<T>(parent: SourceNode<any>, child: Node<any>)
|
local function add_child<T>(parent: StartNode<any>, child: Node<any>)
|
||||||
table.insert(parent, child)
|
table.insert(parent, child)
|
||||||
table.insert(child.parents, parent)
|
table.insert(child.parents, parent)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_scope<T>(node: Node<T>)
|
local function set_owner(node: Node<any>, owner: Node<any>)
|
||||||
|
node.parents.owner = owner
|
||||||
|
table.insert(owner, node)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function open_scope<T>(node: Node<T>)
|
||||||
local n = scopes.n + 1
|
local n = scopes.n + 1
|
||||||
scopes.n = n
|
scopes.n = n
|
||||||
scopes[n] = node
|
scopes[n] = node
|
||||||
end
|
end
|
||||||
|
|
||||||
local function pop_scope()
|
local function close_scope()
|
||||||
local n = scopes.n
|
local n = scopes.n
|
||||||
scopes.n = n - 1
|
scopes.n = n - 1
|
||||||
scopes[n] = nil
|
scopes[n] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
local function add_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||||
if node.cleanups then
|
if node.cleanups then
|
||||||
table.insert(node.cleanups, cleanup)
|
table.insert(node.cleanups, cleanup)
|
||||||
else
|
else
|
||||||
|
|
@ -91,197 +86,147 @@ local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush_cleanups<T>(node: Node<T>)
|
local function run_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)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_and_swap_pop<T>(t: { T }, v: T)
|
local function remove_child<T>(parent: StartNode<T>, child: Node<T>)
|
||||||
local i = table.find(t, v) :: number
|
local idx = table.find(parent, child)
|
||||||
local n = #t
|
assert(idx, "child not found")
|
||||||
t[i] = t[n]
|
local n = #parent
|
||||||
t[n] = nil
|
parent[idx] = parent[n]
|
||||||
|
parent[n] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function unparent<T>(node: Node<T>)
|
local function unparent<T>(node: Node<T>)
|
||||||
local parents = node.parents
|
local parents = node.parents
|
||||||
|
|
||||||
for i, parent in parents do
|
for i, parent in ipairs(parents) do
|
||||||
find_and_swap_pop(parent, node)
|
remove_child(parent, node)
|
||||||
parents[i] = nil
|
parents[i] = nil
|
||||||
end
|
end
|
||||||
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
|
run_cleanups(node)
|
||||||
error("attempt to destroy an active scope", 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
flush_cleanups(node)
|
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
|
||||||
if node.owner then
|
node.effect = false
|
||||||
find_and_swap_pop(node.owner.owned :: { Node<T> }, node)
|
|
||||||
node.owner = false
|
if node.parents.owner then
|
||||||
|
remove_child(node.parents.owner, node)
|
||||||
|
node.parents.owner = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if node.owned then
|
while node[1] do destroy(node[1]) end
|
||||||
local owned = node.owned
|
|
||||||
while owned[1] do destroy(owned[1]) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function destroy_owned<T>(node: Node<T>)
|
|
||||||
if node.owned then
|
|
||||||
local owned = node.owned
|
|
||||||
while owned[1] do destroy(owned[1]) end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
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>)
|
||||||
|
local cur_value = node.cache
|
||||||
|
|
||||||
if flags.strict then
|
if flags.strict then
|
||||||
if table.find(scopes, node) then
|
run_cleanups(node)
|
||||||
error("a scope, that should rerun due to the update of a source, is already active", 0)
|
open_scope(node)
|
||||||
end
|
|
||||||
|
|
||||||
local initial_value = node.cache
|
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
|
||||||
|
|
||||||
for i = 1, 2 do
|
close_scope()
|
||||||
local cur_value = node.cache
|
|
||||||
|
|
||||||
flush_cleanups(node)
|
if not ok then throw(err :: string) end
|
||||||
destroy_owned(node)
|
end
|
||||||
|
|
||||||
push_scope(node)
|
run_cleanups(node) -- todo: move in scope?
|
||||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
open_scope(node)
|
||||||
pop_scope()
|
|
||||||
|
|
||||||
if not ok then
|
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
|
||||||
table.clear(update_queue)
|
|
||||||
update_queue.n = 0
|
close_scope()
|
||||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
|
||||||
|
if not ok then
|
||||||
|
table.clear(update_queue)
|
||||||
|
update_queue.n = 0
|
||||||
|
throw(`side-effect error from source update\n{new_value}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
node.cache = new_value
|
||||||
|
|
||||||
|
return cur_value ~= new_value -- node has changed value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_from<T>(node: StartNode<T>, n0: number)
|
||||||
|
if not node[1] then return end
|
||||||
|
|
||||||
|
local n = n0
|
||||||
|
|
||||||
|
-- unparent all children and queue for eval
|
||||||
|
do
|
||||||
|
local i = 1
|
||||||
|
local child = node[i]
|
||||||
|
while child do
|
||||||
|
unparent(child)
|
||||||
|
|
||||||
|
n += 1
|
||||||
|
update_queue[n] = child
|
||||||
|
|
||||||
|
local next_child = node[i]
|
||||||
|
|
||||||
|
-- children who have this parent as an owner will not be unparented
|
||||||
|
-- if such a child is encountered then skip it
|
||||||
|
if next_child == child then
|
||||||
|
i += 1
|
||||||
|
next_child = node[i]
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value :: T
|
child = next_child
|
||||||
end
|
end
|
||||||
|
|
||||||
return initial_value ~= node.cache
|
|
||||||
else
|
|
||||||
local cur_value = node.cache
|
|
||||||
|
|
||||||
flush_cleanups(node)
|
|
||||||
destroy_owned(node)
|
|
||||||
|
|
||||||
push_scope(node)
|
|
||||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
|
||||||
pop_scope()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
table.clear(update_queue)
|
|
||||||
update_queue.n = 0
|
|
||||||
error(`effect error:\n{new_value}\n`, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
node.cache = new_value
|
|
||||||
return cur_value ~= new_value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function queue_children_for_update<T>(node: SourceNode<T>)
|
|
||||||
local i = update_queue.n
|
|
||||||
while node[1] do
|
|
||||||
i += 1
|
|
||||||
update_queue[i] = node[1]
|
|
||||||
unparent(node[1])
|
|
||||||
end
|
|
||||||
update_queue.n = i
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_update_queue_length()
|
|
||||||
return update_queue.n
|
|
||||||
end
|
|
||||||
|
|
||||||
local function flush_update_queue(from: number)
|
|
||||||
local i = from + 1
|
|
||||||
while i <= update_queue.n do
|
|
||||||
local node = update_queue[i]
|
|
||||||
--assert(node.effect)
|
|
||||||
|
|
||||||
if node.owner and evaluate_node(node) then
|
|
||||||
queue_children_for_update(node)
|
|
||||||
end
|
|
||||||
|
|
||||||
update_queue[i] = false :: any
|
|
||||||
i += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue.n = from
|
update_queue.n = n
|
||||||
end
|
|
||||||
|
|
||||||
local function update_descendants<T>(root: SourceNode<T>)
|
-- evaluate all queued children
|
||||||
local n0 = update_queue.n
|
for i = n0 + 1, n do
|
||||||
queue_children_for_update(root)
|
local child = update_queue[i]
|
||||||
|
if not child.effect then continue end
|
||||||
|
|
||||||
if flags.batch then return end
|
if evaluate_node(child) then
|
||||||
|
update_from(child, n)
|
||||||
local i = n0 + 1
|
|
||||||
while i <= update_queue.n do
|
|
||||||
local node = update_queue[i]
|
|
||||||
--assert(node.effect)
|
|
||||||
|
|
||||||
-- check if node is still owned in case destroyed after queued
|
|
||||||
if node.owner and evaluate_node(node) then
|
|
||||||
queue_children_for_update(node)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
||||||
i += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update_queue.n = n0
|
update_queue.n = n0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function push_scope_as_child_of<T>(node: SourceNode<T>)
|
local function update<T>(node: StartNode<T>)
|
||||||
|
update_from(node, update_queue.n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function track<T>(node: StartNode<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)
|
add_child(node, scope)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T, value: T): Node<T>
|
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
|
||||||
local node: Node<T> = {
|
return {
|
||||||
cache = value,
|
cache = value,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
cleanups = false,
|
cleanups = false,
|
||||||
|
|
||||||
context = false,
|
|
||||||
|
|
||||||
owner = owner,
|
|
||||||
owned = false,
|
|
||||||
|
|
||||||
parents = {},
|
parents = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if owner then
|
|
||||||
if owner.owned then
|
|
||||||
table.insert(owner.owned, node)
|
|
||||||
else
|
|
||||||
owner.owned = { node }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return node
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function create_source_node<T>(value: T): SourceNode<T>
|
local function create_start_node<T>(value: T): StartNode<T>
|
||||||
return { cache = value }
|
return { cache = value }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -289,33 +234,21 @@ local function get_children<T>(node: Node<T>): { Node<unknown> }
|
||||||
return { unpack(node) } :: { Node<any> }
|
return { unpack(node) } :: { Node<any> }
|
||||||
end
|
end
|
||||||
|
|
||||||
local function set_context<T>(node: Node<T>, key: number, value: unknown)
|
|
||||||
if node.context then
|
|
||||||
node.context[key] = value
|
|
||||||
else
|
|
||||||
node.context = { [key] = value }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.freeze {
|
return table.freeze {
|
||||||
push_scope = push_scope,
|
open_scope = open_scope,
|
||||||
pop_scope = pop_scope,
|
close_scope = close_scope,
|
||||||
evaluate_node = evaluate_node,
|
evaluate_node = evaluate_node,
|
||||||
get_scope = get_scope,
|
get_scope = get_scope,
|
||||||
assert_stable_scope = assert_stable_scope,
|
get_owning_scope = get_owning_scope,
|
||||||
push_cleanup = push_cleanup,
|
add_cleanup = add_cleanup,
|
||||||
|
set_owner = set_owner,
|
||||||
destroy = destroy,
|
destroy = destroy,
|
||||||
flush_cleanups = flush_cleanups,
|
run_cleanups = run_cleanups,
|
||||||
push_scope_as_child_of = push_scope_as_child_of,
|
track = track,
|
||||||
update_descendants = update_descendants,
|
update = update,
|
||||||
push_child = push_child,
|
add_child = add_child,
|
||||||
create_node = create_node,
|
create_node = create_node,
|
||||||
create_source_node = create_source_node,
|
create_start_node = create_start_node,
|
||||||
get_children = get_children,
|
get_children = get_children,
|
||||||
flush_update_queue = flush_update_queue,
|
scopes = scopes
|
||||||
get_update_queue_length = get_update_queue_length,
|
|
||||||
set_context = set_context,
|
|
||||||
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
|
|
||||||
116
src/init.luau
116
src/init.luau
|
|
@ -1,12 +1,112 @@
|
||||||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
--------------------------------------------------------------------------------
|
||||||
|
-- vide.luau
|
||||||
|
-- v0.1.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 cleanup = require(script.cleanup)
|
||||||
|
local untrack = require(script.untrack)
|
||||||
|
local derive = require(script.derive)
|
||||||
|
local switch = require(script.switch)
|
||||||
|
local show = require(script.show)
|
||||||
|
local indexes, values = require(script.maps)()
|
||||||
|
local spring, update_springs = require(script.spring)()
|
||||||
|
local action = require(script.action)()
|
||||||
|
local changed = require(script.changed)
|
||||||
|
local throw = require(script.throw)
|
||||||
|
local flags = require(script.flags)
|
||||||
|
|
||||||
|
export type Source<T> = source.Source<T>
|
||||||
|
|
||||||
|
local function step(dt: number)
|
||||||
|
if game then
|
||||||
|
debug.profilebegin("VIDE STEP")
|
||||||
|
debug.profilebegin("VIDE SPRING")
|
||||||
|
end
|
||||||
|
|
||||||
|
update_springs(dt)
|
||||||
|
|
||||||
|
if game then
|
||||||
|
debug.profileend()
|
||||||
|
debug.profileend()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||||
|
task.defer(step, dt)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local vide = {
|
||||||
|
-- core
|
||||||
|
root = root,
|
||||||
|
mount = mount,
|
||||||
|
create = create,
|
||||||
|
source = source,
|
||||||
|
effect = effect,
|
||||||
|
derive = derive,
|
||||||
|
switch = switch,
|
||||||
|
show = show,
|
||||||
|
indexes = indexes,
|
||||||
|
values = values,
|
||||||
|
|
||||||
|
-- util
|
||||||
|
cleanup = cleanup,
|
||||||
|
untrack = untrack,
|
||||||
|
read = function<T>(value: T | () -> T): T
|
||||||
|
return if type(value) == "function" then value() else value
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- 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
|
|
||||||
226
src/maps.luau
Normal file
226
src/maps.luau
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
type StartNode<T> = graph.StartNode<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local create_start_node = graph.create_start_node
|
||||||
|
local set_owner = graph.set_owner
|
||||||
|
local track = graph.track
|
||||||
|
local update = graph.update
|
||||||
|
local get_owning_scope = graph.get_owning_scope
|
||||||
|
local open_scope = graph.open_scope
|
||||||
|
local close_scope = graph.close_scope
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local destroy = graph.destroy
|
||||||
|
|
||||||
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
|
local function check_primitives(t: {})
|
||||||
|
if not flags.strict then return end
|
||||||
|
|
||||||
|
for _, v in next, t do
|
||||||
|
if type(v) == "table" or type(v) == "userdata" then continue end
|
||||||
|
throw("table source map cannot return primitives")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||||
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
|
local subowner = create_node(false, false)
|
||||||
|
set_owner(subowner, owner)
|
||||||
|
|
||||||
|
local input_cache = {} :: Map<K, VI>
|
||||||
|
local output_cache = {} :: Map<K, VO>
|
||||||
|
local input_nodes = {} :: Map<K, StartNode<VI>>
|
||||||
|
local remove_queue = {} :: { K }
|
||||||
|
local scopes = {} :: Map<K, Node<unknown>>
|
||||||
|
|
||||||
|
local function update_children(data)
|
||||||
|
-- queue removed values
|
||||||
|
for i in next, input_cache do
|
||||||
|
if data[i] == nil then
|
||||||
|
table.insert(remove_queue, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- remove queued values
|
||||||
|
for _, i in next, remove_queue do
|
||||||
|
destroy(scopes[i])
|
||||||
|
|
||||||
|
input_cache[i] = nil
|
||||||
|
output_cache[i] = nil
|
||||||
|
input_nodes[i] = nil
|
||||||
|
scopes[i] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(remove_queue)
|
||||||
|
|
||||||
|
open_scope(subowner)
|
||||||
|
|
||||||
|
-- process new or changed values
|
||||||
|
for i, v in next, data do
|
||||||
|
local cv = input_cache[i]
|
||||||
|
|
||||||
|
if cv ~= v then
|
||||||
|
if cv == nil then -- create new scope and run transform
|
||||||
|
local scope = create_node(false, false)
|
||||||
|
scopes[i] = scope :: Node<any>
|
||||||
|
|
||||||
|
local node = create_start_node(v)
|
||||||
|
|
||||||
|
set_owner(scope, subowner)
|
||||||
|
open_scope(scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(transform, function()
|
||||||
|
track(node)
|
||||||
|
return node.cache
|
||||||
|
end, i)
|
||||||
|
|
||||||
|
close_scope()
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
close_scope() -- subowner scope
|
||||||
|
error(result, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
input_nodes[i] = node
|
||||||
|
output_cache[i] = result
|
||||||
|
else -- update source
|
||||||
|
input_nodes[i].cache = v
|
||||||
|
update(input_nodes[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
input_cache[i] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
close_scope()
|
||||||
|
|
||||||
|
local output_array = table.create(#scopes)
|
||||||
|
for _, v in next, output_cache do
|
||||||
|
table.insert(output_array, v)
|
||||||
|
end
|
||||||
|
check_primitives(output_array)
|
||||||
|
|
||||||
|
return output_array
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(false :: any, function()
|
||||||
|
return update_children(input())
|
||||||
|
end)
|
||||||
|
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
track(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||||
|
local owner = get_owning_scope()
|
||||||
|
|
||||||
|
local subowner = create_node(false, false)
|
||||||
|
set_owner(subowner, owner)
|
||||||
|
|
||||||
|
local cur_input_cache_up = {} :: Map<VI, K>
|
||||||
|
local new_input_cache_up = {} :: Map<VI, K>
|
||||||
|
local output_cache = {} :: Map<VI, VO>
|
||||||
|
local input_nodes = {} :: Map<VI, StartNode<K>>
|
||||||
|
local scopes = {} :: Map<VI, Node<unknown>>
|
||||||
|
|
||||||
|
local function update_children(data: Map<K, VI>)
|
||||||
|
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
|
||||||
|
|
||||||
|
if flags.strict then
|
||||||
|
local cache = {}
|
||||||
|
for _, v in next, data do
|
||||||
|
if cache[v] ~= nil then
|
||||||
|
throw "duplicate table value detected"
|
||||||
|
end
|
||||||
|
cache[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
open_scope(subowner)
|
||||||
|
|
||||||
|
-- process data
|
||||||
|
for i, v in next, data do
|
||||||
|
new_input_cache[v] = i
|
||||||
|
|
||||||
|
local cv = cur_input_cache[v]
|
||||||
|
|
||||||
|
if cv == nil then -- create new scope and run transform
|
||||||
|
local scope = create_node(false, false)
|
||||||
|
scopes[v] = scope :: Node<any>
|
||||||
|
|
||||||
|
local node = create_start_node(i)
|
||||||
|
|
||||||
|
set_owner(scope, subowner)
|
||||||
|
open_scope(scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(transform, v, function()
|
||||||
|
track(node)
|
||||||
|
return node.cache
|
||||||
|
end)
|
||||||
|
|
||||||
|
close_scope()
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
close_scope() -- subowner scope
|
||||||
|
error(result, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
input_nodes[v] = node
|
||||||
|
output_cache[v] = result
|
||||||
|
else -- update source
|
||||||
|
if cv ~= i then
|
||||||
|
input_nodes[v].cache = i
|
||||||
|
update(input_nodes[v])
|
||||||
|
end
|
||||||
|
|
||||||
|
cur_input_cache[v] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
close_scope()
|
||||||
|
|
||||||
|
-- remove old values
|
||||||
|
for v in next, cur_input_cache do
|
||||||
|
destroy(scopes[v])
|
||||||
|
|
||||||
|
output_cache[v] = nil
|
||||||
|
input_nodes[v] = nil
|
||||||
|
scopes[v] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update buffer cache
|
||||||
|
table.clear(cur_input_cache)
|
||||||
|
cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache
|
||||||
|
|
||||||
|
local output_array = table.create(#scopes)
|
||||||
|
for _, v in next, output_cache do
|
||||||
|
table.insert(output_array, v)
|
||||||
|
end
|
||||||
|
check_primitives(output_array)
|
||||||
|
|
||||||
|
return output_array
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(false :: any, function()
|
||||||
|
return update_children(input())
|
||||||
|
end)
|
||||||
|
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
track(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return function() return indexes, values end
|
||||||
17
src/memoize.luau
Normal file
17
src/memoize.luau
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
local function memoize<X, Y>(f: (X) -> Y): (X) -> Y
|
||||||
|
local cache: { [X]: Y? } = {}
|
||||||
|
|
||||||
|
return function(x: X): Y
|
||||||
|
local y = cache[x]
|
||||||
|
|
||||||
|
if not y then
|
||||||
|
y = f(x)
|
||||||
|
cache[x] = y
|
||||||
|
end
|
||||||
|
|
||||||
|
return y :: Y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return memoize
|
||||||
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
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(destroy)
|
||||||
local result = component()
|
local result = component()
|
||||||
if target then apply(target, { result }) end
|
if target then apply(target, { result }) end
|
||||||
|
return destroy
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
local function read<T>(value: T | () -> T): T
|
|
||||||
return if type(value) == "function" then value() else value
|
|
||||||
end
|
|
||||||
|
|
||||||
return read
|
|
||||||
|
|
@ -1,35 +1,38 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
type Node<T> = graph.Node<T>
|
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 open_scope = graph.open_scope
|
||||||
local pop_scope = graph.pop_scope
|
local close_scope = graph.close_scope
|
||||||
local destroy = graph.destroy
|
local destroy = graph.destroy
|
||||||
|
|
||||||
local refs = {}
|
local refs = {}
|
||||||
|
|
||||||
local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||||
local node = create_node(false, false, false)
|
local node = create_node(false, false)
|
||||||
|
|
||||||
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)
|
open_scope(node)
|
||||||
|
|
||||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
local result = { pcall(fn, destroy) }
|
||||||
|
|
||||||
pop_scope()
|
close_scope()
|
||||||
|
|
||||||
if not result[1] then
|
if not result[1] then
|
||||||
destroy()
|
refs[node] = nil
|
||||||
error(`error while running root():\n\n{result[2]}`, 0)
|
throw(`mount error\n{result[2]}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return destroy, unpack(result :: any, 2)
|
return unpack(result :: any, 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
return root :: <T...>(fn: (destroy: () -> ()) -> T...) -> (() -> (), T...)
|
return root :: (<T...>(fn: (destroy: () -> ()) -> T...) -> T...) & ((fn: (destroy: () -> ()) -> ()) -> ())
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,18 @@
|
||||||
local source = require "./source"
|
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))?
|
end
|
||||||
): Source<nil | Obj | Array<Obj>>
|
|
||||||
local filtered_input = source()
|
|
||||||
|
|
||||||
effect(function()
|
return switch(truthy) {
|
||||||
local v = input()
|
[true] = component,
|
||||||
if v then
|
[false] = fallback,
|
||||||
filtered_input(v)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local input_is_truthy = derive(function()
|
|
||||||
return not not input()
|
|
||||||
end)
|
|
||||||
|
|
||||||
return switch(input_is_truthy) {
|
|
||||||
[true] = function(present)
|
|
||||||
return component(filtered_input, present)
|
|
||||||
end,
|
|
||||||
[false] = fallback
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return show
|
return show ::
|
||||||
|
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
|
||||||
|
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
type Node<T> = graph.Node<T>
|
|
||||||
local create_source_node = graph.create_source_node
|
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
|
||||||
local update_descendants = graph.update_descendants
|
|
||||||
|
|
||||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
local create_start_node = graph.create_start_node
|
||||||
|
local track = graph.track
|
||||||
|
local update = graph.update
|
||||||
|
|
||||||
|
export type Source<T> = (() -> T) & ((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_start_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)
|
track(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -21,11 +23,9 @@ local function source<T>(initial_value: T): Source<T>
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = v
|
node.cache = v
|
||||||
update_descendants(node)
|
update(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>)
|
||||||
|
|
|
||||||
298
src/spring.luau
298
src/spring.luau
|
|
@ -1,84 +1,104 @@
|
||||||
local graph = require "./graph"
|
if not game then script = require "test/relative-string" end
|
||||||
type Node<T> = graph.Node<T>
|
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||||
type SourceNode<T> = graph.SourceNode<T>
|
|
||||||
local create_node = graph.create_node
|
|
||||||
local create_source_node = graph.create_source_node
|
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
|
||||||
local evaluate_node = graph.evaluate_node
|
|
||||||
local update_descendants = graph.update_descendants
|
|
||||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
|
||||||
|
|
||||||
local UPDATE_RATE = 120
|
|
||||||
local TOLERANCE_FACTOR = 10_000
|
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
||||||
|
Supported datatypes:
|
||||||
|
- number
|
||||||
|
- CFrame
|
||||||
|
- Color3
|
||||||
|
- UDim
|
||||||
|
- UDim2
|
||||||
|
- Vector2
|
||||||
|
- Vector3
|
||||||
|
- Rect
|
||||||
|
|
||||||
Unsupported datatypes:
|
Unsupported datatypes:
|
||||||
- bool
|
- bool
|
||||||
- Vector2int16
|
- Vector2int16
|
||||||
- Vector3int16
|
- Vector3int16
|
||||||
- EnumItem
|
- EnumItem
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
type SpringState<T> = {
|
local throw = require(script.Parent.throw)
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
type StartNode<T> = graph.StartNode<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local create_start_node = graph.create_start_node
|
||||||
|
local get_owning_scope = graph.get_owning_scope
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local update = graph.update
|
||||||
|
local set_owner = graph.set_owner
|
||||||
|
local track = graph.track
|
||||||
|
|
||||||
|
local UPDATE_RATE = 120
|
||||||
|
local TOLERANCE = 0.0001
|
||||||
|
|
||||||
|
type Vec3 = Vector3
|
||||||
|
|
||||||
|
local function Vec3(x: number?, y: number?, z: number?)
|
||||||
|
return Vector3.new(x, y, z)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ZERO = Vec3(0, 0, 0)
|
||||||
|
|
||||||
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||||
|
|
||||||
|
type SpringData<T> = {
|
||||||
k: number, -- spring constant
|
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 = {
|
||||||
|
|
@ -91,15 +111,15 @@ local vec6_to_type = {
|
||||||
end :: Vec6ToType<CFrame>,
|
end :: Vec6ToType<CFrame>,
|
||||||
|
|
||||||
Color3 = function(v)
|
Color3 = function(v)
|
||||||
return Color3.new(math.clamp(v.X, 0, 1), math.clamp(v.Y, 0, 1), math.clamp(v.Z, 0, 1))
|
return Color3.new(v.X, v.Y, v.Z)
|
||||||
end :: Vec6ToType<Color3>,
|
end :: Vec6ToType<Color3>,
|
||||||
|
|
||||||
UDim = function(v)
|
UDim = function(v)
|
||||||
return UDim.new(v.X, math.round(v.Y))
|
return UDim.new(v.X, v.Y)
|
||||||
end :: Vec6ToType<UDim>,
|
end :: Vec6ToType<UDim>,
|
||||||
|
|
||||||
UDim2 = function(a, b)
|
UDim2 = function(a, b)
|
||||||
return UDim2.new(a.X, math.round(a.Y), a.Z, math.round(b.X))
|
return UDim2.new(a.X, a.Y, a.Z, b.X)
|
||||||
end :: Vec6ToType<UDim2>,
|
end :: Vec6ToType<UDim2>,
|
||||||
|
|
||||||
Vector2 = function(v)
|
Vector2 = function(v)
|
||||||
|
|
@ -112,16 +132,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,133 +146,80 @@ 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>]: StartNode<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 = get_owning_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
|
||||||
|
|
||||||
-- todo: is there a solution other than reducing step size?
|
-- todo: is there a solution to this other than upping step frequency?
|
||||||
-- 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
local output = create_source_node(false :: any)
|
local output = create_start_node(false :: any)
|
||||||
|
|
||||||
local function updater_effect()
|
local function updater_effect()
|
||||||
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(false :: any, updater_effect)
|
||||||
|
|
||||||
|
set_owner(updater, owner)
|
||||||
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)
|
return function()
|
||||||
local x = p.position
|
track(output)
|
||||||
local v = p.velocity
|
return output.cache
|
||||||
local dv = p.impulse
|
end
|
||||||
|
|
||||||
if x then
|
|
||||||
local x_123, x_456 = type_to_vec6[typeof(x)](x)
|
|
||||||
data.x_123, data.x_456 = x_123, x_456
|
|
||||||
data.x0_123, data.x0_456 = x_123, x_456
|
|
||||||
end
|
|
||||||
|
|
||||||
if v then
|
|
||||||
data.v_123, data.v_456 = type_to_vec6[typeof(v)](v)
|
|
||||||
end
|
|
||||||
|
|
||||||
if dv then
|
|
||||||
local dv_123, dv_456 = type_to_vec6[typeof(dv)](dv)
|
|
||||||
data.v_123 += dv_123
|
|
||||||
data.v_456 += dv_456
|
|
||||||
end
|
|
||||||
|
|
||||||
-- schedule spring
|
|
||||||
springs[data] = output
|
|
||||||
end :: SpringSettings<T>
|
|
||||||
|
|
||||||
return function(...)
|
|
||||||
if select("#", ...) == 0 then -- no args were given
|
|
||||||
push_scope_as_child_of(output)
|
|
||||||
return output.cache
|
|
||||||
end
|
|
||||||
|
|
||||||
-- set current position to value
|
|
||||||
local v = ... :: T
|
|
||||||
data.x_123, data.x_456 = type_to_vec6[typeof(v)](v)
|
|
||||||
|
|
||||||
-- reset velocity
|
|
||||||
data.v_123 = vector.zero
|
|
||||||
data.v_456 = vector.zero
|
|
||||||
|
|
||||||
-- schedule spring
|
|
||||||
springs[data] = output
|
|
||||||
|
|
||||||
-- set output to value
|
|
||||||
output.cache = v
|
|
||||||
|
|
||||||
return v
|
|
||||||
end, config
|
|
||||||
end
|
|
||||||
|
|
||||||
-- calculates a float tolerance, based on the magnitude of the float
|
|
||||||
local function get_min_step(x: number): number
|
|
||||||
return x/TOLERANCE_FACTOR
|
|
||||||
end
|
|
||||||
local function get_min_vector_step(direction: vector): vector
|
|
||||||
return vector.create(
|
|
||||||
get_min_step(direction.x),
|
|
||||||
get_min_step(direction.y),
|
|
||||||
get_min_step(direction.z)
|
|
||||||
)
|
|
||||||
end
|
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 +229,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(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for _, data in next, remove_queue do
|
||||||
|
springs[data] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(remove_queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
|
||||||
164
src/switch.luau
164
src/switch.luau
|
|
@ -1,113 +1,67 @@
|
||||||
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 StartNode<T> = graph.StartNode<T>
|
||||||
|
local create_node = graph.create_node
|
||||||
|
local evaluate_node = graph.evaluate_node
|
||||||
|
local set_owner = graph.set_owner
|
||||||
|
local track = graph.track
|
||||||
|
local destroy = graph.destroy
|
||||||
|
local get_owning_scope = graph.get_owning_scope
|
||||||
|
local open_scope = graph.open_scope
|
||||||
|
local close_scope = graph.close_scope
|
||||||
|
|
||||||
type Array<T> = { T }
|
|
||||||
type Map<K, V> = { [K]: V }
|
type 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 = get_owning_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(false, false)
|
||||||
|
last_scope = new_scope :: Node<any>
|
||||||
|
|
||||||
|
set_owner(new_scope, owner)
|
||||||
|
open_scope(new_scope)
|
||||||
|
|
||||||
|
local ok, result = pcall(component)
|
||||||
|
|
||||||
|
close_scope()
|
||||||
|
|
||||||
|
if not ok then error(result, 0) end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = create_node(nil :: any, update)
|
||||||
|
|
||||||
|
set_owner(node, owner)
|
||||||
|
evaluate_node(node)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
track(node)
|
||||||
|
return node.cache
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
9
src/throw.luau
Normal file
9
src/throw.luau
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
if not game then script = require "test/relative-string" end
|
||||||
|
|
||||||
|
local trace = require(script.Parent.trace)
|
||||||
|
|
||||||
|
local function throw(msg): any
|
||||||
|
error(msg, trace()-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
return throw
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
local queue = {} :: {
|
|
||||||
{ t: number, fn: () -> (), cancel: boolean }
|
|
||||||
}
|
|
||||||
|
|
||||||
local function timeout(t: number, fn: () -> ())
|
|
||||||
local handle = { t = t, fn = fn, cancel = false }
|
|
||||||
table.insert(queue, handle)
|
|
||||||
return handle
|
|
||||||
end
|
|
||||||
|
|
||||||
local function update_timeouts(dt: number)
|
|
||||||
for i = #queue, 1, -1 do
|
|
||||||
local handle = queue[i]
|
|
||||||
handle.t -= dt
|
|
||||||
|
|
||||||
if handle.cancel or handle.t <= 0 then
|
|
||||||
queue[i] = queue[#queue]
|
|
||||||
queue[#queue] = nil
|
|
||||||
|
|
||||||
if not handle.cancel then
|
|
||||||
handle.fn()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return function() return timeout, update_timeouts end
|
|
||||||
29
src/trace.luau
Normal file
29
src/trace.luau
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- returns path to file as an array with each directory
|
||||||
|
-- accounts for Roblox and Luau contexts
|
||||||
|
local function get_path(s)
|
||||||
|
if string.sub(s, #s - 4, #s) == ".luau" then
|
||||||
|
s = string.sub(s, 1, #s - 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
return string.split(s, string.match(s, "%w+/") and "/" or ".")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get directory of vide root
|
||||||
|
local root do
|
||||||
|
local path = get_path(debug.info(1, "s"))
|
||||||
|
root = path[#path - 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- finds the first stack depth outside of any vide library function
|
||||||
|
return function(): number
|
||||||
|
local stack = 1
|
||||||
|
|
||||||
|
local path = get_path(debug.info(stack, "s"))
|
||||||
|
|
||||||
|
while path[#path] == root or path[#path - 1] == root do
|
||||||
|
stack += 1
|
||||||
|
path = get_path(debug.info(stack, "s"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return stack
|
||||||
|
end
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
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 get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
|
|
||||||
local function untrack<T>(source: () -> T): T
|
local function untrack<T>(source: () -> T): T
|
||||||
local scope = get_scope()
|
local scope = get_scope()
|
||||||
|
if not scope then
|
||||||
|
throw("cannot untrack in non-reactive scope")
|
||||||
|
end; assert(scope)
|
||||||
|
|
||||||
if scope then
|
-- sources are only tracked if the node in scope has an effect
|
||||||
-- sources are only tracked if the node in scope has an effect
|
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
|
|
||||||
return source()
|
|
||||||
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,17 +1,13 @@
|
||||||
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 indexes = vide.indexes
|
local indexes = vide.indexes
|
||||||
local values = vide.values
|
local values = vide.values
|
||||||
local batch = vide.batch
|
|
||||||
local cleanup = vide.cleanup
|
local cleanup = vide.cleanup
|
||||||
local untrack = vide.untrack
|
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local context = vide.context
|
|
||||||
|
|
||||||
assert(not vide.strict)
|
assert(not vide.strict)
|
||||||
|
|
||||||
|
|
@ -20,18 +16,18 @@ local function TITLE(name: string)
|
||||||
print(testkit.color.white(name))
|
print(testkit.color.white(name))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ROOT_BENCH(name: string, fn: () -> ())
|
local N = 2^18 -- 262144
|
||||||
|
|
||||||
|
local function WRAP_BENCH(name: string, fn: () -> ())
|
||||||
vide.root(function(destroy)
|
vide.root(function(destroy)
|
||||||
BENCH(name, fn)
|
BENCH(name, fn)
|
||||||
return destroy
|
return destroy
|
||||||
end)()
|
end)()
|
||||||
end
|
end
|
||||||
|
|
||||||
local N = 2^20
|
|
||||||
|
|
||||||
TITLE "sources"
|
TITLE "sources"
|
||||||
|
|
||||||
BENCH("create source", function()
|
WRAP_BENCH("create source", function()
|
||||||
local cache = table.create(N)
|
local cache = table.create(N)
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -39,7 +35,7 @@ BENCH("create source", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
BENCH("get value", function()
|
WRAP_BENCH("get value", function()
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -47,7 +43,7 @@ BENCH("get value", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
BENCH("set value", function()
|
WRAP_BENCH("set value", function()
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -55,7 +51,7 @@ BENCH("set value", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("derive 1 source", function()
|
WRAP_BENCH("derive 1 source", function()
|
||||||
local cache = table.create(N)
|
local cache = table.create(N)
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
|
|
@ -66,7 +62,7 @@ ROOT_BENCH("derive 1 source", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("derive 4 sources", function()
|
WRAP_BENCH("derive 4 sources", function()
|
||||||
local cache = table.create(N)
|
local cache = table.create(N)
|
||||||
local src = vide.source(1)
|
local src = vide.source(1)
|
||||||
local src2 = vide.source(2)
|
local src2 = vide.source(2)
|
||||||
|
|
@ -82,7 +78,7 @@ end)
|
||||||
|
|
||||||
TITLE "graphs"
|
TITLE "graphs"
|
||||||
|
|
||||||
ROOT_BENCH("update 1->1 graph", function()
|
WRAP_BENCH("update 1->1 graph", function()
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
local _derived = derive(function() return src() end)
|
local _derived = derive(function() return src() end)
|
||||||
|
|
@ -92,7 +88,7 @@ ROOT_BENCH("update 1->1 graph", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update 1->1 graph with cleanup", function()
|
WRAP_BENCH("update 1->1 graph with cleanup", function()
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
||||||
derive(function()
|
derive(function()
|
||||||
|
|
@ -105,7 +101,7 @@ ROOT_BENCH("update 1->1 graph with cleanup", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update 1->1000 graph", function()
|
WRAP_BENCH("update 1->1000 graph", function()
|
||||||
local src = source(-1)
|
local src = source(-1)
|
||||||
|
|
||||||
for i = 1, 1000 do
|
for i = 1, 1000 do
|
||||||
|
|
@ -119,7 +115,7 @@ ROOT_BENCH("update 1->1000 graph", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update 1->1->1->1...1000 graph", function()
|
WRAP_BENCH("update 1->1->1->1...1000 graph", function()
|
||||||
local src = source(-1)
|
local src = source(-1)
|
||||||
|
|
||||||
local last = src
|
local last = src
|
||||||
|
|
@ -135,28 +131,8 @@ ROOT_BENCH("update 1->1->1->1...1000 graph", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- todo: why does it hang at 1k? it didn't before
|
-- todo: repeat with batching
|
||||||
ROOT_BENCH("update 500->1 graph", function()
|
WRAP_BENCH("update 1000->1 graph", function()
|
||||||
local srcs = {}
|
|
||||||
for i = 1, 500 do
|
|
||||||
srcs[i] = source(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
derive(function()
|
|
||||||
for i = 1, 500 do
|
|
||||||
srcs[i]()
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end)
|
|
||||||
|
|
||||||
for i = 1, START(1) do
|
|
||||||
for idx = 1, 500 do
|
|
||||||
srcs[idx](i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
ROOT_BENCH("update 1000->1 graph (batched)", function()
|
|
||||||
local srcs = {}
|
local srcs = {}
|
||||||
for i = 1, 1000 do
|
for i = 1, 1000 do
|
||||||
srcs[i] = source(0)
|
srcs[i] = source(0)
|
||||||
|
|
@ -170,16 +146,14 @@ ROOT_BENCH("update 1000->1 graph (batched)", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
for i = 1, START(1) do
|
for i = 1, START(1) do
|
||||||
batch(function()
|
for idx = 1, 1000 do
|
||||||
for idx = 1, 1000 do
|
srcs[idx](i)
|
||||||
srcs[idx](i)
|
end
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- todo: optimize this case
|
-- todo: optimize, repeat with batching
|
||||||
ROOT_BENCH("update 1000 1->1 common extern. graph", function()
|
WRAP_BENCH("update 1000x 1->1 common extern. graph", function()
|
||||||
local ext = source(-1)
|
local ext = source(-1)
|
||||||
|
|
||||||
local srcs = {}
|
local srcs = {}
|
||||||
|
|
@ -199,8 +173,8 @@ end)
|
||||||
|
|
||||||
TITLE "property apply"
|
TITLE "property apply"
|
||||||
|
|
||||||
ROOT_BENCH("apply 0 properties", function()
|
WRAP_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
|
||||||
|
|
@ -208,8 +182,8 @@ ROOT_BENCH("apply 0 properties", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("apply 8 properties", function()
|
WRAP_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
|
||||||
|
|
@ -226,8 +200,8 @@ ROOT_BENCH("apply 8 properties", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("bind property", function()
|
WRAP_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)
|
||||||
|
|
@ -241,8 +215,8 @@ ROOT_BENCH("bind property", function()
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update binding", function()
|
WRAP_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)
|
||||||
|
|
@ -258,29 +232,11 @@ ROOT_BENCH("update binding", function()
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TITLE "switch()"
|
|
||||||
|
|
||||||
ROOT_BENCH("switch()", function()
|
|
||||||
local M = 2^8
|
|
||||||
|
|
||||||
local map = {}
|
|
||||||
for i = 1, M do
|
|
||||||
map[i] = function() return i end
|
|
||||||
end
|
|
||||||
|
|
||||||
local input = source(0)
|
|
||||||
vide.switch(input)(map)
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
input(bit32.band(i, M - 1) + 1) -- i % m + 1
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
TITLE "indexes()"
|
TITLE "indexes()"
|
||||||
|
|
||||||
N /= 1024
|
N /= 1024
|
||||||
|
|
||||||
ROOT_BENCH("indexes() all new", function()
|
WRAP_BENCH("indexes() all new", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -298,7 +254,7 @@ ROOT_BENCH("indexes() all new", function()
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("indexes() no change", function()
|
WRAP_BENCH("indexes() no change", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -318,7 +274,7 @@ ROOT_BENCH("indexes() no change", function()
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("indexes() all change", function()
|
WRAP_BENCH("indexes() all change", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -342,7 +298,7 @@ ROOT_BENCH("indexes() all change", function()
|
||||||
src(data)
|
src(data)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("indexes() all remove", function()
|
WRAP_BENCH("indexes() all remove", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -366,7 +322,7 @@ end)
|
||||||
|
|
||||||
TITLE "values()"
|
TITLE "values()"
|
||||||
|
|
||||||
ROOT_BENCH("values() all new", function()
|
WRAP_BENCH("values() all new", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -384,7 +340,7 @@ ROOT_BENCH("values() all new", function()
|
||||||
return nil
|
return nil
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("values() no change", function()
|
WRAP_BENCH("values() no change", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -404,7 +360,7 @@ ROOT_BENCH("values() no change", function()
|
||||||
src(data)
|
src(data)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("values() all change", function()
|
WRAP_BENCH("values() all change", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -429,7 +385,7 @@ ROOT_BENCH("values() all change", function()
|
||||||
src(data)
|
src(data)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("values() all remove", function()
|
WRAP_BENCH("values() all remove", function()
|
||||||
local data = {}
|
local data = {}
|
||||||
|
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
|
|
@ -449,99 +405,106 @@ ROOT_BENCH("values() all remove", function()
|
||||||
src(data)
|
src(data)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TITLE "context()"
|
N *= 1024
|
||||||
|
|
||||||
ROOT_BENCH("set context", function()
|
TITLE "cleanup"
|
||||||
local ctx = context()
|
|
||||||
|
WRAP_BENCH("register new cleanup", function()
|
||||||
|
local cleanup = cleanup
|
||||||
|
|
||||||
|
local cleaner = function() end
|
||||||
|
|
||||||
|
local callers = {}
|
||||||
|
|
||||||
|
for i = 1, N do
|
||||||
|
callers[i] = function(fn, v)
|
||||||
|
fn(v)
|
||||||
|
return i -- return unique upvalue to ensure unique closure
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
ctx(i, function() end)
|
callers[i](cleanup, cleaner)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("get context (depth=1)", function()
|
TITLE "aggregate"
|
||||||
local ctx = context()
|
|
||||||
|
do
|
||||||
|
-- the purpose of the two following benchmarks is to measure the overhead of
|
||||||
|
-- aggregate construction
|
||||||
|
WRAP_BENCH("set explicit mock vector2", function()
|
||||||
|
local apply = require "src/apply"
|
||||||
|
local Vector2 = require "test/mock".Vector2
|
||||||
|
|
||||||
|
local label = create "TextLabel" {
|
||||||
|
AnchorPoint = Vector2.new(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
local function run()
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
ctx()
|
apply(label, {
|
||||||
|
AnchorPoint = Vector2.new(i, i)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
ctx(1, function()
|
|
||||||
run()
|
|
||||||
end)
|
end)
|
||||||
end)
|
|
||||||
|
|
||||||
local depth = 10
|
WRAP_BENCH("set aggregate mock vector2", function()
|
||||||
ROOT_BENCH(`get context (depth={depth})`, function()
|
local apply = require "src/apply"
|
||||||
|
local Vector2 = require "test/mock".Vector2
|
||||||
|
|
||||||
local ctx = context()
|
local label = create "TextLabel" {
|
||||||
|
AnchorPoint = Vector2.new(1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
local function run()
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
ctx()
|
apply(label, {
|
||||||
|
AnchorPoint = { i, i }
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local function nest_effect(fn)
|
|
||||||
untrack(function()
|
|
||||||
effect(fn)
|
|
||||||
return nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local f = run
|
|
||||||
for i = 1, depth - 1 do
|
|
||||||
local f_inner = f
|
|
||||||
f = function()
|
|
||||||
nest_effect(f_inner)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ctx(1, function()
|
|
||||||
f()
|
|
||||||
end)
|
end)
|
||||||
end)
|
end
|
||||||
|
|
||||||
TITLE "spring()"
|
-- innacurate due to no Vector3 in vanilla Luau
|
||||||
|
-- mock vector is 200x slower than native vector
|
||||||
|
|
||||||
ROOT_BENCH("spring update", function()
|
-- WRAP_BENCH("spring update", function()
|
||||||
local root, source, spring = vide.root, vide.source, vide.spring
|
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
|
|
||||||
local src = source(0)
|
-- local src = source(0)
|
||||||
|
|
||||||
root(function()
|
-- root(function()
|
||||||
for i = 1, N do
|
-- for i = 1, N do
|
||||||
spring(src)
|
-- spring(src)
|
||||||
end
|
-- end
|
||||||
|
|
||||||
START(N)
|
-- START(N)
|
||||||
|
|
||||||
src(1)
|
-- src(1)
|
||||||
|
|
||||||
return nil
|
-- return nil
|
||||||
end)
|
-- end)
|
||||||
end)
|
-- end)
|
||||||
|
|
||||||
ROOT_BENCH("spring step", function()
|
-- N /= 1024
|
||||||
local root, source, spring = vide.root, vide.source, vide.spring
|
|
||||||
|
|
||||||
local src = source(0)
|
-- WRAP_BENCH("spring step", function()
|
||||||
|
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
|
|
||||||
root(function()
|
-- local src = source(0)
|
||||||
for i = 1, N do
|
|
||||||
spring(src)
|
|
||||||
end
|
|
||||||
|
|
||||||
src(1)
|
-- root(function()
|
||||||
|
-- for i = 1, N do
|
||||||
|
-- spring(src)
|
||||||
|
-- end
|
||||||
|
|
||||||
START(N)
|
-- src(1)
|
||||||
|
|
||||||
vide.step(1/60)
|
-- START(N)
|
||||||
|
|
||||||
return nil
|
-- vide.step(1/60)
|
||||||
end)
|
|
||||||
end)
|
-- 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 reset = "\27[H\27[2J" -- ANSI clear terminal
|
local spring = vide.spring
|
||||||
local offset = string.rep("\n", OFFSET - h_f)
|
local effect = vide.effect
|
||||||
local bar = remainder_to_block(h - h_f) .. "\n" .. string.rep(BLOCK .. "\n", h_f)
|
|
||||||
--print(reset .. offset .. bar .. "\n" .. string.format("%.1f", h))
|
local value = source(MAX)
|
||||||
print(reset .. offset .. bar .. "\n" .. h)
|
local sprung = spring(value, 1, 0.3)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
local v = sprung()
|
||||||
|
local fv = math.floor(v)
|
||||||
|
local reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||||
|
local offset = string.rep("\n", MAX - fv + OFFSET)
|
||||||
|
local bar = testkit.color.gray(remainder_to_block(v - fv) .. "\n" .. string.rep(BLOCK .. "\n", fv))
|
||||||
|
print(reset .. offset .. bar .. "\n" .. v)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local T = 3
|
||||||
|
local elapsed = T/1.2
|
||||||
|
repeat local dt = step()
|
||||||
|
vide.step(dt)
|
||||||
|
|
||||||
|
elapsed += dt
|
||||||
|
while elapsed >= T do
|
||||||
|
elapsed -= T
|
||||||
|
value(value() == MAX and MIN or MAX)
|
||||||
|
end
|
||||||
|
|
||||||
|
until false
|
||||||
end
|
end
|
||||||
|
|
||||||
local program_time = os.clock()
|
vide.root(main)
|
||||||
|
|
||||||
local function step(): number
|
|
||||||
local FPS = 30
|
|
||||||
local DT = 1/FPS
|
|
||||||
|
|
||||||
repeat until os.clock() - program_time >= DT
|
|
||||||
program_time += DT
|
|
||||||
return DT
|
|
||||||
end
|
|
||||||
|
|
||||||
local function loop()
|
|
||||||
local callback = system()
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local dt = step()
|
|
||||||
local x = callback(dt)
|
|
||||||
redraw_block(x)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
loop()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- testkit.luau
|
-- testkit.luau
|
||||||
-- v0.7.2
|
-- v0.7.1
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
local color = {
|
local color = {
|
||||||
|
|
@ -323,7 +323,7 @@ end
|
||||||
|
|
||||||
local function print2(v: unknown)
|
local function print2(v: unknown)
|
||||||
type Buffer = { n: number, [number]: string }
|
type Buffer = { n: number, [number]: string }
|
||||||
type Cyclic = { n: number, [{}]: number }
|
type Cyclic = { [{}]: true }
|
||||||
|
|
||||||
-- overkill concatenationless string buffer
|
-- overkill concatenationless string buffer
|
||||||
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
|
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
|
||||||
|
|
@ -347,19 +347,16 @@ local function print2(v: unknown)
|
||||||
else -- is table
|
else -- is table
|
||||||
local tabbed_indent = indent .. TAB
|
local tabbed_indent = indent .. TAB
|
||||||
|
|
||||||
|
str.n += 1
|
||||||
|
|
||||||
if cyclic[value] then
|
if cyclic[value] then
|
||||||
str.n += 1
|
str[str.n] = color.gray "*cyclic reference*"
|
||||||
str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
|
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
cyclic.n += 1
|
cyclic[value] = true
|
||||||
cyclic[value] = cyclic.n
|
|
||||||
end
|
end
|
||||||
|
|
||||||
str.n += 3
|
str[str.n] = "{\n"
|
||||||
str[str.n - 2] = "{ "
|
|
||||||
str[str.n - 1] = color.gray(tostring(cyclic[value]))
|
|
||||||
str[str.n - 0] = "\n"
|
|
||||||
|
|
||||||
local i, v = next(value, nil)
|
local i, v = next(value, nil)
|
||||||
while v ~= nil do
|
while v ~= nil do
|
||||||
|
|
@ -396,7 +393,7 @@ local function print2(v: unknown)
|
||||||
end
|
end
|
||||||
|
|
||||||
local str = { n = 0 }
|
local str = { n = 0 }
|
||||||
local cyclic = { n = 0 }
|
local cyclic = {}
|
||||||
tos(v, 0, str, cyclic)
|
tos(v, 0, str, cyclic)
|
||||||
print(table.concat(str))
|
print(table.concat(str))
|
||||||
end
|
end
|
||||||
|
|
@ -458,7 +455,7 @@ return {
|
||||||
return BENCH, START
|
return BENCH, START
|
||||||
end,
|
end,
|
||||||
|
|
||||||
print = print2,
|
print2 = print2,
|
||||||
|
|
||||||
seq = shallow_eq,
|
seq = shallow_eq,
|
||||||
deq = deep_eq,
|
deq = deep_eq,
|
||||||
|
|
|
||||||
1881
test/tests.luau
1881
test/tests.luau
File diff suppressed because it is too large
Load diff
13
todo.md
13
todo.md
|
|
@ -1,4 +1,13 @@
|
||||||
# todo
|
# todo
|
||||||
|
|
||||||
- improve error traces
|
- property binding optimization
|
||||||
- prevent redundant re-eval of nodes in a complex diamond graph
|
- would no longer allow `cleanup()` usage in binding scopes
|
||||||
|
- solution to nested reactivity, see: SolidJS stores
|
||||||
|
- optimize wide graph updating
|
||||||
|
- implement from solid:
|
||||||
|
- Portal
|
||||||
|
- batch
|
||||||
|
- optimize `indexes()` double-diffing
|
||||||
|
- define behavior of deriving a source within a derived source
|
||||||
|
- define behavior of node destruction under a root that has a child in another root
|
||||||
|
- improve crash course, some sections feel like information dumps
|
||||||
|
|
|
||||||
|
|
@ -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.1.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