mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Compare commits
No commits in common. "main" and "0.3.1" have entirely different histories.
84 changed files with 2207 additions and 3426 deletions
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
|
|
@ -1,33 +0,0 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: setup rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: build
|
||||
run: rojo build default.project.json -o build.rbxm
|
||||
|
||||
- name: release
|
||||
run: gh release upload ${{github.event.release.tag_name}} build.rbxm
|
||||
|
||||
|
||||
|
||||
13
.github/workflows/deploy.yml
vendored
13
.github/workflows/deploy.yml
vendored
|
|
@ -1,6 +1,9 @@
|
|||
name: site-deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main] # todo: remove later
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
@ -21,21 +24,21 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build with VitePress
|
||||
run: npm run docs:build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
|
|
@ -49,4 +52,4 @@ jobs:
|
|||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v2
|
||||
|
|
|
|||
6
.github/workflows/unit-test.yml
vendored
6
.github/workflows/unit-test.yml
vendored
|
|
@ -8,13 +8,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Luau zip
|
||||
uses: robinraju/release-downloader@v1.9
|
||||
uses: robinraju/release-downloader@v1.6
|
||||
with:
|
||||
repository: Roblox/luau
|
||||
tag: "0.679"
|
||||
tag: "0.620"
|
||||
fileName: luau-ubuntu.zip
|
||||
out-file-path: bin
|
||||
|
||||
|
|
|
|||
54
.github/workflows/wallypesde.yml
vendored
54
.github/workflows/wallypesde.yml
vendored
|
|
@ -1,54 +0,0 @@
|
|||
name: publish to wally and pesde
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
wally:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: setup rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: login
|
||||
run: wally login --token "${{ secrets.WALLY_TOKEN }}"
|
||||
|
||||
- name: publish
|
||||
run: wally publish
|
||||
|
||||
pesde:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: setup rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: setup pesde
|
||||
run: pesde install
|
||||
|
||||
- name: login
|
||||
run: pesde auth login --token "${{ secrets.PESDE_TOKEN }}"
|
||||
|
||||
- name: publish
|
||||
run: pesde publish --yes
|
||||
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,16 +1,10 @@
|
|||
.vscode
|
||||
_local
|
||||
|
||||
aftman.toml
|
||||
sourcemap.json
|
||||
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
docs/node_modules
|
||||
docs/package-lock.json
|
||||
|
||||
luau_packages/
|
||||
lune_packages/
|
||||
.pesde/
|
||||
|
||||
wally.lock
|
||||
pesde.lock
|
||||
|
|
|
|||
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -6,57 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.4.1] - 2026-07-11
|
||||
|
||||
### Changed
|
||||
|
||||
- `branch()` is now allowed to be used within a reactive scope.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `create()` overloads. Supported is now only `create(class)(props)`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `create()` types in the new solver should now work without `::`.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.4.0] - 2026-01-17
|
||||
|
||||
### Added
|
||||
|
||||
- `create("ClassName", { props })` and `create(Instance, { props })` syntax.
|
||||
- `cleanup()` now accepts `thread` types.
|
||||
- Implicit effects to set children can now recursively create more implicit
|
||||
effects to set children.
|
||||
- `spring()` returns a second value, a setter to set position, velocity and
|
||||
impulse.
|
||||
- Improved `spring()` updating and unscheduling.
|
||||
- `show()` now receives a source to its callback returning the current value
|
||||
of the condition.
|
||||
- Ignore `false` passed as a child.
|
||||
- Flag `vide.defaults` to disable the setting of default properties.
|
||||
- Delayed scope destruction for control flow functions: `show()` `switch()` `indexes()` `values()`.
|
||||
- Better `create()` types for the new type solver.
|
||||
|
||||
### Changed
|
||||
|
||||
- A scope can no longer be destroyed while it is active. Strict mode will check
|
||||
for this.
|
||||
- Implicit effects to set children now unparent all children when the effect is
|
||||
destroyed.
|
||||
- Error reporting should be improved with better formatting when effects invoke
|
||||
other effects and no more loss of stack traces.
|
||||
- Nesting parent properties now work, and they are now also checked for
|
||||
duplicates like other properties.
|
||||
|
||||
### Removed
|
||||
|
||||
- Aggregate construction when setting properties with `create()`.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.3.1] - 2024-10-09
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ for a quick introduction to the library.
|
|||
|
||||
## Code sample
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
|
|||
|
|
@ -1,83 +1,70 @@
|
|||
//import { defineConfig } from "vitepress"
|
||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid({
|
||||
title: "Vide",
|
||||
titleTemplate: ":title - A reactive UI library for Luau",
|
||||
description: "A reactive UI library for Luau.",
|
||||
base: "/vide/",
|
||||
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
|
||||
|
||||
themeConfig: {
|
||||
logo: "/logo.svg",
|
||||
|
||||
search: {
|
||||
provider: "local"
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "API", link: "/api/reactivity-core"},
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
"/api/": [
|
||||
{
|
||||
text: "API",
|
||||
items: [
|
||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
||||
{ text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
|
||||
{ text: "Element Creation", link: "/api/creation" },
|
||||
{ text: "Animation", link: "/api/animation" },
|
||||
{ text: "Strict Mode", link: "/api/strict-mode" },
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"/tut/": [
|
||||
{
|
||||
text: "Crash Course",
|
||||
items: [
|
||||
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
|
||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
|
||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
|
||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Advanced",
|
||||
items: [
|
||||
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Design Patterns",
|
||||
items: [
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/centau/vide" }
|
||||
]
|
||||
}
|
||||
})
|
||||
//import { defineConfig } from "vitepress"
|
||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid({
|
||||
title: "Vide",
|
||||
titleTemplate: ":title - A reactive UI library for Luau",
|
||||
description: "A reactive UI library for Luau.",
|
||||
base: "/vide/",
|
||||
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
|
||||
|
||||
themeConfig: {
|
||||
logo: "/logo.svg",
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "API", link: "/api/reactivity-core"},
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
"/api/": [
|
||||
{
|
||||
text: "API",
|
||||
items: [
|
||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
||||
{ text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
|
||||
{ text: "Element Creation", link: "/api/creation" },
|
||||
{ text: "Animation", link: "/api/animation" },
|
||||
{ text: "Strict Mode", link: "/api/strict-mode" },
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"/tut/": [
|
||||
{
|
||||
text: "Crash Course",
|
||||
items: [
|
||||
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
|
||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Advanced Reactivity",
|
||||
items: [
|
||||
{ text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/centau/vide" }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
.home > * > .VPNavBar:not(.top) {
|
||||
backdrop-filter: blur(0.5rem);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.home > * > .VPNavBar > .divider {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.home > .VPContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.VPHome {
|
||||
margin-top: auto !important;
|
||||
margin-bottom: auto !important;
|
||||
}
|
||||
|
||||
.VPBadge a {
|
||||
text-decoration: none;
|
||||
color: inherit
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './vars.css'
|
||||
import './index.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
}
|
||||
// .vitepress/theme/index.js
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './vars.css'
|
||||
export default DefaultTheme
|
||||
|
|
|
|||
|
|
@ -1,52 +1,3 @@
|
|||
/* Colors */
|
||||
|
||||
:root {
|
||||
--vp-c-brand-1: #3086ff;
|
||||
--vp-c-brand-2: #75aeff;
|
||||
|
||||
--vp-button-brand-bg: #3661a2;
|
||||
--vp-button-brand-hover-bg: #24447f;
|
||||
--vp-button-brand-press-bg: #4896f3;
|
||||
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#3661a2,
|
||||
#4896f3
|
||||
);
|
||||
--vp-home-hero-logo-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#3661a2,
|
||||
#4896f3
|
||||
);
|
||||
|
||||
--vp-home-hero-image-filter: blur(96px);
|
||||
|
||||
--vp-c-bg: #f2f5f8;
|
||||
--vp-c-bg-alt: #dfe8f5;
|
||||
--vp-c-bg-elv: #dde7f4;
|
||||
--vp-c-bg-soft: #e8f1fe;
|
||||
|
||||
--vp-c-border: #c0c3c6;
|
||||
--vp-c-divider: #dfe2e6;
|
||||
--vp-c-gutter: #dfe2e6;
|
||||
--vp-plugin-tabs-tab-bg: var(--vp-c-bg);
|
||||
|
||||
--vp-badge-info-bg: #122d26;
|
||||
--vp-badge-info-text: #6bdbbd;
|
||||
--vp-badge-tip-bg: #132741;
|
||||
--vp-badge-tip-text: #70abfa;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-brand-2: #234782;
|
||||
--vp-c-bg: #0d131b;
|
||||
--vp-c-bg-alt: #111720;
|
||||
--vp-c-bg-elv: #182231;
|
||||
--vp-c-bg-soft: #182231;
|
||||
|
||||
--vp-c-border: #111720;
|
||||
--vp-c-divider: #1d273c;
|
||||
--vp-c-gutter: #181d27;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vp-c-brand-1: #3086ff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,25 @@
|
|||
# Animation
|
||||
# Animation API
|
||||
|
||||
## spring() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
## spring()
|
||||
|
||||
Returns a new source with a value always moving torwards the input source value.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function spring<T>(
|
||||
source: () -> T & Animatable,
|
||||
period: number = 1,
|
||||
damping_ratio: number = 1
|
||||
): (() -> T, SpringControl<T>)
|
||||
): () -> T
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
||||
|
||||
type SpringControl<T> = ({
|
||||
position: T?,
|
||||
velocity: T?,
|
||||
impulse: T?
|
||||
}) -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
An effect is created to update the new source every frame based on the input
|
||||
source value.
|
||||
|
||||
The movement is physically simulated according to a
|
||||
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
||||
|
|
@ -44,7 +39,3 @@ Returns a new source with a value always moving torwards the input source value.
|
|||
You can change when the solver runs by calling `vide.step(dt)`, which will
|
||||
advance the simulation time by `dt` seconds and automatically stop the
|
||||
solver running in heartbeat.
|
||||
|
||||
::: warning
|
||||
Large periods or damping ratios can break the spring.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,4 +1,39 @@
|
|||
# Element Creation
|
||||
# Element Creation API
|
||||
|
||||
<br/>
|
||||
|
||||
## mount()
|
||||
|
||||
Runs a function in a new stable scope and optionally applies its result to a
|
||||
target instance.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The result of the function is applied to a target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
The function is ran in a new stable scope, just like
|
||||
[root()](reactivity-core.md#root).
|
||||
|
||||
Returns a function that when called will destroy the stable scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "Vide" }
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
## create()
|
||||
|
||||
|
|
@ -6,11 +41,11 @@ Creates a new UI element, applying any given properties.
|
|||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function create(class: string): (Properties) -> Instance
|
||||
function create(instance: Instance): (Properties) -> Instance
|
||||
|
||||
type Properties = Map<string|number, unknown>
|
||||
type Properties = Map<string|number, any>
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
|
@ -41,53 +76,73 @@ Creates a new UI element, applying any given properties.
|
|||
|
||||
Basic element creation.
|
||||
|
||||
```luau
|
||||
local frame = create "TextButton" {
|
||||
Name = "Button",
|
||||
Size = UDim2.fromOffset(200, 160),
|
||||
|
||||
Activated = function()
|
||||
print "clicked"
|
||||
end,
|
||||
|
||||
create "UICorner" {}
|
||||
```lua
|
||||
local frame = create "Frame" {
|
||||
Name = "NewFrame",
|
||||
Position = UDim2.fromScale(1, 0)
|
||||
}
|
||||
```
|
||||
|
||||
A component using property nesting.
|
||||
|
||||
```lua
|
||||
type Layout = {
|
||||
Layout = {
|
||||
Position: UDim2?,
|
||||
Size: UDim2?,
|
||||
AnchorPoint: Vector2?
|
||||
}
|
||||
}
|
||||
|
||||
type Children = {
|
||||
Children = Array<Instance>
|
||||
}
|
||||
|
||||
function Background(props: Layout & Children & {
|
||||
Color: Color3
|
||||
})
|
||||
return create "Frame" {
|
||||
BackgroundColor3 = props.Color,
|
||||
props.Layout,
|
||||
props.Children
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
## action()
|
||||
|
||||
Creates a special object that can be passed to `create()` to invoke custom
|
||||
actions on instances.
|
||||
Creates a callback that can be passed to `create()` to invoke custom actions on
|
||||
instances.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function action((Instance) -> (), priority: number = 1): Action
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
When passed to `create()`, the function is called with the instance being
|
||||
created as the only argument. Actions take precedence over property and
|
||||
child assignments.
|
||||
When passed to `create()`, the given callback is called with the instance
|
||||
being created as the only argument. Actions take precedence over property
|
||||
and child assignments.
|
||||
|
||||
A priority can be optionally specified to ensure certain actions run after
|
||||
other actions. Lower priority values are ran first.
|
||||
other actions. Higher priority numbers are ran after lower priority numbers.
|
||||
|
||||
- **Example**
|
||||
|
||||
An action to listen to changed properties:
|
||||
|
||||
```luau
|
||||
local function changed(property: string, fn: (new) -> ())
|
||||
```lua
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
fn(instance[property])
|
||||
local con - instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
-- disconnect on scope destruction to allow gc of instance
|
||||
-- disconnect on reactive scope destruction to allow gc of instance
|
||||
cleanup(function()
|
||||
cn:Disconnect()
|
||||
con:Disconnect()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
|
@ -95,7 +150,7 @@ actions on instances.
|
|||
local output = source ""
|
||||
|
||||
create "TextBox" {
|
||||
-- will update the output source anytime the text property is changed
|
||||
-- will update the `output` source anytime the text property is changed
|
||||
changed("Text", output)
|
||||
}
|
||||
```
|
||||
|
|
@ -106,47 +161,16 @@ A wrapper for `action()` to listen for property changes.
|
|||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function changed(property: string, fn: (unknown) -> ()): Action
|
||||
```lua
|
||||
function changed(property: string, callback: (...unknown) -> ()): Action
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Will run the given function immediately and whenever the property updates.
|
||||
Will run the given callback any time the property is changed, as well as
|
||||
when the action is initially run.
|
||||
|
||||
The function is called with the updated property value.
|
||||
The changed connection is disconnected when the scope the action is ran in
|
||||
is destroyed.
|
||||
|
||||
Runs with an action priority of 1.
|
||||
|
||||
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||
|
||||
Runs a function in a new stable scope and optionally applies its result to a
|
||||
target instance.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
This is a utility for `root()` when parenting a component to an existing
|
||||
instance.
|
||||
|
||||
The result of the function is applied to a target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
Returns a function that when called will destroy the stable scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "Vide" }
|
||||
}
|
||||
end
|
||||
|
||||
local destroy = mount(App, game.StarterGui)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,44 +1,36 @@
|
|||
# Reactivity: Core
|
||||
# Reactivity API: Core
|
||||
|
||||
## Scopes
|
||||
|
||||
Vide code can run in one of two scopes: <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge> or <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>.
|
||||
|
||||
- Reactive scopes rerun if a source read within updates.
|
||||
- Stable scopes never rerun.
|
||||
- Reactive scopes cannot be created directly within another reactive scope.
|
||||
- When a scope is destroyed, all scopes created within are also destroyed.
|
||||
|
||||
Different functions in Vide's API will run code in different scopes.
|
||||
<br/>
|
||||
|
||||
:::warning
|
||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
||||
for this.
|
||||
:::
|
||||
|
||||
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||
## root()
|
||||
|
||||
Runs a function in a new stable scope.
|
||||
Creates and runs a function in a new stable scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
||||
|
||||
type Destructor = () -> ()
|
||||
```lua
|
||||
function root<T...>(fn: (() -> ()) -> T...): (() -> (), T...)
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a destructor and any values returned by the callback.
|
||||
Returns a function to destroy the root scope. Also passes this function as
|
||||
the first argument into its callback.
|
||||
|
||||
All values returned by the callback are also returned following the destructor.
|
||||
|
||||
## source()
|
||||
|
||||
Creates a new source.
|
||||
Creates a new source with the given value.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function source<T>(value: T): Source<T>
|
||||
|
||||
type Source<T> =
|
||||
|
|
@ -48,108 +40,83 @@ Creates a new source.
|
|||
|
||||
- **Details**
|
||||
|
||||
Call the returned source with no argument to read its value.
|
||||
Call the returned source with an argument to set its value.
|
||||
Calling the returned source with no argument will return its stored value,
|
||||
calling with an argument will set a new value.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
print(count())-- 0
|
||||
count(count() + 1)
|
||||
print(count()) -- 1
|
||||
|
||||
count() -- 0
|
||||
|
||||
count(count() + 1) -- 1
|
||||
```
|
||||
|
||||
## effect() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
## effect()
|
||||
|
||||
Runs a function in a new reactive scope.
|
||||
Runs a side-effect in a new reactive scope on source update.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function effect(fn: () -> ())
|
||||
```lua
|
||||
function effect(callback: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The function is ran once immediately.
|
||||
Any time a source referenced in the callback is updated, the callback will
|
||||
be reran.
|
||||
|
||||
The callback is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local count = source(1)
|
||||
```lua
|
||||
local num = source(1)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
print(num())
|
||||
end)
|
||||
|
||||
-- prints 1
|
||||
|
||||
count(2)
|
||||
num(num() + 1)
|
||||
|
||||
-- prints 2
|
||||
```
|
||||
|
||||
## derive() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
## derive()
|
||||
|
||||
Runs a function in a new reactive scope to compute a value for new source.
|
||||
Derives a new source in a new reactive scope from existing sources.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function derive<T>(fn: () -> T): () -> T
|
||||
```lua
|
||||
function derive<T>(source: () -> T): () -> T
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Anytime the reactive scope reruns, the output source value is set to what is
|
||||
returned.
|
||||
The derived source will have its value recalculated when any source source
|
||||
it derives from is updated.
|
||||
|
||||
The function is ran once immediately.
|
||||
Anytime its value is recalculated it is also cached, subsequent calls will
|
||||
retun this cached value until it recalculates again.
|
||||
|
||||
The callback is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
local text = derive(function() return `count: {count()}` end)
|
||||
|
||||
print(text()) -- "count: 0"
|
||||
text() -- "count: 0"
|
||||
|
||||
count(1)
|
||||
|
||||
print(text()) -- "count: 1"
|
||||
text() -- "count: 1"
|
||||
```
|
||||
|
||||
A `derive()` should be used instead of a pure function when you expect it to
|
||||
be read multiple times between updates, because `derive()` will cache the
|
||||
result to prevent recomputing it on every read.
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [Pure Function]
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
print "ran"
|
||||
return `count: {count()}`
|
||||
end
|
||||
|
||||
count(1)
|
||||
print(text()) -- prints "ran" followed by "count: 1"
|
||||
print(text()) -- prints "ran" followed by "count: 1"
|
||||
```
|
||||
|
||||
```luau [Derived Source]
|
||||
local count = source(0)
|
||||
|
||||
local text = derive(function() -- [!code highlight]
|
||||
print "ran"
|
||||
return `count: {count()}`
|
||||
end) -- [!code highlight]
|
||||
|
||||
count(1) -- prints "ran"
|
||||
print(text()) -- prints "count: 1"
|
||||
print(text()) -- prints "count: 1"
|
||||
```
|
||||
|
||||
:::
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,229 +0,0 @@
|
|||
# Reactivity: Dynamic Scopes
|
||||
|
||||
Dynamic scopes are scopes that are created or destroyed in response to
|
||||
source updates. Vide provides functions for some common use-cases for dynamic scopes.
|
||||
|
||||
## show() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
|
||||
Shows a component if the source is truthy. Optionally shows a fallback component
|
||||
if the source is falsey.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function show<T>(source: () -> unknown, component: Constructor<T>): () -> T?
|
||||
function show<T, U>(source: () -> unknown, component: Constructor<T>, fallback: () -> U): () -> T | U
|
||||
|
||||
type Constructor<T> = () -> (T, number?)
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
|
||||
The component is run in a stable scope when truthy, otherwise the stable
|
||||
scope is destroyed.
|
||||
|
||||
Returns a source holding an instance of the currently shown component or
|
||||
`nil` if no component is currently shown.
|
||||
|
||||
Destruction of the scope can be delayed by returning the number of seconds
|
||||
to delay by, after the component.
|
||||
|
||||
## switch() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
|
||||
Shows one of a set of components depending on a source and a mapping table.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function switch<K, V>(source: () -> K): (map: Map<K, Constructor<V>>): () -> V?
|
||||
|
||||
type Constructor<T> = () -> (T, number?)
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
|
||||
When the source updates, its value is inputted into a map to get a component
|
||||
constructor. This component is then run in a stable scope. The previous
|
||||
stable scope is destroyed.
|
||||
|
||||
Returns a source holding an instance of the currently shown component or
|
||||
`nil` if no component is currently shown.
|
||||
|
||||
Destruction of the scope can be delayed by returning the number of seconds
|
||||
to delay by, after the component.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local logged = source(false)
|
||||
|
||||
local button = switch(logged) {
|
||||
[true] = function()
|
||||
return Button { Text = "Log out", Toggle = logged }
|
||||
end,
|
||||
|
||||
[false] = function()
|
||||
return Button { Text = "Log in", Toggle = logged }
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
## indexes() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
|
||||
Shows a component for each index in a table.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function indexes<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
constructor: (value: () -> VI, index: KI) -> (VO, number?)
|
||||
): Array<VO>
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
|
||||
When the source table updates, a component is generated for each index in
|
||||
the table.
|
||||
|
||||
- For any added index, the `constructor` function is run in a new stable
|
||||
scope to produce an instance that is cached.
|
||||
- For any removed index, the stable scope for that index is destroyed.
|
||||
|
||||
The `constructor` function is called with:
|
||||
|
||||
1. A *source containing the index's value*.
|
||||
2. The *index itself*.
|
||||
|
||||
Anytime an existing index's value changes, the `constructor` function is not
|
||||
rerun, instead, that index's corresponding source is updated with the new
|
||||
value.
|
||||
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
Destruction of the scope can be delayed by returning the number of seconds
|
||||
to delay by, after the component.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = indexes(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return i .. ": " .. item().name
|
||||
end,
|
||||
|
||||
Image = function()
|
||||
return "rbxassetid://" .. item().icon
|
||||
end,
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
## values() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||
|
||||
Shows a component for each value in a table.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function values<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
constructor: (value: VI, index: () -> KI) -> (VO, number?)
|
||||
): Array<VO>
|
||||
|
||||
- **Details**
|
||||
|
||||
Operates with the same idea as `indexes()`, but applied to values instead of
|
||||
indexes.
|
||||
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
|
||||
When the source table updates, a component is generated for each value in
|
||||
the table.
|
||||
|
||||
- For any added value, the `constructor` function is run in a new stable scope
|
||||
to produce an instance that is cached.
|
||||
- For any removed value, the stable scope for that value is destroyed.
|
||||
|
||||
The `constructor` function is called with:
|
||||
|
||||
1. The *value itself*.
|
||||
2. A *source containing the value's index*.
|
||||
|
||||
Anytime an existing value's index changes, the `constructor` function is not
|
||||
rerun, instead, that value's corresponding source is updated with the new
|
||||
index.
|
||||
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
Destruction of the scope can be delayed by returning the number of seconds
|
||||
to delay by, after the component.
|
||||
|
||||
::: warning
|
||||
Having the same values appear multiple times in the input source table can
|
||||
cause unexpected behavior. Strict mode has checks for this.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = values(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return i() .. ": " .. item.Name
|
||||
end
|
||||
|
||||
Image = "rbxassetid://" .. item.icon,
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
- **Extra**
|
||||
|
||||
When should you use `indexes()` and `values()`?
|
||||
|
||||
`values()` should be used when you have a fixed set of objects where the
|
||||
same objects can be re-arranged in the source table. It maps a value to a
|
||||
UI element.
|
||||
|
||||
e.g.
|
||||
- List of all players.
|
||||
- Inventory of items.
|
||||
- Chat message history.
|
||||
- Toast notifications.
|
||||
|
||||
`indexes()` should be used in other cases, especially when your source table
|
||||
has primitive values. It maps an index to a UI element.
|
||||
|
||||
e.g.
|
||||
- List of character or weapon stats.
|
||||
|
||||
In most cases, both functions will produce the same observed result.
|
||||
The main difference is performance, picking the right function to use can
|
||||
result in less property updates and less re-renders. One case to note is
|
||||
that `values()` works nicely when animating re-ordering of instances, since
|
||||
the source index can be used to animate a change in position for the UI
|
||||
element.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
218
docs/api/reactivity-flow.md
Normal file
218
docs/api/reactivity-flow.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Reactivity API: Control Flow
|
||||
|
||||
<br/>
|
||||
|
||||
## show()
|
||||
|
||||
Shows one of two components depending on an input source.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function show<T>(source: () -> unknown, component: () -> T): () -> T?
|
||||
function show<T, U>(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
When the input source changes from a falsey to a truthy value, the
|
||||
component will be reran under a new stable scope. If it changes from a
|
||||
truthy to falsey value, the stable scope the component was created in will
|
||||
be destroyed, and the returned source will output `nil`, or a fallback
|
||||
component if given.
|
||||
|
||||
The fallback component is also ran under a new stable scope, and destroyed
|
||||
when the input source switches back to truthy.
|
||||
|
||||
## switch()
|
||||
|
||||
Shows one of a set of components depending on an input source and a mapping table.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
When the input source changes, the new value will be used to lookup a given
|
||||
mapping table to get a component, which will be ran under a new stable
|
||||
scope. If the input source changes, the stable scope the component was
|
||||
created in will be destroyed, and a new component created under a new
|
||||
stable scope. If no component is found for an input value, the switch will
|
||||
output `nil`.
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
local logged = source(false)
|
||||
|
||||
local button = switch(logged) {
|
||||
[true] = function()
|
||||
return Button { Text = "Log out", Toggle = logged }
|
||||
end,
|
||||
|
||||
[false] = function()
|
||||
return Button { Text = "Log in", Toggle = logged }
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
## indexes()
|
||||
|
||||
Maps each index in a table source to an object.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function indexes<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: () -> VI, index: KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
When the input source changes, each *index* in the new table is compared with
|
||||
the last input table.
|
||||
|
||||
- For any new index, the `transform` function is ran under a new stable
|
||||
scope to produce a new instance.
|
||||
- For any removed index, the stable scope for that index is destroyed.
|
||||
- Unchanged indexes are untouched.
|
||||
|
||||
The transform function is called only ever *once* for each index in the
|
||||
source table.
|
||||
|
||||
1. First argument is a *source containing the index's value*.
|
||||
2. Second argument is the *index itself*.
|
||||
|
||||
Anytime an existing index's value changes, the transform function is not
|
||||
rerun, instead the source value for that index will update, causing anything
|
||||
depending on it to update too.
|
||||
|
||||
- **Example**
|
||||
|
||||
The intended purpose of this function is to map each index in a table to
|
||||
a UI element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = indexes(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return i .. ": " .. item().name
|
||||
end,
|
||||
|
||||
Image = function()
|
||||
return "rbxassetid://" .. item().icon
|
||||
end,
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
## values()
|
||||
|
||||
Maps each value in a table source to an object.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function values<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: VI, index: () -> KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a source holding an array of instances currently shown.
|
||||
|
||||
When the input source changes, each *value* in the new table is compared with
|
||||
the last input table. Similar to `indexes()` but for values instead of indexes.
|
||||
|
||||
- For any new value, the `transform` function is ran under a new stable
|
||||
scope to produce a new instance.
|
||||
- For any removed value, the stable scope for that value is destroyed.
|
||||
- Unchanged values are untouched.
|
||||
|
||||
The transform function is only ever called *once* for each value in the
|
||||
source table.
|
||||
|
||||
1. First argument is the *value itself*.
|
||||
2. Second argument is a *source containing the value's index*.
|
||||
|
||||
Anytime an existing value's index changes, the transform function is not
|
||||
rerun, instead the source index for that value will update, causing anything
|
||||
depending on it to update too.
|
||||
|
||||
::: warning
|
||||
Having primitive values in the input source table can cause unexpected
|
||||
behavior, as duplicate values can result in multiple tranforms being ran for
|
||||
a single value, meaning there can be multiple source indexes bound to the
|
||||
same UI element. Strict mode has checks for this.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
||||
The intended purpose of this function is to map each value in a table to
|
||||
a UI element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = values(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return i() .. ": " .. item.Name
|
||||
end
|
||||
|
||||
Image = "rbxassetid://" .. item.icon,
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
- **Extra**
|
||||
|
||||
When should you use `indexes()` and `values()`?
|
||||
|
||||
`values()` should be used when you have a fixed set of objects where the
|
||||
same objects can be re-arranged in the source table. It maps a value to a
|
||||
UI element.
|
||||
|
||||
e.g.
|
||||
- List of all players.
|
||||
- Inventory of items.
|
||||
- Chat message history.
|
||||
- Toast notifications.
|
||||
|
||||
`indexes()` should be used in other cases, especially when your source table
|
||||
has primitive values. It maps an index to a UI element.
|
||||
|
||||
e.g.
|
||||
- List of character or weapon stats.
|
||||
|
||||
In most cases, both functions will appear to have the same behavior.
|
||||
The main difference is performance, picking the right function to use can
|
||||
result in less property updates and less re-renders. One case to note is
|
||||
that `values()` works nicely when animating re-ordering of instances, since
|
||||
the value is not destroyed when indexes are changed, and the source index
|
||||
can be used to animate a change in position for the UI element.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
@ -1,47 +1,41 @@
|
|||
# Reactivity: Utility
|
||||
# Reactivity API: Utility
|
||||
|
||||
## cleanup()
|
||||
|
||||
Queues a callback to run when a scope is reran or destroyed.
|
||||
Runs a callback anytime a scope is reran or destroyed.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function cleanup(v: Function | Disconnectable | Destroyable | thread)
|
||||
```lua
|
||||
function cleanup(callback: () -> ())
|
||||
function cleanup(obj: Destroyable)
|
||||
function cleanup(obj: Disconnectable)
|
||||
|
||||
type Function = () -> ()
|
||||
type Destroyable = { destroy: () -> () }
|
||||
type Disconnectable = { disconnect: () -> () }
|
||||
```
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local count = source(0)
|
||||
```lua
|
||||
local data = source(1)
|
||||
|
||||
local destroy = root(function()
|
||||
effect(function()
|
||||
count()
|
||||
effect(function()
|
||||
local label = create "TextLabel" { Text = data() }
|
||||
|
||||
cleanup(function()
|
||||
print "cleaned"
|
||||
end)
|
||||
cleanup(function()
|
||||
label:Destroy()
|
||||
end)
|
||||
end
|
||||
|
||||
-- nothing printed yet
|
||||
count(1) -- prints "cleaned"
|
||||
count(2) -- prints "cleaned"
|
||||
destroy() -- prints "cleaned"
|
||||
end)
|
||||
```
|
||||
|
||||
## untrack() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||
## untrack()
|
||||
|
||||
Runs a function in a new stable scope.
|
||||
Runs a given function in a new stable scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function untrack<T>(source: () -> T): T
|
||||
```
|
||||
|
||||
|
|
@ -52,7 +46,7 @@ Runs a function in a new stable scope.
|
|||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local a = source(0)
|
||||
local b = source(0)
|
||||
|
||||
|
|
@ -61,84 +55,65 @@ Runs a function in a new stable scope.
|
|||
end)
|
||||
|
||||
print(sum()) -- 0
|
||||
b(1) -- untracked so reactive scope created by derive() does not rerun
|
||||
b(1)
|
||||
print(sum()) -- 0
|
||||
a(1) -- reactive scope created by derive() reruns
|
||||
a(1)
|
||||
print(sum()) -- 2
|
||||
```
|
||||
|
||||
## read()
|
||||
|
||||
Utility used to read a value that is either a primitive or a source.
|
||||
Utility used to read a value that is either a primitive or a source. Sources
|
||||
read can still be tracked inside a reactive scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function read<T>(value: T | () -> T): T
|
||||
```
|
||||
|
||||
## batch()
|
||||
|
||||
Runs a function where any source updates made within the function do not
|
||||
trigger effects until after the function ends.
|
||||
Runs a given function where any source updates made within the function do not
|
||||
trigger effects until after the function finishes running.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function batch(fn: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Improves performance when an effect depends on multiple sources, and those
|
||||
sources need to be updated.
|
||||
sources need to be updated. Updating those sources inside a batch call will
|
||||
only cause the effect to run once after the batch call ends instead of after
|
||||
each time a source is updated.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local a = source(0)
|
||||
local b = source(0)
|
||||
|
||||
effect(function()
|
||||
print(a() + b())
|
||||
end)
|
||||
|
||||
-- prints "0"
|
||||
|
||||
batch(function()
|
||||
a(1) -- no print
|
||||
b(2) -- no print
|
||||
end)
|
||||
|
||||
-- prints "3"
|
||||
```
|
||||
|
||||
## context() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||
## context()
|
||||
|
||||
Creates a new context.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
function context<T>(default: T): Context<T>
|
||||
|
||||
type Context<T> =
|
||||
() -> T -- get
|
||||
& <U>(T, () -> U) -> U -- set
|
||||
& (T, () -> ()) -> () -- set
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Calling `context()` returns a new context function.
|
||||
Call this function with no arguments to get the context value.
|
||||
Call this function with a value and a function to create a new context with
|
||||
the given value.
|
||||
|
||||
The new context is run under a stable scope.
|
||||
Call this function with a value and a callback to set a new context with the
|
||||
given value.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local theme = context()
|
||||
|
||||
local function Button()
|
||||
|
|
@ -156,3 +131,4 @@ Creates a new context.
|
|||
end)
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Strict mode is library-wide and can get set by doing:
|
||||
|
||||
```luau
|
||||
```lua
|
||||
vide.strict = true
|
||||
```
|
||||
|
||||
|
|
@ -14,24 +14,25 @@ and identifying improper usage.
|
|||
|
||||
Currently, strict mode will:
|
||||
|
||||
1. Run reactive scopes twice when a source updates.
|
||||
2. Throw an error if yields occur where they are not allowed.
|
||||
3. Checks for `indexes()` and `values()` outputting primitive values.
|
||||
4. Checks for `values()` input having duplicate values.
|
||||
5. Checks for duplicate nested properties at same depth.
|
||||
6. Checks for destruction of an active scope.
|
||||
7. Better error reporting and stack traces.
|
||||
1. Run derived sources twice a source updates.
|
||||
2. Run effects twice when a source updates.
|
||||
3. Throw an error if yields occur where they are not allowed.
|
||||
4. Checks for `indexes()` and `values()` returning primitive values.
|
||||
5. Checks for `values()` input having duplicate values.
|
||||
6. Checks for duplicate nested properties at same depth.
|
||||
7. Better error reporting and stack traces + creation traces of property bindings.
|
||||
|
||||
By rerunning reactive scopes twice each time they update, it helps ensure that
|
||||
computations are pure, and that any cleanup is done correctly.
|
||||
By rerunning derived sources and effects twice each time they update, it helps
|
||||
ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
|
||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||
which strict mode will catch.
|
||||
|
||||
As well as additional safety checks, Vide will dedicate extra resources to
|
||||
recording and better emitting stack traces where errors occur, particularly
|
||||
when implicit effects are created for instance property updating.
|
||||
when binding properties to sources.
|
||||
|
||||
It is recommended to develop UI with strict mode and to disable it when pushing to
|
||||
production. In Roblox, production code compiles at O2 by default, so you do not
|
||||
production. In Roblox, production code compiles at O2 by default, so you don't
|
||||
need to worry about disabling strict mode unless you have manually enabled it.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
pageClass: home
|
||||
next:
|
||||
text: 'Introduction'
|
||||
link: '/tut/crash-course/1-introduction'
|
||||
|
||||
hero:
|
||||
name: "Vide"
|
||||
name: Vide
|
||||
text: ""
|
||||
tagline: A reactive UI library for Luau.
|
||||
image:
|
||||
src: /logo.svg
|
||||
alt: Vide
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Tutorials
|
||||
|
|
@ -19,4 +17,8 @@ hero:
|
|||
text: API Reference
|
||||
link: /api/reactivity-core
|
||||
|
||||
features:
|
||||
- title: In Development
|
||||
details: Not recommended for production use.
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.4.5",
|
||||
"vitepress-plugin-mermaid": "2.0.17"
|
||||
}
|
||||
}
|
||||
{
|
||||
"type": "module",
|
||||
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"vitepress": "1.0.0-rc.25",
|
||||
"vitepress-plugin-mermaid": "2.0.14"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="375" height="220" viewBox="15 7 115 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="150" height="88" viewBox="15 7 115 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1_41)">
|
||||
<g clip-path="url(#clip0_1_41)">
|
||||
<rect x="18" y="12" width="110" height="48" rx="8" fill="url(#paint0_radial_1_41)"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
|
@ -1,31 +1,37 @@
|
|||
<svg width="200" height="200" viewBox="0 0 152 152" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ii_282_2)">
|
||||
<path d="M63.659 130.625C69.1438 140.125 82.8561 140.125 88.341 130.625L144.659 33.0785C148.573 26.2993 143.068 17.9607 135.296 18.8969L106.185 22.4029C99.2887 23.2337 93.3954 27.7609 90.8157 34.21L76 71.25L57 23.75L16.7038 18.8969C8.93178 17.9607 3.42658 26.2993 7.34067 33.0785L63.659 130.625Z" fill="url(#paint0_radial_282_2)"/>
|
||||
<path d="M57 23.75L76 71.25L95 119.091C90.9207 126.157 80.6246 125.862 76.9557 118.575L27.4208 20.1875L57 23.75Z" fill="url(#paint1_radial_282_2)"/>
|
||||
<path d="M144.659 33.0785L88.3409 130.625C82.8561 140.125 69.1438 140.125 63.659 130.625L57 119.091L38 86.1821C45.7268 99.4265 65.3087 97.9783 71.0034 83.7416L76 71.25L95 23.75L135.296 18.8969C143.068 17.9607 148.573 26.2993 144.659 33.0785Z" fill="#4896F3"/>
|
||||
<svg width="384" height="384" viewBox="8 8 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ddd_1_134)">
|
||||
<path d="M21.4019 35.5C22.5566 37.5 25.4434 37.5 26.5981 35.5L38.4546 14.9639C39.2786 13.5367 38.1196 11.7812 36.4834 11.9783L30.3548 12.7164C28.9029 12.8913 27.6622 13.8444 27.1191 15.2021L24 23L20 13L11.5166 11.9783C9.88038 11.7812 8.72139 13.5367 9.54541 14.9639L21.4019 35.5Z" fill="url(#paint0_radial_1_134)"/>
|
||||
<path d="M20 13L24 23L28 33.0718C27.1412 34.5593 24.9736 34.4973 24.2012 32.9632L13.7728 12.25L20 13Z" fill="url(#paint1_radial_1_134)"/>
|
||||
<path d="M38.4546 14.9639L26.5981 35.5C25.4434 37.5 22.5566 37.5 21.4019 35.5L20 33.0718L16 26.1436C17.6267 28.9319 21.7492 28.627 22.9481 25.6298L24 23L28 13L36.4834 11.9783C38.1196 11.7812 39.2786 13.5367 38.4546 14.9639Z" fill="#4896F3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ii_282_2" x="6.04761" y="18.8246" width="163.905" height="166.925" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<filter id="filter0_ddd_1_134" x="-4.72681" y="-0.0369511" width="61.4536" height="57.037" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="24" dy="48"/>
|
||||
<feGaussianBlur stdDeviation="96"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feOffset dx="2" dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_282_2"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_134"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="12" dy="24"/>
|
||||
<feGaussianBlur stdDeviation="48"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feOffset dx="1" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_282_2" result="effect2_innerShadow_282_2"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1_134" result="effect2_dropShadow_1_134"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.12549 0 0 0 0 0.313726 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_1_134" result="effect3_dropShadow_1_134"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1_134" result="shape"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(76 149.625) rotate(-90) scale(133 153.575)">
|
||||
<radialGradient id="paint0_radial_1_134" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24 39.5) rotate(-90) scale(28 32.3316)">
|
||||
<stop stop-color="#24447F"/>
|
||||
<stop offset="1" stop-color="#3661A2"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_282_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(61.75 19) rotate(74.3578) scale(123.317 72.2423)">
|
||||
<radialGradient id="paint1_radial_1_134" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21 12) rotate(74.3578) scale(25.9615 15.2089)">
|
||||
<stop stop-color="#1D314F"/>
|
||||
<stop offset="1" stop-color="#0D1A2E"/>
|
||||
</radialGradient>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,148 +0,0 @@
|
|||
# Dynamic Scopes
|
||||
|
||||
Dynamic scopes are scopes that are created and destroyed in response to
|
||||
source updates. This is needed for conditionally rendering parts of your UI,
|
||||
such as opening and closing menus.
|
||||
|
||||
While Vide provides functions for common ways to do this, this section will
|
||||
show how you can implement them yourself so you are not limited by only what is
|
||||
provided.
|
||||
|
||||
## Recreating [`show()`](/api/reactivity-dynamic#show-reactive)
|
||||
|
||||
The most basic one, `show()`, can be
|
||||
implemented yourself like so:
|
||||
|
||||
```luau
|
||||
local function show(toggle: () -> unknown, component: () -> Instance)
|
||||
return derive(function()
|
||||
return if toggle() then untrack(component) else nil
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
The main thing to note here is the use of `untrack()`. This function runs its
|
||||
callback in a new stable scope. Without this, if the component were to create
|
||||
a reactive scope, an error would occur since a reactive scope cannot be created
|
||||
within a reactive scope.
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#444455",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#444455"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph derive ["derive (reactive)"]
|
||||
|
||||
subgraph untrack ["untrack (stable)"]
|
||||
subgraph effect ["effect (reactive)"]
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You can see from the above graph how the effect would not be created directly
|
||||
inside the derive, there is a stable scope between them. This requirement exists
|
||||
as a guard against unintentional rerendering of UI.
|
||||
|
||||
## Recreating [`switch()`](/api/reactivity-dynamic#switch-reactive)
|
||||
|
||||
```luau
|
||||
local function switch(key)
|
||||
return function(map)
|
||||
return derive(function()
|
||||
local component = map[key()]
|
||||
return if component then untrack(component) else nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Recreating [`indexes()`](/api/reactivity-dynamic#indexes-reactive)
|
||||
|
||||
This is a more complicated function because it manages multiple scopes at the
|
||||
same time, unlike the previous functions. Because some scopes may persist
|
||||
between reruns, we cannot use `untrack()` anymore which automatically destroys
|
||||
on rerun; we must use `root()` where the lifetime of each scope is managed
|
||||
manually and independently.
|
||||
|
||||
```luau
|
||||
local function indexes<I, VI, VO>(
|
||||
input: () -> Map<I, VI>,
|
||||
transform: (value: () -> VI, index: I) -> VO
|
||||
)
|
||||
local index_caches = {} :: Map<I, {
|
||||
previous_input: VI,
|
||||
output: VO,
|
||||
source: (VI) -> VI,
|
||||
destroy: () -> ()
|
||||
}?>
|
||||
|
||||
-- destroy all scopes if the parent scope is destroyed
|
||||
cleanup(function()
|
||||
for _, cache in index_caches do
|
||||
assert(cache).destroy()
|
||||
end
|
||||
end)
|
||||
|
||||
return derive(function()
|
||||
local new_input = input()
|
||||
|
||||
-- destroy scopes of removed indexes
|
||||
for i, cache in index_caches do
|
||||
if new_input[i] == nil then
|
||||
assert(cache).destroy()
|
||||
index_caches[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- create scopes or update sources of added or changed index values
|
||||
for i, v in new_input do
|
||||
local cache = index_caches[i]
|
||||
|
||||
if cache == nil then -- no scope created for this index, create one
|
||||
local src = source(v)
|
||||
|
||||
local destroy, result = root(function()
|
||||
return transform(src, i)
|
||||
end)
|
||||
|
||||
index_caches[i] = {
|
||||
destroy = destroy,
|
||||
source = src,
|
||||
output = result,
|
||||
previous_input = v
|
||||
}
|
||||
elseif cache.previous_input ~= v then -- scope exists, update source
|
||||
cache.previous_input = v
|
||||
cache.source(v)
|
||||
else -- scope exists and value has not changed; do nothing
|
||||
end
|
||||
end
|
||||
|
||||
-- return the cached output values as an array
|
||||
local array = table.create(#index_caches)
|
||||
|
||||
for _, cache in index_caches do
|
||||
table.insert(array, assert(cache).output)
|
||||
end
|
||||
|
||||
return array
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Though the above functions are already provided to you by Vide, this serves as
|
||||
an example for how you may create your own dynamic scope functions.
|
||||
192
docs/tut/advanced/nested-scoping.md
Normal file
192
docs/tut/advanced/nested-scoping.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Nested Scopes
|
||||
|
||||
Nesting scopes gives you finer control over the reactive graph, but needs more work to do. The built-in control flow functions try to cover the
|
||||
most common cases, but they do not cover all of them.
|
||||
|
||||
This tutorial will demonstrate how to implement a `show()` control flow function
|
||||
using just sources and effects.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local show = vide.show
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
root(function()
|
||||
local toggled = source(true)
|
||||
|
||||
show(toggled, Button)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> show
|
||||
|
||||
subgraph show[show effect]
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Above is the reactive graph for `show()`. It creates a new effect depending on
|
||||
`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
|
||||
`show` effect calls `Counter`, which creates a new reactive scope to update its
|
||||
text whenever `count` changes. As per the rules of reactive scopes, a reactive
|
||||
scope rerunning will destroy any scopes created within it. So the text
|
||||
effect's reactive scope is destroyed whenever the show effect is rerun.
|
||||
|
||||
The same can be achieved without the use of `show()`:
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
local toggled = source(true)
|
||||
|
||||
effect(function()
|
||||
if toggled() then
|
||||
local destroy = root(function()
|
||||
Counter()
|
||||
end)
|
||||
cleanup(destroy)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> effect
|
||||
|
||||
subgraph effect
|
||||
subgraph mount2[inner mount]
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is another way to achieve the same. Here we use `root()` within the effect
|
||||
to manually create and destroy a new stable scope whenever the effect reruns.
|
||||
|
||||
The reason for creating a stable scope is to prevent the effect from tracking
|
||||
any sources that may be read inside the `Counter()` call. Otherwise, the effect
|
||||
may be rerun needlessly and recreate the counter.
|
||||
|
||||
Alternatively, instead of using `root()`:
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = count,
|
||||
Activated = function() count(count() + 1) end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
local toggled = source(true)
|
||||
|
||||
effect(function()
|
||||
if toggled() then
|
||||
untrack(Button)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
toggle --> effect
|
||||
|
||||
subgraph effect
|
||||
text[Text effect]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Without the use of `untrack()`, an error would occur, since Vide does not allow
|
||||
the creation of reactive scopes inside reactive scopes. `untrack()` creates a
|
||||
stable scope inside the reactive scope, and we can create another reactive scope
|
||||
inside that stable scope. The
|
||||
reason for this, is because if the `Counter` component reads from a source
|
||||
internally, that can cause the reactive scope calling `Counter()` to track that
|
||||
source, causing unintentional reruns. As a guard against this, you are forced to
|
||||
use `untrack()` to create nested reactive scopes.
|
||||
|
||||
The final result is the same as using the `show()` component. An effect is
|
||||
created which creates the counter, which creates its own reactive scope. The
|
||||
effect rerunning causes the counter's internal reactive scope to be destroyed,
|
||||
making sure everything is cleaned up.
|
||||
0
docs/tut/control-flow/1-intro.md
Normal file
0
docs/tut/control-flow/1-intro.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
1
docs/tut/control-flow/2-show.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# show()
|
||||
1
docs/tut/control-flow/3-switch.md
Normal file
1
docs/tut/control-flow/3-switch.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# switch()
|
||||
1
docs/tut/control-flow/4-indexes.md
Normal file
1
docs/tut/control-flow/4-indexes.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# indexes()
|
||||
1
docs/tut/control-flow/5-values.md
Normal file
1
docs/tut/control-flow/5-values.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# values()
|
||||
|
|
@ -6,16 +6,10 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
|||
|
||||
## Why Vide?
|
||||
|
||||
Vide's reactive and declarative API aims to let you program UI as simply as
|
||||
possible, with a strong focus on how data flows through your application.
|
||||
Vide provides a reactive and declarative API to simplify managing UI.
|
||||
|
||||
Some of Vide's main design choices:
|
||||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
- Syntax minimal.
|
||||
- Data oriented.
|
||||
- Typechecking compatible.
|
||||
- Instance independent.
|
||||
|
||||
Vide's reactivity operates with the concept
|
||||
of scopes which carries a learning curve, though is what makes Vide's minimal
|
||||
syntax possible. The crash course will introduce these concepts gradually.
|
||||
- Minimal syntax
|
||||
- Complete typechecking
|
||||
- Independence from instances
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ a side-effect from a source update. Vide provides a function `cleanup()` which
|
|||
is used to queue a callback for the next time a reactive scope is rerun or
|
||||
destroyed, or when a stable scope is destroyed.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local count = source(0)
|
||||
|
||||
|
||||
local destroy = root(function()
|
||||
effect(function()
|
||||
local x = count()
|
||||
|
|
|
|||
98
docs/tut/crash-course/11-control-flow.md
Normal file
98
docs/tut/crash-course/11-control-flow.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Control Flow
|
||||
|
||||
Eventually you may need a way to dynamically create and destroy UI elements
|
||||
resulting from source updates. Vide provides functions to help you do this,
|
||||
known as *control flow* functions.
|
||||
|
||||
These functions return new sources, which hold the instances to be displayed.
|
||||
The new sources can be used in `create()` to update the children of a container
|
||||
instance.
|
||||
|
||||
## indexes()
|
||||
|
||||
`indexes()` *maps* each table index to a new UI element that can
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
```lua
|
||||
local list = source {
|
||||
"finish the crash course",
|
||||
"star Vide's GitHub"
|
||||
}
|
||||
|
||||
local function TodoList(props: { list: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(list, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
TodoList { list = list }
|
||||
```
|
||||
|
||||
For each index in the given source table, the given function to `indexes()` will
|
||||
be run in a new stable scope with:
|
||||
|
||||
1. a source containing the value at the index
|
||||
2. the index itself
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
given source for that index is updated.
|
||||
|
||||
Any time the input source table is updated, the given function will be ran for
|
||||
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||
value), will have its corresponding stable scope destroyed.
|
||||
|
||||
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root ["root scope"]
|
||||
direction LR
|
||||
todoList --> indexes -.- subroot1 & subroot2
|
||||
|
||||
subgraph subroot1 ["indexes scope 1"]
|
||||
direction LR
|
||||
value1[todo] --> prop1["prop binding"]
|
||||
end
|
||||
|
||||
subgraph subroot2 ["indexes scope 2"]
|
||||
direction LR
|
||||
value2[todo] --> prop2[prop binding]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When you edit a table in a source, you must set that table again to actually
|
||||
update the source.
|
||||
|
||||
```lua
|
||||
local src = source { 1, 2 }
|
||||
local data = src()
|
||||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
# Dynamic Scopes
|
||||
|
||||
Eventually you may need a way to dynamically create and destroy UI elements
|
||||
resulting from source updates. Vide provides functions to help you do this,
|
||||
known as *dynamic scope* functions.
|
||||
|
||||
These functions create and destroy scopes for you in response to source
|
||||
updates. They return a source containing the created component. This source can
|
||||
be parented as a child which will update the shown children whenever the source
|
||||
updates.
|
||||
|
||||
The simplest example is using `show()`.
|
||||
|
||||
```luau
|
||||
local source = vide.source
|
||||
local create = vide.create
|
||||
local show = vide.show
|
||||
local root = vide.root
|
||||
|
||||
function Button(props: { Text: string, Activated: () -> () })
|
||||
return create "TextButton" {
|
||||
Text = props.Text,
|
||||
Activated = props.Activated
|
||||
}
|
||||
end
|
||||
|
||||
function Menu()
|
||||
return create "TextLabel" {
|
||||
Text = "This is a menu"
|
||||
}
|
||||
end
|
||||
|
||||
function App()
|
||||
local toggled = source(false)
|
||||
|
||||
return create "ScreenGui" {
|
||||
Button {
|
||||
Text = "Toggle Menu",
|
||||
Activated = function()
|
||||
toggled(not toggled())
|
||||
end
|
||||
},
|
||||
|
||||
show(toggled, Menu) -- [!code highlight]
|
||||
}
|
||||
end
|
||||
|
||||
root(function()
|
||||
App().Parent = game.StarterGui
|
||||
end)
|
||||
```
|
||||
|
||||
This is a complete example of rendering UI which has a single button that
|
||||
toggles the opening of a menu.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Another common function is `indexes()`. This function creates a component for
|
||||
each index in a table.
|
||||
|
||||
Each component created is done so in a new and independent stable scope. The
|
||||
indexes of the table are checked each source update to prevent redunant
|
||||
destruction and recreation of UI elements.
|
||||
|
||||
```luau
|
||||
local source = vide.source
|
||||
local create = vide.create
|
||||
local indexes = vide.indexes
|
||||
local root = vide.root
|
||||
|
||||
local function Todo(props: {
|
||||
Text: () -> string,
|
||||
Position: number,
|
||||
Activated: () -> ()
|
||||
})
|
||||
return create "TextButton" {
|
||||
Text = function() return props.Position .. ": " .. props.Text() end,
|
||||
LayoutOrder = props.Position,
|
||||
Activated = Activated
|
||||
}
|
||||
end
|
||||
|
||||
local function TodoList(props: { List: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(props.List, function(text, i) -- [!code highlight]
|
||||
return Todo {
|
||||
Text = text,
|
||||
Position = i,
|
||||
Activated = function() -- remove the todo when clicked
|
||||
local list = props.List()
|
||||
table.remove(list, i)
|
||||
props.List(list)
|
||||
end
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
function App()
|
||||
local list = source {
|
||||
"finish the crash course",
|
||||
"star Vide's GitHub"
|
||||
}
|
||||
|
||||
return create "ScreenGui" {
|
||||
TodoList { List = list },
|
||||
}
|
||||
end
|
||||
|
||||
root(function()
|
||||
App().Parent = game.StarterGui
|
||||
end)
|
||||
```
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root ["root"]
|
||||
direction LR
|
||||
todoList --> indexes -.- subroot1 & subroot2
|
||||
|
||||
subgraph subroot1 ["indexes scope 1"]
|
||||
direction LR
|
||||
value1[todo] --> prop1["prop binding"]
|
||||
end
|
||||
|
||||
subgraph subroot2 ["indexes scope 2"]
|
||||
direction LR
|
||||
value2[todo] --> prop2[prop binding]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When you edit a table in a source, you must set that table again to actually
|
||||
update the source.
|
||||
|
||||
```luau
|
||||
local src = source { 1, 2 }
|
||||
local data = src()
|
||||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
All dynamic scope functions also support delaying the destruction of the scope.
|
||||
This is useful for playing any sort of animation or effect before the UI
|
||||
instance is removed.
|
||||
|
||||
If you have the following code, for example:
|
||||
|
||||
```lua
|
||||
local function Menu()
|
||||
return create "Frame" {}
|
||||
end
|
||||
|
||||
local toggled = source(true)
|
||||
|
||||
create "ScreenGui" {
|
||||
show(toggled, function()
|
||||
return Menu {}
|
||||
end)
|
||||
}
|
||||
|
||||
toggled(false) -- menu will disappear immediately
|
||||
```
|
||||
|
||||
```lua
|
||||
local function Menu(props: { Visible: () -> boolean })
|
||||
local transparency = spring(function()
|
||||
return if p.Visible then 0 else 1
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
BackgroundTransparency = transparency
|
||||
}
|
||||
end
|
||||
|
||||
local toggled = source(true)
|
||||
|
||||
create "ScreenGui" {
|
||||
show(toggled, function(_, present)
|
||||
return Menu { p.Visible = present }, 3 -- give a generous 3 seconds for the spring to complete before destroying
|
||||
end)
|
||||
}
|
||||
|
||||
toggled(false)
|
||||
-- `present` will go `false` immediately
|
||||
-- transparency will begin being sprung
|
||||
-- after 3 seconds the scope is destroyed, giving the spring enough time to complete
|
||||
```
|
||||
|
||||
If `toggled` goes from truthy to falsey, beginning the timer, but then back
|
||||
to truthy before the timer finishes, the timer is cancelled and the scope is
|
||||
not destroyed.
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
# Actions
|
||||
|
||||
Actions are special callbacks that you can pass along with properties,
|
||||
Actions in Vide are special callbacks that you can pass along with properties,
|
||||
to run some code on an instance receiving them.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local action = vide.action
|
||||
```
|
||||
|
||||
```lua
|
||||
create "TextLabel" {
|
||||
Text = "test",
|
||||
|
||||
|
|
@ -20,15 +22,14 @@ create "TextLabel" {
|
|||
Actions can be wrapped with functions for reuse. Below is an example of an
|
||||
action used to listen for property changes:
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local action = vide.action
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
local function changed(prop: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ effect(function()
|
|||
print(output())
|
||||
end)
|
||||
|
||||
instance.Text = "foo" -- "foo" will be printed by the effect
|
||||
instance.Text = "foo" -- "foo" will be printed from the effect
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Strict mode will run derived sources and effects twice each time they update.
|
|||
This is to help ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done properly.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -23,7 +23,6 @@ local count = source(0)
|
|||
|
||||
local ran = 0
|
||||
effect(function()
|
||||
count()
|
||||
ran += 1
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Created by:
|
|||
|
||||
- `root()`
|
||||
- `untrack()`
|
||||
- `show()`
|
||||
- `switch()`
|
||||
- `indexes()`
|
||||
|
||||
Stable scopes do not track sources and never rerun.
|
||||
|
|
@ -47,14 +47,23 @@ Created by:
|
|||
Reactive scopes do track sources and will rerun when those sources update.
|
||||
|
||||
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
||||
can be created within a reactive scope.
|
||||
can.
|
||||
|
||||
## Scope Cleanup
|
||||
## Scope Owners
|
||||
|
||||
When a scope is rerun or destroyed, all scopes created within it are
|
||||
automatically destroyed.
|
||||
A scope created within another scope is *owned* by the other scope, with the
|
||||
exception of the scope created by `root()`.
|
||||
|
||||
Any functions queued by `cleanup()` are also ran.
|
||||
When a scope is rerun or destroyed, all scopes owned by it are automatically
|
||||
destroyed.
|
||||
|
||||
`root()` creates a stable scope with no owner, instead it is destroyed manually.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
|
||||
|
||||
Queue a function to run using `cleanup()`.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
|
|
@ -64,12 +73,12 @@ relationships between effects and the sources they depend on.
|
|||
|
||||
### Code
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local text = derive(function()
|
||||
return "count: " .. count()
|
||||
return "count: " .. text()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
|
|
@ -84,12 +93,12 @@ end)
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#202530"
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -109,5 +118,6 @@ Notes:
|
|||
- An update to `count` will cause `text` to rerun, which
|
||||
then causes `effect` to rerun.
|
||||
- When the root scope is destroyed, `text` and
|
||||
`effect` will be destroyed alongside it, since they were created within it.
|
||||
`count` will be untouched and future updates to `count` will have no effect.
|
||||
`effect` will be destroyed alongside it, since they are
|
||||
owned by it. `count` will be untouched and future updates
|
||||
to `count` will have no effect.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Instances are created using `create()`.
|
|||
Parentheses `()` can be omitted when calling functions with string or
|
||||
table literals for brevity.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
||||
return create "ScreenGui" {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ together.
|
|||
|
||||
::: code-group
|
||||
|
||||
```luau [Button.luau]
|
||||
```lua [Button.luau]
|
||||
local create = vide.create
|
||||
|
||||
local function Button(props: {
|
||||
|
|
@ -34,7 +34,7 @@ end
|
|||
return Button
|
||||
```
|
||||
|
||||
```luau [Menu.luau]
|
||||
```lua [Menu.luau]
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Vide's reactivity.
|
|||
|
||||
A source can be created using `source()`.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
|
||||
local count = source(0)
|
||||
|
|
@ -16,13 +16,13 @@ The value passed to `source()` is the initial value of the source.
|
|||
The value of a source can be set by calling it with an argument, and can be read
|
||||
by calling it with no arguments.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Sources can be *derived* by wrapping them in functions.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Effects
|
||||
|
||||
Effects are functions that are ran in response to source updates.
|
||||
Effects are functions that are ran in response to source updates. They are
|
||||
A source and effect is analogous to a signal and connection.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -23,13 +23,10 @@ count(1)
|
|||
Any source read inside an effect is tracked and will rerun the effect when
|
||||
that source is updated.
|
||||
|
||||
The effect runs its callback once immediately to initially figure out what
|
||||
sources are being read.
|
||||
|
||||
Derived sources are also tracked, it does not matter how deeply nested
|
||||
Derived sources are also tracked, it doesn't matter how deeply nested
|
||||
inside a function a source is.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -50,22 +47,3 @@ count(2)
|
|||
|
||||
If a source is updated with the same value it already had, it will not rerun
|
||||
effects depending on it.
|
||||
|
||||
You can also read from a source within an effect without the effect tracking it.
|
||||
|
||||
```luau
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
||||
local a = source(0)
|
||||
local b = source(0)
|
||||
|
||||
effect(function()
|
||||
print(`a: {a()} b: {untrack(b)}`)
|
||||
end)
|
||||
|
||||
a(1) -- prints "a: 1 b: 0"
|
||||
b(1) -- prints nothing
|
||||
a(2) -- prints "a: 2 b: 1"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,46 +7,62 @@ But the disconnecting of many signals and connections is tedious and verbose.
|
|||
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||
API, given that you follow a few rules.
|
||||
|
||||
There are two types of scopes: stable and reactive.
|
||||
Scopes come in two flavors; stable and reactive.
|
||||
|
||||
- A scope must be created within another scope.
|
||||
- Stable scopes never rerun.
|
||||
- Reactive scopes can rerun.
|
||||
- A reactive scope cannot be created within another reactive scope, only within
|
||||
a stable scope.
|
||||
- All scopes must be created within another scope with the exception of `root()`
|
||||
- Stable scopes never rerun
|
||||
- Reactive scopes can rerun
|
||||
- A reactive scope cannot be created within another reactive scope
|
||||
|
||||
An exception to the first rule is `root()`, which creates the initial scope that
|
||||
you destroy manually with a destructor function it returns.
|
||||
|
||||
`root()` creates a stable scope.
|
||||
`effect()` creates a reactive scope.
|
||||
`root()` creates a stable scope.
|
||||
|
||||
Whenever a scope is destroyed, any scope created within that scope is also
|
||||
destroyed, and so on.
|
||||
destroyed, and so on. This is why all scopes must be created within another
|
||||
scope, except `root()` which is used to create the initial scope that you can
|
||||
manually destroy.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(0)
|
||||
|
||||
local function setup()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
setup() -- error, effect() tried to create a reactive scope with no stable scope
|
||||
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
|
||||
|
||||
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
||||
local count = root(setup) -- ok since effect() was called within a stable scope
|
||||
count(1) -- prints "1"
|
||||
```
|
||||
|
||||
The scope created by `root()` can be destroyed.
|
||||
|
||||
```lua
|
||||
local function setup()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
local destroy, count = root(setup)
|
||||
|
||||
count(1) -- prints "1"
|
||||
count(2) -- prints "2"
|
||||
|
||||
destroy()
|
||||
|
||||
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
|
||||
count(2) -- effect is destroyed; no longer prints
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
|
@ -57,12 +73,12 @@ The reactive graph for the above example looks like so:
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -74,7 +90,7 @@ subgraph root
|
|||
end
|
||||
```
|
||||
|
||||
When the stable `root()` scope is destroyed, the reactive `effect()`
|
||||
When the stable `root()` is destroyed, the reactive `effect()`
|
||||
scope will also be destroyed since it was created within it.
|
||||
|
||||
This is important because you may have an effect that updates the property of a
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# Reactive Components
|
||||
# Stateful Components
|
||||
|
||||
Reactive components in Vide are created using sources and effects - sources to
|
||||
Stateful components in Vide are created using sources and effects - sources to
|
||||
store the data, and effects to display the data.
|
||||
|
||||
```luau
|
||||
## Internal State
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
|
@ -31,13 +33,19 @@ its internal count, and automatically update its text to reflect that count.
|
|||
Each instance of `Counter()` will maintain its own independent count, since the
|
||||
count source is created inside the component.
|
||||
|
||||
## External State
|
||||
|
||||
External sources can also be passed into components for them to use.
|
||||
|
||||
```luau
|
||||
local function CountDisplay(props: { count: () -> number })
|
||||
```lua
|
||||
local function Counter(props: { count: () -> number })
|
||||
local count = props.count
|
||||
|
||||
local instance = create "TextLabel" {}
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
|
|
@ -48,11 +56,11 @@ end
|
|||
|
||||
local count = source(0)
|
||||
|
||||
CountDisplay {
|
||||
Counter {
|
||||
count = count
|
||||
}
|
||||
|
||||
count(1) -- the CountDisplay component will update to display this count
|
||||
count(1) -- the Counter component will update to display this count
|
||||
```
|
||||
|
||||
Sources can be created internally or passed in from externally, there are no
|
||||
|
|
@ -3,9 +3,7 @@
|
|||
Explicitly creating effects to update properties is tedious. You can
|
||||
*implicitly* create an effect to update properties instead.
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [Implicit Effect]
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
@ -24,30 +22,6 @@ local function Counter()
|
|||
end
|
||||
```
|
||||
|
||||
```luau [Explicit Effect]
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
end)
|
||||
|
||||
return instance
|
||||
end
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This example is equivalent to the example seen on the previous page.
|
||||
|
||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||
|
|
@ -60,7 +34,7 @@ with a number key instead of string key) can return an instance or an array of
|
|||
instances. An effect is automatically created to unparent removed instances and
|
||||
parent new instances on source update.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local items = source {
|
||||
create "TextLabel" { Text = "A" }
|
||||
}
|
||||
|
|
@ -72,12 +46,12 @@ local function List(props: { children: () -> { Instance } })
|
|||
}
|
||||
end
|
||||
|
||||
local list = List { children = items } -- creates a list with text label "A"
|
||||
local list = List { children = items } -- creates a list with a single text label "A"
|
||||
|
||||
items {
|
||||
create "TextLabel" { Text = "B" },
|
||||
create "TextLabel" { Text = "C" }
|
||||
}
|
||||
|
||||
-- this will automatically unparent text label "A", and parent labels "B" and "C"
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
We have seen the basic way to derive a source:
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local text = function()
|
||||
|
|
@ -18,7 +18,7 @@ However, in some cases where this source could be used by multiple effects at
|
|||
the same time, the function wrapping the source will needlessly rerun to convert
|
||||
the count into a string for each effect using it.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ To avoid this, you can use `derive()` to derive a new source instead. This will
|
|||
run a function in a reactive scope only when a source used inside updated.
|
||||
Reading this derived source multiple times will just return a cached result.
|
||||
|
||||
```luau
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local derive = vide.derive
|
||||
|
|
@ -57,8 +57,7 @@ effect(function() text() end)
|
|||
count(1) -- prints "ran" x1
|
||||
```
|
||||
|
||||
Because `derive()` creates a reactive scope, it must be called within a stable
|
||||
scope, just like `effect()`.
|
||||
`derive()` must also be called within a stable scope, just like `effect()`.
|
||||
|
||||
If the recalculated value is the same as the old value, the derived source will
|
||||
not rerun the effects using it.
|
||||
|
|
@ -69,12 +68,12 @@ The reactive graph for the above example:
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#111720",
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#111720",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -87,7 +86,7 @@ end
|
|||
```
|
||||
|
||||
Deriving a source in this manner is similar to creating an effect to update
|
||||
another source. You should avoid doing this using an effect however.
|
||||
another source. You should never manually do this using an effect however.
|
||||
Improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another
|
||||
source.
|
||||
|
|
|
|||
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"
|
||||
175
src/apply.luau
175
src/apply.luau
|
|
@ -1,20 +1,24 @@
|
|||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||
local Vector2 = game and Vector2 or require "test/mock".Vector2 :: never
|
||||
local UDim2 = game and UDim2 or require "test/mock".UDim2 :: never
|
||||
|
||||
local flags = require "./flags"
|
||||
local implicit_effect = require "./implicit_effect"
|
||||
local _, is_action = require "./action"()
|
||||
local graph = require "./graph"
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local _, is_action = require(script.Parent.action)()
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
type Array<V> = { V }
|
||||
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
type Cache = {
|
||||
local free_caches: {
|
||||
-- event listeners to connect after properties are set
|
||||
events: Array<
|
||||
| string -- 1. event name
|
||||
| () -> () -- 2. listener
|
||||
events: Map<
|
||||
string, -- event name
|
||||
() -> () -- listener
|
||||
>,
|
||||
|
||||
-- actions to run after events are connected
|
||||
|
|
@ -23,34 +27,30 @@ type Cache = {
|
|||
Array<(Instance) -> ()> -- action callbacks
|
||||
>,
|
||||
|
||||
-- what to parent the instance to after running actions
|
||||
parent: unknown,
|
||||
|
||||
-- cache to detect duplicate property setting at same nesting depth
|
||||
nested_debug: Map<
|
||||
number, -- depth
|
||||
Map<string, true> -- set of property names
|
||||
>,
|
||||
|
||||
-- use stack instead of recursive function to process nesting layers one at time
|
||||
-- deeper-nested properties take precedence over shallower-nested ones
|
||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||
nested_stack: { {} | number }
|
||||
}
|
||||
}?
|
||||
|
||||
local free_cache: Cache?
|
||||
|
||||
local function borrow_cache(): Cache
|
||||
if free_cache then
|
||||
local cache = free_cache
|
||||
free_cache = nil
|
||||
return cache
|
||||
local function borrow_caches(): typeof(assert(free_caches))
|
||||
if free_caches then
|
||||
local caches = free_caches :: typeof(assert(free_caches))
|
||||
free_caches = nil
|
||||
return caches
|
||||
else
|
||||
return {
|
||||
events = {},
|
||||
actions = setmetatable({} :: any, { -- lazy init
|
||||
__index = function(self, i) self[i] = {}; return self[i] end
|
||||
}),
|
||||
parent = nil,
|
||||
nested_debug = setmetatable({} :: any, {
|
||||
__index = function(self, i: number) self[i] = {}; return self[i] end
|
||||
}),
|
||||
|
|
@ -59,61 +59,36 @@ local function borrow_cache(): Cache
|
|||
end
|
||||
end
|
||||
|
||||
local function return_cache(cache: Cache )
|
||||
free_cache = cache
|
||||
local function return_caches(caches: typeof(free_caches) )
|
||||
free_caches = caches
|
||||
end
|
||||
|
||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
||||
for property, value in properties do
|
||||
if type(property) == "string" then
|
||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
||||
if cache.nested_debug[depth][property] then
|
||||
error(`duplicate property {property} at depth {depth}`, 0)
|
||||
end
|
||||
cache.nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if property == "Parent" then
|
||||
cache.parent = value
|
||||
continue
|
||||
end
|
||||
|
||||
if type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
table.insert(cache.events, property) -- add event name to buffer
|
||||
table.insert(cache.events, value :: () -> ()) -- add event listener to buffer
|
||||
else
|
||||
implicit_effect.property(instance, property, value :: () -> ()) -- create implicit effect for property
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
implicit_effect.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(cache.actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
elseif flags.defer_nested_properties then
|
||||
table.insert(cache.nested_stack, value :: {})
|
||||
table.insert(cache.nested_stack, depth + 1) -- push table to stack for later processing
|
||||
else
|
||||
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
|
||||
end
|
||||
elseif type(value) == "userdata" then
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
-- map of datatype names to class default constructor for aggregate init
|
||||
local aggregates = {}
|
||||
for name, class in {
|
||||
CFrame = CFrame,
|
||||
Color3 = Color3,
|
||||
UDim = UDim,
|
||||
UDim2 = UDim2,
|
||||
Vector2 = Vector2,
|
||||
Vector3 = Vector3,
|
||||
Rect = Rect
|
||||
} :: Map<string, { [string]: any }> do
|
||||
aggregates[name] = class.new
|
||||
end
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
if not properties then
|
||||
error "attempt to call a constructor returned by create() with no properties"
|
||||
throw("attempt to call a constructor returned by create() with no properties")
|
||||
end
|
||||
|
||||
local caches = borrow_cache()
|
||||
local strict = flags.strict
|
||||
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
|
||||
local caches = borrow_caches()
|
||||
local events = caches.events
|
||||
local actions = caches.actions
|
||||
local nested_debug = caches.nested_debug
|
||||
|
|
@ -122,39 +97,79 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
-- process all properties
|
||||
local depth = 1
|
||||
repeat
|
||||
process_properties(properties, instance, caches, depth)
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if strict then -- check for duplicate prop assignment at nesting depth
|
||||
if nested_debug[depth][property] then
|
||||
throw(`duplicate property {property} at depth {depth}`)
|
||||
end
|
||||
nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "table" then -- attempt aggregate init
|
||||
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||
if ctor == nil then
|
||||
throw(`cannot aggregate type {typeof(value)} for property {property}`)
|
||||
end
|
||||
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||
elseif type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
events[property] = value :: () -> () -- add event to buffer
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
else
|
||||
table.insert(nested_stack, value :: {})
|
||||
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||
end
|
||||
else
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
depth = table.remove(nested_stack) :: number
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
|
||||
until not properties
|
||||
|
||||
for i = 1, #events, 2 do
|
||||
local event_name = events[i]
|
||||
local event_listener = events[i + 1]
|
||||
;(instance :: any)[event_name]:Connect(event_listener)
|
||||
for event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
end
|
||||
|
||||
for _, queued in actions do
|
||||
for _, callback in queued do
|
||||
for _, queued in next, actions do
|
||||
for _, callback in next, queued do
|
||||
callback(instance)
|
||||
end
|
||||
end
|
||||
|
||||
local parent = caches.parent
|
||||
-- finally set parent if any
|
||||
if parent then
|
||||
if type(parent) == "function" then
|
||||
implicit_effect.parent(instance, parent :: () -> Instance)
|
||||
bind.parent(instance, parent :: () -> Instance)
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
-- clear caches
|
||||
table.clear(events)
|
||||
for _, queued in actions do table.clear(queued) end
|
||||
caches.parent = nil
|
||||
if flags.strict then table.clear(nested_debug) end
|
||||
for _, queued in next, actions do table.clear(queued) end
|
||||
if strict then table.clear(nested_debug) end
|
||||
table.clear(nested_stack)
|
||||
|
||||
return_cache(caches)
|
||||
return_caches(caches)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
local flags = require "./flags"
|
||||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
local function batch(setter: () -> ())
|
||||
local already_batching = flags.batch
|
||||
|
|
@ -10,14 +13,14 @@ local function batch(setter: () -> ())
|
|||
from = graph.get_update_queue_length()
|
||||
end
|
||||
|
||||
local ok, err: string? = xpcall(setter, debug.traceback)
|
||||
local ok, err: string? = pcall(setter)
|
||||
|
||||
if not already_batching then
|
||||
flags.batch = false
|
||||
graph.flush_update_queue(from)
|
||||
end
|
||||
|
||||
if not ok then error(`error occured while batching updates: {err}`, 0) end
|
||||
if not ok then throw(`error occured while batching updates: {err}`) end
|
||||
end
|
||||
|
||||
return batch
|
||||
|
|
|
|||
105
src/bind.luau
Normal file
105
src/bind.luau
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
function create_implicit_effect<T>(updater: (T) -> T, binding: T)
|
||||
evaluate_node(create_node(assert_stable_scope(), updater, binding))
|
||||
end
|
||||
|
||||
type PropertyBinding = {
|
||||
instance: Instance,
|
||||
property: string,
|
||||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function update_property_effect(p: PropertyBinding)
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
return p
|
||||
end
|
||||
|
||||
type ParentBinding = {
|
||||
instance: Instance,
|
||||
parent: () -> Instance
|
||||
}
|
||||
|
||||
local function update_parent_effect(p: ParentBinding)
|
||||
p.instance.Parent = p.parent()
|
||||
return p
|
||||
end
|
||||
|
||||
type ChildrenBinding = {
|
||||
instance: Instance,
|
||||
cur_children_set: { [Instance]: true },
|
||||
new_children_set: { [Instance]: true },
|
||||
children: () -> Instance | { Instance }
|
||||
}
|
||||
|
||||
type ArrayOrV<V> = V | { V }
|
||||
local function update_children_effect(p: ChildrenBinding)
|
||||
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||
|
||||
local new_children = p.children() -- all (and only) children that should be parented after this update
|
||||
|
||||
if type(new_children) ~= "table" then
|
||||
new_children = { new_children }
|
||||
end
|
||||
|
||||
local function process_child(child: ArrayOrV<Instance>)
|
||||
if type(child) == "table" then
|
||||
for _, child in next, child do
|
||||
process_child(child)
|
||||
end
|
||||
else
|
||||
if new_child_set[child] then return end -- stops redundant reparenting
|
||||
|
||||
new_child_set[child] = true -- record child set from this update
|
||||
if not cur_children_set[child] then
|
||||
child.Parent = p.instance -- if child wasn't already parented then parent it
|
||||
else
|
||||
cur_children_set[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_child(new_children)
|
||||
|
||||
for child in next, cur_children_set do
|
||||
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||
end
|
||||
|
||||
table.clear(cur_children_set) -- clear cache, preserve capacity
|
||||
p.cur_children_set, p.new_children_set = new_child_set, cur_children_set
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
return {
|
||||
property = function(instance, property, source)
|
||||
return create_implicit_effect(update_property_effect, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
})
|
||||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
return create_implicit_effect(update_parent_effect, {
|
||||
instance = instance,
|
||||
parent = parent
|
||||
})
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
return create_implicit_effect(update_children_effect, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
children = children
|
||||
})
|
||||
end
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
local destroy = graph.destroy
|
||||
local get_scope = graph.get_scope
|
||||
|
||||
local function branch<T>(fn: () -> T): (() -> (), T)
|
||||
local current = get_scope()
|
||||
if not current then
|
||||
error(`cannot use branch() outside a stable or reactive scope`, 0)
|
||||
end
|
||||
|
||||
local parent = current.owner
|
||||
if not parent then
|
||||
error(`current scope is not owned by a scope`, 0)
|
||||
end
|
||||
|
||||
local node = create_node(parent, false, false)
|
||||
|
||||
local destroy = function()
|
||||
destroy(node)
|
||||
end
|
||||
|
||||
push_scope(node)
|
||||
|
||||
local ok, result = xpcall(fn, debug.traceback)
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
destroy()
|
||||
error(`error while running branch():\n\n{result}`, 0)
|
||||
end
|
||||
|
||||
return destroy, result
|
||||
end
|
||||
|
||||
return branch
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
local action = require "./action"()
|
||||
local cleanup = require "./cleanup"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local action = require(script.Parent.action)()
|
||||
local cleanup = require(script.Parent.cleanup)
|
||||
|
||||
local function changed<T>(property: string, callback: (T) -> ())
|
||||
return action(function(instance)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
||||
|
||||
local graph = require "./graph"
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local get_scope = graph.get_scope
|
||||
local push_cleanup = graph.push_cleanup
|
||||
|
||||
local function helper(obj: any)
|
||||
return
|
||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
||||
elseif type(obj) == "thread" then function() task.cancel(obj) end
|
||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||
elseif obj.destroy then function() obj:destroy() end
|
||||
elseif obj.disconnect then function() obj:disconnect() end
|
||||
elseif obj.Destroy then function() obj:Destroy() end
|
||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||
else error "cannot cleanup given object"
|
||||
else throw("cannot cleanup given object")
|
||||
end
|
||||
|
||||
local function cleanup(value: unknown)
|
||||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
error "cannot cleanup outside a stable or reactive scope"
|
||||
throw "cannot cleanup outside a stable or reactive scope"
|
||||
end; assert(scope)
|
||||
|
||||
if type(value) == "function" then
|
||||
|
|
@ -35,7 +36,6 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
|||
|
||||
return cleanup ::
|
||||
( (callback: () -> ()) -> () ) &
|
||||
( (thread: thread) -> () ) &
|
||||
( (instance: Destroyable) -> () ) &
|
||||
( (connection: Disconnectable) -> () ) &
|
||||
( (instance: Instance) -> () ) &
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_scope = graph.get_scope
|
||||
|
|
@ -43,10 +46,10 @@ local function context<T>(...: T): Context<T>
|
|||
if has_default ~= nil then
|
||||
return default_value
|
||||
else
|
||||
error("attempt to get context when no context is set and no default context is set", 0)
|
||||
throw("attempt to get context when no context is set and no default context is set")
|
||||
end
|
||||
else -- set
|
||||
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
||||
if not scope then return throw("attempt to set context outside of a vide scope") end
|
||||
|
||||
local value, component = ...
|
||||
|
||||
|
|
@ -61,7 +64,7 @@ local function context<T>(...: T): Context<T>
|
|||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
error(`error while running context:\n\n{result}`, 0)
|
||||
throw(`error while running context:\n\n{result}`)
|
||||
end
|
||||
|
||||
return result
|
||||
|
|
|
|||
169
src/create.luau
169
src/create.luau
|
|
@ -1,119 +1,84 @@
|
|||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
local Instance = game and Instance or require "../test/mock".Instance :: never
|
||||
if not game then script = require "test/relative-string" end
|
||||
local typeof = game and typeof or require "test/mock".typeof:: never
|
||||
local Instance = game and Instance or require "test/mock".Instance :: never
|
||||
|
||||
local defaults = require "./defaults"
|
||||
local apply = require "./apply"
|
||||
local flags = require "./flags"
|
||||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local apply = require(script.Parent.apply)
|
||||
|
||||
local function create_constructor_for_class(class: string): ({ [unknown]: unknown }) -> Instance
|
||||
local function constructor(properties: { [unknown]: unknown }): Instance
|
||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||
|
||||
setmetatable(ctor_cache :: any, {
|
||||
__index = function(self, class)
|
||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||
if not ok then error(`invalid class name {class}`, 0) end
|
||||
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
||||
|
||||
if flags.defaults then
|
||||
local default: { [string]: unknown }? = defaults[class]
|
||||
if default then
|
||||
for i, v in default do
|
||||
(instance :: any)[i] = v
|
||||
end
|
||||
local default: { [string]: unknown }? = defaults[class]
|
||||
if default then
|
||||
for i, v in next, default do
|
||||
(instance :: any)[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
return apply(instance, properties)
|
||||
end
|
||||
local function ctor(properties: Props): Instance
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
|
||||
return constructor
|
||||
self[class] = ctor
|
||||
return ctor
|
||||
end
|
||||
})
|
||||
|
||||
local function create_instance(class: string)
|
||||
return ctor_cache[class]
|
||||
end
|
||||
|
||||
local constructor_cache = {} :: { [string]: ({ [unknown]: unknown }) -> Instance }
|
||||
|
||||
-- todo: remove support for different overloads
|
||||
local function create(class_or_instance: string|Instance, properties: { [unknown]: unknown }?): unknown
|
||||
if type(class_or_instance) ~= "string" and typeof(class_or_instance) ~= "Instance" then
|
||||
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
||||
local function clone_instance(instance: Instance)
|
||||
return function(properties: Props): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
local constructor: ({ [unknown]: unknown }) -> Instance
|
||||
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
constructor = constructor_cache[class_or_instance]
|
||||
if not constructor then
|
||||
constructor = create_constructor_for_class(class_or_instance)
|
||||
constructor_cache[class_or_instance] = constructor
|
||||
end
|
||||
return create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
return clone_instance(class_or_instance)
|
||||
else
|
||||
constructor = function(props)
|
||||
local clone = assert(class_or_instance:Clone(), "attempt to clone a non-archivable instance")
|
||||
return apply(clone, props)
|
||||
end
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
return nil :: never
|
||||
end
|
||||
|
||||
return if properties
|
||||
then constructor(properties)
|
||||
else constructor
|
||||
end
|
||||
|
||||
export type Instances = {
|
||||
Folder: Folder,
|
||||
BillboardGui: BillboardGui,
|
||||
CanvasGroup: CanvasGroup,
|
||||
Frame: Frame,
|
||||
ImageButton: ImageButton,
|
||||
ImageLabel: ImageLabel,
|
||||
ScreenGui: ScreenGui,
|
||||
ScrollingFrame: ScrollingFrame,
|
||||
SurfaceGui: SurfaceGui,
|
||||
TextBox: TextBox,
|
||||
TextButton: TextButton,
|
||||
TextLabel: TextLabel,
|
||||
UIAspectRatioConstraint: UIAspectRatioConstraint,
|
||||
UICorner: UICorner,
|
||||
UIGradient: UIGradient,
|
||||
UIGridLayout: UIGridLayout,
|
||||
UIListLayout: UIListLayout,
|
||||
UISizeConstraint: UISizeConstraint,
|
||||
UITextSizeConstraint: UITextSizeConstraint,
|
||||
UIScale: UIScale,
|
||||
UIPadding: UIPadding,
|
||||
UIStroke: UIStroke,
|
||||
UIFlexItem: UIFlexItem,
|
||||
UIPageLayout: UIPageLayout,
|
||||
UITableLayout: UITableLayout,
|
||||
VideoFrame: VideoFrame,
|
||||
ViewportFrame: ViewportFrame,
|
||||
ProximityPrompt: ProximityPrompt,
|
||||
UIDragDetector: UIDragDetector,
|
||||
Camera: Camera,
|
||||
WorldModel: WorldModel,
|
||||
}
|
||||
|
||||
export type function Properties(instance: type?)
|
||||
local properties = types.newtable()
|
||||
|
||||
while instance do
|
||||
for i, v in instance:properties() do
|
||||
local connector = v.read and v.read.tag == "table" and v.read:readproperty(types.singleton("Connect"))
|
||||
if connector then
|
||||
local params = connector:parameters().head
|
||||
if not params then continue end
|
||||
local listener = params[2]
|
||||
if not listener then continue end
|
||||
properties:setproperty(i, types.optional(listener))
|
||||
elseif v.write then
|
||||
properties:setproperty(i, types.optional(types.unionof(
|
||||
v.write,
|
||||
types.newfunction({}, { head = { v.write } })
|
||||
)))
|
||||
end
|
||||
end
|
||||
|
||||
instance = instance:readparent()
|
||||
end
|
||||
|
||||
properties:setindexer(types.number, types.any)
|
||||
|
||||
return properties
|
||||
end
|
||||
|
||||
type Create = <Name>(Name|keyof<Instances>|"") -> (Properties<index<Instances, Name>>) -> index<Instances, Name>
|
||||
|
||||
return create :: Create
|
||||
type Props = { [any]: any }
|
||||
return (create :: any) ::
|
||||
( <T>(T & Instance) -> (Props) -> T ) &
|
||||
( ("Folder") -> (Props) -> Folder ) &
|
||||
( ("BillboardGui") -> (Props) -> BillboardGui ) &
|
||||
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
|
||||
( ("Frame") -> (Props) -> Frame ) &
|
||||
( ("ImageButton") -> (Props) -> ImageButton ) &
|
||||
( ("ImageLabel") -> (Props) -> ImageLabel ) &
|
||||
( ("ScreenGui") -> (Props) -> ScreenGui ) &
|
||||
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
|
||||
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
|
||||
( ("TextBox") -> (Props) -> TextBox ) &
|
||||
( ("TextButton") -> (Props) -> TextButton ) &
|
||||
( ("TextLabel") -> (Props) -> TextLabel ) &
|
||||
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
|
||||
( ("UICorner") -> (Props) -> UICorner ) &
|
||||
( ("UIGradient") -> (Props) -> UIGradient ) &
|
||||
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
|
||||
( ("UIListLayout") -> (Props) -> UIListLayout ) &
|
||||
( ("UIPadding") -> (Props) -> UIPadding ) &
|
||||
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
|
||||
( ("UIScale") -> (Props) -> UIScale ) &
|
||||
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
|
||||
( ("UIStroke") -> (Props) -> UIStroke ) &
|
||||
( ("UITableLayout") -> (Props) -> UITableLayout ) &
|
||||
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
|
||||
( ("VideoFrame") -> (Props) -> VideoFrame ) &
|
||||
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
|
||||
( (string) -> (Props) -> Instance )
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
local Enum = game and Enum or require "../test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
|
||||
local Enum = game and Enum or require "test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
|
||||
return {
|
||||
Part = {
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
Size = vector.create(1, 1, 1),
|
||||
Size = Vector3.new(1, 1, 1),
|
||||
Anchored = true
|
||||
},
|
||||
|
||||
|
|
@ -110,4 +111,4 @@ return {
|
|||
BorderColor3 = Color3.new(0, 0, 0),
|
||||
BorderSizePixel = 0
|
||||
}
|
||||
} :: { [string]: { [string]: unknown} }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
|
|
@ -10,7 +12,7 @@ local function derive<T>(source: () -> T): () -> T
|
|||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
push_scope_as_child_of(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
|
|
|||
|
|
@ -4,9 +4,4 @@ end
|
|||
|
||||
local is_O2 = inline_test() ~= "inline_test"
|
||||
|
||||
return {
|
||||
strict = not is_O2,
|
||||
defaults = true,
|
||||
defer_nested_properties = true,
|
||||
batch = false,
|
||||
}
|
||||
return { strict = not is_O2, batch = false }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
local flags = require "./flags"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
export type SourceNode<T> = {
|
||||
cache: T,
|
||||
|
|
@ -21,23 +24,9 @@ export type Node<T> = {
|
|||
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||
|
||||
local function efn(err: string)
|
||||
local trace = debug.traceback(err, 2)
|
||||
|
||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
||||
return "\n "
|
||||
end)
|
||||
end
|
||||
|
||||
trace ..= "\nsource update stacktrace:"
|
||||
return trace
|
||||
end
|
||||
|
||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||
|
||||
local thread = coroutine.create(xpcall)
|
||||
--local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||
|
||||
assert(resume_ok)
|
||||
|
|
@ -58,9 +47,9 @@ local function assert_stable_scope(): Node<unknown>
|
|||
|
||||
if not scope then
|
||||
local caller_name = debug.info(2, "n")
|
||||
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
|
||||
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
|
||||
elseif scope.effect then
|
||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
||||
throw("cannot create a new reactive scope inside another reactive scope")
|
||||
end
|
||||
|
||||
return scope
|
||||
|
|
@ -93,9 +82,9 @@ end
|
|||
|
||||
local function flush_cleanups<T>(node: Node<T>)
|
||||
if node.cleanups then
|
||||
for _, fn in node.cleanups do
|
||||
local ok, err: string? = xpcall(fn, debug.traceback)
|
||||
if not ok then error(`cleanup error: {err}`, 0) end
|
||||
for _, fn in next, node.cleanups do
|
||||
local ok, err: string? = pcall(fn)
|
||||
if not ok then throw(`cleanup error: {err}`) end
|
||||
end
|
||||
|
||||
table.clear(node.cleanups)
|
||||
|
|
@ -119,10 +108,6 @@ local function unparent<T>(node: Node<T>)
|
|||
end
|
||||
|
||||
local function destroy<T>(node: Node<T>)
|
||||
if flags.strict and table.find(scopes, node) then
|
||||
error("attempt to destroy an active scope", 0)
|
||||
end
|
||||
|
||||
flush_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
|
|
@ -148,10 +133,6 @@ local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
|||
|
||||
local function evaluate_node<T>(node: Node<T>)
|
||||
if flags.strict then
|
||||
if table.find(scopes, node) then
|
||||
error("a scope, that should rerun due to the update of a source, is already active", 0)
|
||||
end
|
||||
|
||||
local initial_value = node.cache
|
||||
|
||||
for i = 1, 2 do
|
||||
|
|
@ -167,7 +148,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
||||
throw(`effect stacktrace:\n{new_value :: string}`)
|
||||
end
|
||||
|
||||
node.cache = new_value :: T
|
||||
|
|
@ -187,7 +168,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
error(`effect error:\n{new_value}\n`, 0)
|
||||
throw(`effect stacktrace:\n{new_value}\n`)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
|
@ -249,7 +230,7 @@ local function update_descendants<T>(root: SourceNode<T>)
|
|||
update_queue.n = n0
|
||||
end
|
||||
|
||||
local function push_scope_as_child_of<T>(node: SourceNode<T>)
|
||||
local function push_child_to_scope<T>(node: SourceNode<T>)
|
||||
local scope = get_scope()
|
||||
if scope and scope.effect then -- do not track nodes with no effect
|
||||
push_child(node, scope)
|
||||
|
|
@ -306,7 +287,7 @@ return table.freeze {
|
|||
push_cleanup = push_cleanup,
|
||||
destroy = destroy,
|
||||
flush_cleanups = flush_cleanups,
|
||||
push_scope_as_child_of = push_scope_as_child_of,
|
||||
push_child_to_scope = push_child_to_scope,
|
||||
update_descendants = update_descendants,
|
||||
push_child = push_child,
|
||||
create_node = create_node,
|
||||
|
|
@ -315,7 +296,5 @@ return table.freeze {
|
|||
flush_update_queue = flush_update_queue,
|
||||
get_update_queue_length = get_update_queue_length,
|
||||
set_context = set_context,
|
||||
scopes = scopes,
|
||||
|
||||
q = update_queue
|
||||
scopes = scopes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
125
src/init.luau
125
src/init.luau
|
|
@ -1,12 +1,121 @@
|
|||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
||||
--------------------------------------------------------------------------------
|
||||
-- vide.luau
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local vide = require("@self/lib")
|
||||
local version = { major = 0, minor = 3, patch = 1 }
|
||||
|
||||
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>
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.root)
|
||||
local mount = require(script.mount)
|
||||
local create = require(script.create)
|
||||
local apply = require(script.apply)
|
||||
local source = require(script.source)
|
||||
local effect = require(script.effect)
|
||||
local derive = require(script.derive)
|
||||
local cleanup = require(script.cleanup)
|
||||
local untrack = require(script.untrack)
|
||||
local read = require(script.read)
|
||||
local batch = require(script.batch)
|
||||
local context = require(script.context)
|
||||
local switch = require(script.switch)
|
||||
local show = require(script.show)
|
||||
local indexes, values = require(script.maps)()
|
||||
local spring, update_springs = require(script.spring)()
|
||||
local action = require(script.action)()
|
||||
local changed = require(script.changed)
|
||||
local throw = require(script.throw)
|
||||
local flags = require(script.flags)
|
||||
|
||||
export type Source<T> = source.Source<T>
|
||||
export type source<T> = Source<T>
|
||||
export type Context<T> = context.Context<T>
|
||||
export type context<T> = Context<T>
|
||||
|
||||
local function step(dt: number)
|
||||
if game then
|
||||
debug.profilebegin("VIDE STEP")
|
||||
debug.profilebegin("VIDE SPRING")
|
||||
end
|
||||
|
||||
update_springs(dt)
|
||||
|
||||
if game then
|
||||
debug.profileend()
|
||||
debug.profileend()
|
||||
end
|
||||
end
|
||||
|
||||
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||
task.defer(step, dt)
|
||||
end)
|
||||
|
||||
local vide = {
|
||||
version = version,
|
||||
|
||||
-- core
|
||||
root = root,
|
||||
mount = mount,
|
||||
create = create,
|
||||
source = source,
|
||||
effect = effect,
|
||||
derive = derive,
|
||||
switch = switch,
|
||||
show = show,
|
||||
indexes = indexes,
|
||||
values = values,
|
||||
|
||||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
context = context,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
||||
-- actions
|
||||
action = action,
|
||||
changed = changed,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
|
||||
-- temporary
|
||||
apply = function(instance: Instance)
|
||||
return function(props: { [any]: any })
|
||||
apply(instance, props)
|
||||
return instance
|
||||
end
|
||||
end,
|
||||
|
||||
-- runtime
|
||||
step = function(dt: number)
|
||||
if stepped then
|
||||
stepped:Disconnect()
|
||||
stepped = nil
|
||||
end
|
||||
step(dt)
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if index == "strict" then
|
||||
return flags.strict
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end,
|
||||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if index == "strict" then
|
||||
flags.strict = value :: boolean
|
||||
else
|
||||
throw(`{tostring(index)} is not a valid member of vide`)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
return vide
|
||||
|
|
|
|||
122
src/lib.luau
122
src/lib.luau
|
|
@ -1,122 +0,0 @@
|
|||
local version = { major = 0, minor = 4, patch = 1 }
|
||||
|
||||
local root = require "./root"
|
||||
local branch = require "./branch"
|
||||
local mount = require "./mount"
|
||||
local create = require "./create"
|
||||
local apply = require "./apply"
|
||||
local source = require "./source"
|
||||
local effect = require "./effect"
|
||||
local derive = require "./derive"
|
||||
local cleanup = require "./cleanup"
|
||||
local untrack = require "./untrack"
|
||||
local read = require "./read"
|
||||
local batch = require "./batch"
|
||||
local context = require "./context"
|
||||
local switch = require "./switch"
|
||||
local show = require "./show"
|
||||
local indexes = require "./indexes"
|
||||
local values = require "./values"
|
||||
local spring, update_springs = require "./spring"()
|
||||
local action = require "./action"()
|
||||
local changed = require "./changed"
|
||||
local timeout, update_timeouts = require "./timeout"()
|
||||
local flags = require "./flags"
|
||||
|
||||
export type Source<T> = source.Source<T>
|
||||
export type source<T> = Source<T>
|
||||
export type Context<T> = context.Context<T>
|
||||
export type context<T> = Context<T>
|
||||
export type Instances = create.Instances
|
||||
export type Properties<T> = create.Properties<T>
|
||||
|
||||
local function step(dt: number)
|
||||
if game then debug.profilebegin("VIDE STEP") end
|
||||
|
||||
if game then debug.profilebegin("VIDE SPRING") end
|
||||
update_springs(dt)
|
||||
if game then debug.profileend() end
|
||||
|
||||
if game then debug.profilebegin("VIDE SCHEDULER") end
|
||||
update_timeouts(dt)
|
||||
if game then debug.profileend() end
|
||||
|
||||
if game then debug.profileend() end
|
||||
end
|
||||
|
||||
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||
task.defer(step, dt)
|
||||
end)
|
||||
|
||||
local vide = {
|
||||
version = version,
|
||||
|
||||
-- core
|
||||
root = root,
|
||||
--branch = branch,
|
||||
mount = mount,
|
||||
create = create,
|
||||
source = source,
|
||||
effect = effect,
|
||||
derive = derive,
|
||||
switch = switch,
|
||||
show = show,
|
||||
indexes = indexes,
|
||||
values = values,
|
||||
|
||||
-- util
|
||||
cleanup = cleanup,
|
||||
untrack = untrack,
|
||||
read = read,
|
||||
batch = batch,
|
||||
context = context,
|
||||
|
||||
-- animations
|
||||
spring = spring,
|
||||
|
||||
-- actions
|
||||
action = action,
|
||||
changed = changed,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
defaults = (nil :: any) :: boolean,
|
||||
defer_nested_properties = (nil :: any) :: boolean,
|
||||
|
||||
-- temporary
|
||||
apply = function(instance: Instance)
|
||||
return function(props: { [any]: any })
|
||||
apply(instance, props)
|
||||
return instance
|
||||
end
|
||||
end,
|
||||
|
||||
-- runtime
|
||||
step = function(dt: number)
|
||||
if stepped then
|
||||
stepped:Disconnect()
|
||||
stepped = nil
|
||||
end
|
||||
step(dt)
|
||||
end
|
||||
}
|
||||
|
||||
setmetatable(vide :: any, {
|
||||
__index = function(_, index: unknown): ()
|
||||
if flags[index] == nil then
|
||||
error(`{tostring(index)} is not a valid member of vide`, 0)
|
||||
else
|
||||
return flags[index]
|
||||
end
|
||||
end,
|
||||
|
||||
__newindex = function(_, index: unknown, value: unknown)
|
||||
if flags[index] == nil then
|
||||
error(`{tostring(index)} is not a valid member of vide, 0`)
|
||||
else
|
||||
flags[index] = value
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
return vide
|
||||
219
src/maps.luau
Normal file
219
src/maps.luau
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_source_node = graph.create_source_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local update_descendants = graph.update_descendants
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local destroy = graph.destroy
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function check_primitives(t: {})
|
||||
if not flags.strict then return end
|
||||
|
||||
for _, v in next, t do
|
||||
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||
throw("table source map cannot return primitives")
|
||||
end
|
||||
end
|
||||
|
||||
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||
local owner = assert_stable_scope()
|
||||
local subowner = create_node(owner, false, false)
|
||||
|
||||
local input_cache = {} :: Map<K, VI>
|
||||
local output_cache = {} :: Map<K, VO>
|
||||
local input_nodes = {} :: Map<K, SourceNode<VI>>
|
||||
local remove_queue = {} :: { K }
|
||||
local scopes = {} :: Map<K, Node<unknown>>
|
||||
|
||||
local function update_children(data)
|
||||
-- queue removed values
|
||||
for i in next, input_cache do
|
||||
if data[i] == nil then
|
||||
table.insert(remove_queue, i)
|
||||
end
|
||||
end
|
||||
|
||||
-- remove queued values
|
||||
for _, i in next, remove_queue do
|
||||
destroy(scopes[i])
|
||||
|
||||
input_cache[i] = nil
|
||||
output_cache[i] = nil
|
||||
input_nodes[i] = nil
|
||||
scopes[i] = nil
|
||||
end
|
||||
|
||||
table.clear(remove_queue)
|
||||
|
||||
push_scope(subowner)
|
||||
|
||||
-- process new or changed values
|
||||
for i, v in next, data do
|
||||
local cv = input_cache[i]
|
||||
|
||||
if cv ~= v then
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[i] = scope :: Node<any>
|
||||
|
||||
local node = create_source_node(v)
|
||||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end, i)
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
pop_scope() -- subowner scope
|
||||
error(result, 0)
|
||||
end
|
||||
|
||||
input_nodes[i] = node
|
||||
output_cache[i] = result
|
||||
else -- update source
|
||||
input_nodes[i].cache = v
|
||||
update_descendants(input_nodes[i])
|
||||
end
|
||||
|
||||
input_cache[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
pop_scope()
|
||||
|
||||
local output_array = table.create(#scopes)
|
||||
for _, v in next, output_cache do
|
||||
table.insert(output_array, v)
|
||||
end
|
||||
check_primitives(output_array)
|
||||
|
||||
return output_array
|
||||
end
|
||||
|
||||
local node = create_node(owner, function()
|
||||
return update_children(input())
|
||||
end, false :: any)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||
local owner = assert_stable_scope()
|
||||
local subowner = create_node(owner, false, false)
|
||||
|
||||
local cur_input_cache_up = {} :: Map<VI, K>
|
||||
local new_input_cache_up = {} :: Map<VI, K>
|
||||
local output_cache = {} :: Map<VI, VO>
|
||||
local input_nodes = {} :: Map<VI, SourceNode<K>>
|
||||
local scopes = {} :: Map<VI, Node<unknown>>
|
||||
|
||||
local function update_children(data: Map<K, VI>)
|
||||
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
|
||||
|
||||
if flags.strict then
|
||||
local cache = {}
|
||||
for _, v in next, data do
|
||||
if cache[v] ~= nil then
|
||||
throw "duplicate table value detected"
|
||||
end
|
||||
cache[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
push_scope(subowner)
|
||||
|
||||
-- process data
|
||||
for i, v in next, data do
|
||||
new_input_cache[v] = i
|
||||
|
||||
local cv = cur_input_cache[v]
|
||||
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[v] = scope :: Node<any>
|
||||
|
||||
local node = create_source_node(i)
|
||||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, v, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end)
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
pop_scope() -- subowner scope
|
||||
error(result, 0)
|
||||
end
|
||||
|
||||
input_nodes[v] = node
|
||||
output_cache[v] = result
|
||||
else -- update source
|
||||
if cv ~= i then
|
||||
input_nodes[v].cache = i
|
||||
update_descendants(input_nodes[v])
|
||||
end
|
||||
|
||||
cur_input_cache[v] = nil
|
||||
end
|
||||
end
|
||||
|
||||
pop_scope()
|
||||
|
||||
-- remove old values
|
||||
for v in next, cur_input_cache do
|
||||
destroy(scopes[v])
|
||||
|
||||
output_cache[v] = nil
|
||||
input_nodes[v] = nil
|
||||
scopes[v] = nil
|
||||
end
|
||||
|
||||
-- update buffer cache
|
||||
table.clear(cur_input_cache)
|
||||
cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache
|
||||
|
||||
local output_array = table.create(#scopes)
|
||||
for _, v in next, output_cache do
|
||||
table.insert(output_array, v)
|
||||
end
|
||||
check_primitives(output_array)
|
||||
|
||||
return output_array
|
||||
end
|
||||
|
||||
local node = create_node(owner, function()
|
||||
return update_children(input())
|
||||
end, false :: any)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
||||
return function() return indexes, values end
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
local root = require "./root"
|
||||
local apply = require "./apply"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.Parent.root)
|
||||
local apply = require(script.Parent.apply)
|
||||
|
||||
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
return root(function()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function read<T>(value: T | () -> T): T
|
||||
return if type(value) == "function" then value() else value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local push_scope = graph.push_scope
|
||||
|
|
@ -13,20 +16,21 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
|||
refs[node] = true -- prevent gc of root node
|
||||
|
||||
local destroy = function()
|
||||
if not refs[node] then error "root already destroyed" end
|
||||
if not refs[node] then throw "root already destroyed" end
|
||||
refs[node] = nil
|
||||
destroy(node)
|
||||
end
|
||||
|
||||
push_scope(node)
|
||||
|
||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local result = { xpcall(fn, efn, destroy) }
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not result[1] then
|
||||
destroy()
|
||||
error(`error while running root():\n\n{result[2]}`, 0)
|
||||
throw(`error while running root():\n\n{result[2]}`)
|
||||
end
|
||||
|
||||
return destroy, unpack(result :: any, 2)
|
||||
|
|
|
|||
|
|
@ -1,36 +1,18 @@
|
|||
local source = require "./source"
|
||||
local derive = require "./derive"
|
||||
local effect = require "./effect"
|
||||
local untrack = require "./untrack"
|
||||
local switch = require "./switch"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
type Array<T> = { T }
|
||||
type Source<T> = () -> T
|
||||
local switch = require(script.Parent.switch)
|
||||
|
||||
local function show<T, Obj>(
|
||||
input: Source<T?>,
|
||||
component: (Source<T>, Source<boolean>) -> (Obj, ...number),
|
||||
fallback: ((Source<boolean>) -> (Obj, ...number))?
|
||||
): Source<nil | Obj | Array<Obj>>
|
||||
local filtered_input = source()
|
||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||
local function truthy()
|
||||
return not not source()
|
||||
end
|
||||
|
||||
effect(function()
|
||||
local v = input()
|
||||
if v then
|
||||
filtered_input(v)
|
||||
end
|
||||
end)
|
||||
|
||||
local input_is_truthy = derive(function()
|
||||
return not not input()
|
||||
end)
|
||||
|
||||
return switch(input_is_truthy) {
|
||||
[true] = function(present)
|
||||
return component(filtered_input, present)
|
||||
end,
|
||||
[false] = fallback
|
||||
return switch(truthy) {
|
||||
[true] = component,
|
||||
[false] = fallback,
|
||||
}
|
||||
end
|
||||
|
||||
return show
|
||||
return show ::
|
||||
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
|
||||
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_source_node = graph.create_source_node
|
||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local update_descendants = graph.update_descendants
|
||||
|
||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||
|
|
@ -9,9 +11,9 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
|
|||
local function source<T>(initial_value: T): Source<T>
|
||||
local node = create_source_node(initial_value)
|
||||
|
||||
local function update_source(...): T
|
||||
return function(...): T
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
push_scope_as_child_of(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
|
||||
|
|
@ -24,8 +26,6 @@ local function source<T>(initial_value: T): Source<T>
|
|||
update_descendants(node)
|
||||
return v
|
||||
end
|
||||
|
||||
return update_source
|
||||
end
|
||||
|
||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||
|
|
|
|||
259
src/spring.luau
259
src/spring.luau
|
|
@ -1,4 +1,28 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
|
||||
--[[
|
||||
|
||||
Supported datatypes:
|
||||
- number
|
||||
- CFrame
|
||||
- Color3
|
||||
- UDim
|
||||
- UDim2
|
||||
- Vector2
|
||||
- Vector3
|
||||
- Rect
|
||||
|
||||
Unsupported datatypes:
|
||||
- bool
|
||||
- Vector2int16
|
||||
- Vector3int16
|
||||
- EnumItem
|
||||
|
||||
]]
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
|
|
@ -6,79 +30,74 @@ local create_source_node = graph.create_source_node
|
|||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local update_descendants = graph.update_descendants
|
||||
local push_scope_as_child_of = graph.push_scope_as_child_of
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
|
||||
local UPDATE_RATE = 120
|
||||
local TOLERANCE_FACTOR = 10_000
|
||||
local TOLERANCE = 0.0001
|
||||
|
||||
type Vec3 = Vector3
|
||||
|
||||
local function Vec3(x: number?, y: number?, z: number?)
|
||||
return Vector3.new(x, y, z)
|
||||
end
|
||||
|
||||
local ZERO = Vec3(0, 0, 0)
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||
|
||||
--[[
|
||||
Unsupported datatypes:
|
||||
- bool
|
||||
- Vector2int16
|
||||
- Vector3int16
|
||||
- EnumItem
|
||||
]]
|
||||
|
||||
type SpringState<T> = {
|
||||
type SpringData<T> = {
|
||||
k: number, -- spring constant
|
||||
c: number, -- damping coeff
|
||||
|
||||
x0_123: vector, x0_456: vector, -- initial position
|
||||
x_123: vector, x_456: vector, -- current position
|
||||
x1_123: vector, x1_456: vector, -- target position
|
||||
v_123: vector, v_456: vector, -- current velocity
|
||||
-- dimensions 1-3
|
||||
x0_123: Vec3,
|
||||
x1_123: Vec3,
|
||||
v_123: Vec3,
|
||||
|
||||
-- dimensions 4-6
|
||||
x0_456: Vec3,
|
||||
x1_456: Vec3,
|
||||
v_456: Vec3,
|
||||
|
||||
source_value: T -- current value of spring input source
|
||||
}
|
||||
|
||||
type SpringSettings<T> = ({
|
||||
position: T?,
|
||||
velocity: T?,
|
||||
impulse: T?
|
||||
}) -> ()
|
||||
|
||||
type TypeToVec6<T> = (T) -> (vector, vector)
|
||||
type Vec6ToType<T> = (vector, vector) -> T
|
||||
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
||||
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
||||
|
||||
local type_to_vec6 = {
|
||||
number = function(v)
|
||||
return vector.create(v, 0, 0), vector.zero
|
||||
return Vec3(v, 0, 0), ZERO
|
||||
end :: TypeToVec6<number>,
|
||||
|
||||
CFrame = function(v)
|
||||
return v.Position, vector.create(v:ToEulerAnglesXYZ())
|
||||
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
||||
end :: TypeToVec6<CFrame>,
|
||||
|
||||
Color3 = function(v)
|
||||
-- todo: hsv, oklab?
|
||||
return vector.create(v.R, v.G, v.B), vector.zero
|
||||
return Vec3(v.R, v.G, v.B), ZERO
|
||||
end :: TypeToVec6<Color3>,
|
||||
|
||||
UDim = function(v)
|
||||
return vector.create(v.Scale, v.Offset, 0), vector.zero
|
||||
return Vec3(v.Scale, v.Offset, 0), ZERO
|
||||
end :: TypeToVec6<UDim>,
|
||||
|
||||
UDim2 = function(v)
|
||||
return vector.create(v.X.Scale, v.X.Offset, v.Y.Scale), vector.create(v.Y.Offset, 0, 0)
|
||||
return Vec3(v.X.Scale, v.X.Offset, v.Y.Scale), Vec3(v.Y.Offset, 0, 0)
|
||||
end :: TypeToVec6<UDim2>,
|
||||
|
||||
Vector2 = function(v)
|
||||
return vector.create(v.X, v.Y, 0), vector.zero
|
||||
return Vec3(v.X, v.Y, 0), ZERO
|
||||
end :: TypeToVec6<Vector2>,
|
||||
|
||||
Vector3 = function(v)
|
||||
return v, vector.zero
|
||||
return v, ZERO
|
||||
end :: TypeToVec6<Vector3>,
|
||||
|
||||
Rect = function(v)
|
||||
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
|
||||
end :: TypeToVec6<Rect>,
|
||||
|
||||
table = function(v)
|
||||
return vector.create(v[1] or 0, v[2] or 0, v[3] or 0), vector.create(v[4] or 0, 0, 0)
|
||||
end :: TypeToVec6<{ number }>
|
||||
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
|
||||
end :: TypeToVec6<Rect>
|
||||
}
|
||||
|
||||
local vec6_to_type = {
|
||||
|
|
@ -112,16 +131,12 @@ local vec6_to_type = {
|
|||
|
||||
Rect = function(a, b)
|
||||
return Rect.new(a.X, a.Y, a.Z, b.X)
|
||||
end :: Vec6ToType<Rect>,
|
||||
|
||||
table = function(a, b)
|
||||
return { a.X, a.Y, a.Z, b.X }
|
||||
end :: Vec6ToType<{ number }>
|
||||
end :: Vec6ToType<Rect>
|
||||
}
|
||||
|
||||
local invalid_type = {
|
||||
__index = function(_, t: string)
|
||||
error(`cannot spring type {t}`, 0)
|
||||
throw(`cannot spring type {t}`)
|
||||
end
|
||||
}
|
||||
|
||||
|
|
@ -130,15 +145,17 @@ setmetatable(vec6_to_type, invalid_type)
|
|||
|
||||
-- maps spring data to its corresponding output node
|
||||
-- lifetime of spring data is tied to output node
|
||||
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
|
||||
setmetatable(springs :: any, { __mode = "v" })
|
||||
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
||||
setmetatable(springs, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local owner = assert_stable_scope()
|
||||
|
||||
-- https://en.wikipedia.org/wiki/Damping
|
||||
|
||||
local w_n = 2*math.pi / (period or 1)
|
||||
local z = damping_ratio or 1
|
||||
|
||||
local k = w_n^2
|
||||
local c_c = 2*w_n
|
||||
local c = z * c_c
|
||||
|
|
@ -146,22 +163,20 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
-- todo: is there a solution other than reducing step size?
|
||||
-- todo: this does not catch all solver exploding cases
|
||||
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
||||
error("spring damping too high, consider reducing damping or increasing period", 0)
|
||||
throw("spring damping too high, consider reducing damping or increasing period")
|
||||
end
|
||||
|
||||
local data: SpringState<T> = {
|
||||
local data: SpringData<T> = {
|
||||
k = k,
|
||||
c = c,
|
||||
|
||||
x0_123 = vector.zero,
|
||||
x_123 = vector.zero,
|
||||
x1_123 = vector.zero,
|
||||
v_123 = vector.zero,
|
||||
x0_123 = ZERO,
|
||||
x1_123 = ZERO,
|
||||
v_123 = ZERO,
|
||||
|
||||
x0_456 = vector.zero,
|
||||
x_456 = vector.zero,
|
||||
x1_456 = vector.zero,
|
||||
v_456 = vector.zero,
|
||||
x0_456 = ZERO,
|
||||
x1_456 = ZERO,
|
||||
v_456 = ZERO,
|
||||
|
||||
source_value = false :: any,
|
||||
}
|
||||
|
|
@ -172,57 +187,33 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
local value = source()
|
||||
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
||||
data.source_value = value
|
||||
springs[data] = output
|
||||
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
|
||||
return value
|
||||
end
|
||||
|
||||
local updater = create_node(owner, updater_effect, false :: any)
|
||||
|
||||
evaluate_node(updater)
|
||||
|
||||
-- set initial position to goal
|
||||
data.x_123, data.x_456 = data.x1_123, data.x1_456
|
||||
data.x0_123, data.x0_456 = data.x1_123, data.x1_456
|
||||
|
||||
-- set output to goal
|
||||
output.cache = data.source_value
|
||||
|
||||
local config = function(p)
|
||||
local x = p.position
|
||||
local v = p.velocity
|
||||
local dv = p.impulse
|
||||
|
||||
if x then
|
||||
local x_123, x_456 = type_to_vec6[typeof(x)](x)
|
||||
data.x_123, data.x_456 = x_123, x_456
|
||||
data.x0_123, data.x0_456 = x_123, x_456
|
||||
end
|
||||
|
||||
if v then
|
||||
data.v_123, data.v_456 = type_to_vec6[typeof(v)](v)
|
||||
end
|
||||
|
||||
if dv then
|
||||
local dv_123, dv_456 = type_to_vec6[typeof(dv)](dv)
|
||||
data.v_123 += dv_123
|
||||
data.v_456 += dv_456
|
||||
end
|
||||
|
||||
-- schedule spring
|
||||
springs[data] = output
|
||||
end :: SpringSettings<T>
|
||||
|
||||
return function(...)
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
push_scope_as_child_of(output)
|
||||
push_child_to_scope(output)
|
||||
return output.cache
|
||||
end
|
||||
|
||||
-- set current position to value
|
||||
local v = ... :: T
|
||||
data.x_123, data.x_456 = type_to_vec6[typeof(v)](v)
|
||||
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
|
||||
|
||||
-- reset velocity
|
||||
data.v_123 = vector.zero
|
||||
data.v_456 = vector.zero
|
||||
data.v_123 = ZERO
|
||||
data.v_456 = ZERO
|
||||
|
||||
-- schedule spring
|
||||
springs[data] = output
|
||||
|
|
@ -231,32 +222,21 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
output.cache = v
|
||||
|
||||
return v
|
||||
end, config
|
||||
end
|
||||
|
||||
-- calculates a float tolerance, based on the magnitude of the float
|
||||
local function get_min_step(x: number): number
|
||||
return x/TOLERANCE_FACTOR
|
||||
end
|
||||
local function get_min_vector_step(direction: vector): vector
|
||||
return vector.create(
|
||||
get_min_step(direction.x),
|
||||
get_min_step(direction.y),
|
||||
get_min_step(direction.z)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function step_springs(dt: number)
|
||||
for s in springs do
|
||||
local k = s.k
|
||||
local c = s.c
|
||||
local x_123, x_456 = s.x_123, s.x_456
|
||||
local x1_123, x1_456 = s.x1_123, s.x1_456
|
||||
local u_123, u_456 = s.v_123, s.v_456
|
||||
for data in next, springs do
|
||||
local k, c,
|
||||
x0_123, x1_123, u_123,
|
||||
x0_456, x1_456, u_456 =
|
||||
data.k, data.c,
|
||||
data.x0_123, data.x1_123, data.v_123,
|
||||
data.x0_456, data.x1_456, data.v_456
|
||||
|
||||
-- calculate displacement from target
|
||||
local dx_123 = x_123 - x1_123
|
||||
local dx_456 = x_456 - x1_456
|
||||
local dx_123 = x0_123 - x1_123
|
||||
local dx_456 = x0_456 - x1_456
|
||||
|
||||
-- calculate spring force
|
||||
local fs_123 = dx_123*-k
|
||||
|
|
@ -266,50 +246,53 @@ local function step_springs(dt: number)
|
|||
local ff_123 = u_123*-c
|
||||
local ff_456 = u_456*-c
|
||||
|
||||
-- calculate acceleration
|
||||
local a_123 = (fs_123 + ff_123)
|
||||
local a_456 = (fs_456 + ff_456)
|
||||
-- calculate acceleration step
|
||||
local dv_123 = (fs_123 + ff_123)*dt
|
||||
local dv_456 = (fs_456 + ff_456)*dt
|
||||
|
||||
-- step acceleration
|
||||
local v_123 = u_123 + a_123*dt
|
||||
local v_456 = u_456 + a_456*dt
|
||||
-- apply acceleration step
|
||||
local v_123 = u_123 + dv_123
|
||||
local v_456 = u_456 + dv_456
|
||||
|
||||
-- step velocity
|
||||
local y_123 = x_123 + v_123*dt
|
||||
local y_456 = x_456 + v_456*dt
|
||||
-- calculate new position
|
||||
local x_123 = x0_123 + v_123*dt
|
||||
local x_456 = x0_456 + v_456*dt
|
||||
|
||||
s.x_123, s.x_456 = y_123, y_456
|
||||
s.v_123, s.v_456 = v_123, v_456
|
||||
data.x0_123, data.x0_456 = x_123, x_456
|
||||
data.v_123, data.v_456 = v_123, v_456
|
||||
end
|
||||
end
|
||||
|
||||
local remove_queue = {}
|
||||
|
||||
local function update_spring_sources()
|
||||
for data, output in springs do
|
||||
local x0_123, x0_456 = data.x0_123, data.x0_456
|
||||
local x_123, x_456 = data.x_123, data.x_456
|
||||
local x1_123, x1_456 = data.x1_123, data.x1_456
|
||||
local v_123, v_456 = data.v_123, data.v_456
|
||||
for data, output in next, springs do
|
||||
local x0_123, x1_123, v_123,
|
||||
x0_456, x1_456, v_456 =
|
||||
data.x0_123, data.x1_123, data.v_123,
|
||||
data.x0_456, data.x1_456, data.v_456
|
||||
|
||||
local dx_123, dx_456 =
|
||||
x0_123 - x1_123,
|
||||
x0_456 - x1_456
|
||||
|
||||
local tol_123 = vector.abs(get_min_vector_step(x0_123 - x1_123))
|
||||
local tol_456 = vector.abs(get_min_vector_step(x0_456 - x1_456))
|
||||
|
||||
if
|
||||
-- position is at goal (within tolerance)
|
||||
vector.max(vector.abs(x_123 - x1_123), tol_123) == tol_123
|
||||
and vector.max(vector.abs(x_456 - x1_456), tol_456) == tol_456
|
||||
|
||||
-- velocity is at 0 (within tolerance)
|
||||
and vector.max(vector.abs(v_123/10), tol_123) == tol_123
|
||||
and vector.max(vector.abs(v_456/10), tol_456) == tol_456
|
||||
then
|
||||
springs[data] = nil
|
||||
-- todo: can this false positive?
|
||||
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
||||
-- close enough to target, unshedule spring and set value to target
|
||||
table.insert(remove_queue, data)
|
||||
output.cache = data.source_value
|
||||
else
|
||||
output.cache = vec6_to_type[typeof(data.source_value)](x_123, x_456)
|
||||
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
|
||||
end
|
||||
|
||||
update_descendants(output)
|
||||
end
|
||||
|
||||
for _, data in next, remove_queue do
|
||||
springs[data] = nil
|
||||
end
|
||||
|
||||
table.clear(remove_queue)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
161
src/switch.luau
161
src/switch.luau
|
|
@ -1,113 +1,64 @@
|
|||
local branch = require "./branch"
|
||||
local source = require "./source"
|
||||
local effect = require "./effect"
|
||||
local timeout = require "./timeout" ()
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local destroy = graph.destroy
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
|
||||
type Array<T> = { T }
|
||||
type Map<K, V> = { [K]: V }
|
||||
type Source<T> = () -> T
|
||||
type Component<T> = (Source<boolean>) -> (T, ...number)
|
||||
|
||||
local function switch_map<K, Obj>(
|
||||
input: Source<K>,
|
||||
map: Map<K, Component<Obj>>
|
||||
): Source<nil | Obj | Array<Obj>>
|
||||
local scopes = {} :: Map<K, {
|
||||
destroy: () -> (),
|
||||
object: Obj,
|
||||
delay: number,
|
||||
present: (boolean?) -> boolean,
|
||||
timeout: { cancel: boolean }?
|
||||
}>
|
||||
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
|
||||
local owner = assert_stable_scope()
|
||||
|
||||
local output = source(nil :: nil | Obj | Array<Obj>)
|
||||
local function update_output()
|
||||
local objects = {}
|
||||
|
||||
for _, scope in scopes do
|
||||
table.insert(objects, scope.object)
|
||||
end
|
||||
|
||||
output(
|
||||
if objects[2] then objects
|
||||
elseif objects[1] then objects[1]
|
||||
else nil
|
||||
)
|
||||
end
|
||||
|
||||
effect(function()
|
||||
local key: K? = input()
|
||||
|
||||
-- destroy (or queue destroy) all scopes not associated with the input key
|
||||
for k, scope in scopes do
|
||||
if k == key then continue end
|
||||
|
||||
scope.present(false)
|
||||
|
||||
if scope.delay == 0 then
|
||||
scope.destroy()
|
||||
scopes[k] = nil
|
||||
else
|
||||
if scope.timeout == nil then
|
||||
scope.timeout = timeout(scope.delay, function()
|
||||
scope.destroy()
|
||||
scopes[k] = nil
|
||||
update_output()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create new scope or abort destruction of existing scope if key exists
|
||||
if key ~= nil then
|
||||
local scope = scopes[key]
|
||||
|
||||
if scope then
|
||||
scope.present(true)
|
||||
|
||||
if scope.timeout then
|
||||
scope.timeout.cancel = true
|
||||
scope.timeout = nil
|
||||
end
|
||||
else
|
||||
local component = map[key]
|
||||
|
||||
if component ~= nil then
|
||||
if type(component) ~= "function" then
|
||||
error("map must map a value to a function", 0)
|
||||
end
|
||||
|
||||
local present = source(false)
|
||||
|
||||
local delay = nil :: number?
|
||||
local destroy, object = branch(function()
|
||||
local object, t = component(present)
|
||||
delay = t
|
||||
return object
|
||||
end)
|
||||
|
||||
present(true)
|
||||
|
||||
scopes[key] = {
|
||||
destroy = destroy,
|
||||
object = object,
|
||||
delay = delay or 0,
|
||||
present = present,
|
||||
timeout = nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_output()
|
||||
end)
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
local function switch<K, Obj>(input: Source<K>): (map: Map<K, Component<Obj>>) -> Source<nil | Obj | Array<Obj>>
|
||||
return function(map)
|
||||
return switch_map(input, map)
|
||||
local last_scope: Node<false>?
|
||||
local last_component: (() -> U)?
|
||||
|
||||
local function update(cached): U?
|
||||
local component = map[source()]
|
||||
if component == last_component then return cached end
|
||||
last_component = component
|
||||
|
||||
if last_scope then
|
||||
destroy(last_scope :: Node<any>)
|
||||
last_scope = nil
|
||||
end
|
||||
|
||||
if component == nil then return nil end
|
||||
|
||||
if type(component) ~= "function" then
|
||||
throw "map must map a value to a function"
|
||||
end
|
||||
|
||||
local new_scope = create_node(owner, false, false)
|
||||
last_scope = new_scope :: Node<any>
|
||||
|
||||
push_scope(new_scope)
|
||||
|
||||
local ok, result = pcall(component)
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local node = create_node(owner, update, nil)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
7
src/throw.luau
Normal file
7
src/throw.luau
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local function VIDE_ASSERT(msg): any
|
||||
error(msg, 0)
|
||||
end
|
||||
|
||||
return VIDE_ASSERT
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
local queue = {} :: {
|
||||
{ t: number, fn: () -> (), cancel: boolean }
|
||||
}
|
||||
|
||||
local function timeout(t: number, fn: () -> ())
|
||||
local handle = { t = t, fn = fn, cancel = false }
|
||||
table.insert(queue, handle)
|
||||
return handle
|
||||
end
|
||||
|
||||
local function update_timeouts(dt: number)
|
||||
for i = #queue, 1, -1 do
|
||||
local handle = queue[i]
|
||||
handle.t -= dt
|
||||
|
||||
if handle.cancel or handle.t <= 0 then
|
||||
queue[i] = queue[#queue]
|
||||
queue[#queue] = nil
|
||||
|
||||
if not handle.cancel then
|
||||
handle.fn()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return function() return timeout, update_timeouts end
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
local graph = require "./graph"
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local get_scope = graph.get_scope
|
||||
|
||||
|
|
@ -10,16 +12,16 @@ local function untrack<T>(source: () -> T): T
|
|||
local effect = scope.effect
|
||||
scope.effect = false
|
||||
|
||||
local ok, result = xpcall(source, debug.traceback)
|
||||
local ok, result = pcall(source)
|
||||
|
||||
scope.effect = effect :: () -> ()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result :: T
|
||||
return result
|
||||
else
|
||||
return source()
|
||||
end
|
||||
end
|
||||
|
||||
return untrack :: ( <T>(fn: () -> T) -> T ) & ( (fn: () -> ()) -> () )
|
||||
return untrack
|
||||
|
|
|
|||
131
src/values.luau
131
src/values.luau
|
|
@ -1,131 +0,0 @@
|
|||
local flags = require "./flags"
|
||||
local branch = require "./branch"
|
||||
local source = require "./source"
|
||||
local effect = require "./effect"
|
||||
local timeout = require "./timeout" ()
|
||||
|
||||
type Array<T> = { T }
|
||||
type Map<K, V> = { [K]: V }
|
||||
type Source<T> = () -> T
|
||||
|
||||
local function values<K, V, Obj>(
|
||||
input: Source<Map<K, V>>,
|
||||
component: (V, Source<K>, Source<boolean>) -> (Obj, ...number)
|
||||
): Source<Array<Obj>>
|
||||
local update_count = 0
|
||||
local scopes = {} :: Map<V, {
|
||||
destroy: () -> (),
|
||||
object: Obj,
|
||||
index: K?,
|
||||
index_source: (K?) -> K,
|
||||
count: number,
|
||||
delay: number,
|
||||
present: (boolean?) -> boolean,
|
||||
timeout: { cancel: boolean }?,
|
||||
}>
|
||||
|
||||
local output = source({} :: Array<Obj>)
|
||||
local function update_output()
|
||||
local objects = table.create(4)
|
||||
|
||||
for _, scope in scopes do
|
||||
table.insert(objects, scope.object)
|
||||
end
|
||||
|
||||
output(objects)
|
||||
end
|
||||
|
||||
effect(function()
|
||||
local data = input()
|
||||
|
||||
local count = update_count
|
||||
update_count += 1
|
||||
|
||||
local children_need_update = false -- set to true if a scope is created or destroyed
|
||||
|
||||
if flags.strict then -- check for duplicate values
|
||||
local map = {}
|
||||
for _, v in data do
|
||||
if map[v] then
|
||||
error("table source passed to `values()` contains duplicate values", 0)
|
||||
end
|
||||
map[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- create or update scopes
|
||||
for i, v in data do
|
||||
local scope = scopes[v]
|
||||
|
||||
if scope == nil then -- create new scope and create component
|
||||
local index_source = source(i)
|
||||
local present = source(false)
|
||||
|
||||
local delay = nil :: number?
|
||||
local destroy, object = branch(function()
|
||||
local object, t = component(v, index_source, present)
|
||||
delay = t
|
||||
return object
|
||||
end)
|
||||
|
||||
present(true)
|
||||
|
||||
children_need_update = true
|
||||
|
||||
scopes[v] = {
|
||||
destroy = destroy,
|
||||
object = object,
|
||||
index = i,
|
||||
index_source = index_source,
|
||||
count = count,
|
||||
delay = delay or 0,
|
||||
present = present,
|
||||
timeout = nil,
|
||||
}
|
||||
else -- update scope
|
||||
scope.count = count
|
||||
|
||||
if scope.index ~= i then
|
||||
if scope.timeout then -- value is in input table again; cancel destruction
|
||||
scope.timeout.cancel = true
|
||||
scope.timeout = nil
|
||||
scope.present(true)
|
||||
end
|
||||
|
||||
scope.index = i
|
||||
scope.index_source(i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- destroy scopes
|
||||
for v, scope in scopes do
|
||||
if scope.count < count then -- if count is not latest then value is no longer in the input table
|
||||
scope.present(false)
|
||||
|
||||
if scope.delay == 0 then
|
||||
scope.destroy()
|
||||
scopes[v] = nil
|
||||
children_need_update = true
|
||||
else
|
||||
scope.index = nil -- set to nil for the `scope.index ~= i` check
|
||||
if scope.timeout == nil then
|
||||
scope.timeout = timeout(scope.delay, function() -- todo: possible redundant updates
|
||||
scope.destroy()
|
||||
scopes[v] = nil
|
||||
update_output()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if children_need_update then
|
||||
update_output()
|
||||
end
|
||||
end)
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
return values
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
local testkit = require("./testkit")
|
||||
local testkit = require("test/testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
|
||||
local vide = require "../../vide"
|
||||
local vide = require "src/init"
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
|
|
@ -27,7 +27,7 @@ local function ROOT_BENCH(name: string, fn: () -> ())
|
|||
end)()
|
||||
end
|
||||
|
||||
local N = 2^20
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
TITLE "sources"
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ end)
|
|||
TITLE "property apply"
|
||||
|
||||
ROOT_BENCH("apply 0 properties", function()
|
||||
local apply = require "../src/apply"
|
||||
local apply = require "src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -209,7 +209,7 @@ ROOT_BENCH("apply 0 properties", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("apply 8 properties", function()
|
||||
local apply = require "../src/apply"
|
||||
local apply = require "src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -227,7 +227,7 @@ ROOT_BENCH("apply 8 properties", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("bind property", function()
|
||||
local apply = require "../src/apply"
|
||||
local apply = require "src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
local src = source(1)
|
||||
|
|
@ -242,7 +242,7 @@ ROOT_BENCH("bind property", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("update binding", function()
|
||||
local apply = require "../src/apply"
|
||||
local apply = require "src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
local src = source(1)
|
||||
|
|
@ -504,44 +504,85 @@ ROOT_BENCH(`get context (depth={depth})`, function()
|
|||
end)
|
||||
end)
|
||||
|
||||
TITLE "spring()"
|
||||
N *= 1024
|
||||
|
||||
ROOT_BENCH("spring update", function()
|
||||
local root, source, spring = vide.root, vide.source, vide.spring
|
||||
TITLE "aggregate"
|
||||
|
||||
local src = source(0)
|
||||
do
|
||||
-- the purpose of the two following benchmarks is to measure the overhead of
|
||||
-- aggregate construction
|
||||
ROOT_BENCH("set explicit mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
|
||||
root(function()
|
||||
for i = 1, N do
|
||||
spring(src)
|
||||
local label = create "TextLabel" {
|
||||
AnchorPoint = Vector2.new(1, 1)
|
||||
}
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(label, {
|
||||
AnchorPoint = Vector2.new(i, i)
|
||||
})
|
||||
end
|
||||
|
||||
START(N)
|
||||
|
||||
src(1)
|
||||
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
|
||||
ROOT_BENCH("spring step", function()
|
||||
local root, source, spring = vide.root, vide.source, vide.spring
|
||||
ROOT_BENCH("set aggregate mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
|
||||
local src = source(0)
|
||||
local label = create "TextLabel" {
|
||||
AnchorPoint = Vector2.new(1, 1)
|
||||
}
|
||||
|
||||
root(function()
|
||||
for i = 1, N do
|
||||
spring(src)
|
||||
for i = 1, START(N) do
|
||||
apply(label, {
|
||||
AnchorPoint = { i, i }
|
||||
})
|
||||
end
|
||||
|
||||
src(1)
|
||||
|
||||
START(N)
|
||||
|
||||
vide.step(1/60)
|
||||
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
-- innacurate due to no Vector3 in vanilla Luau
|
||||
-- mock vector is 200x slower than native vector
|
||||
|
||||
-- ROOT_BENCH("spring update", function()
|
||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
-- local src = source(0)
|
||||
|
||||
-- root(function()
|
||||
-- for i = 1, N do
|
||||
-- spring(src)
|
||||
-- end
|
||||
|
||||
-- START(N)
|
||||
|
||||
-- src(1)
|
||||
|
||||
-- return nil
|
||||
-- end)
|
||||
-- end)
|
||||
|
||||
-- N /= 1024
|
||||
|
||||
-- ROOT_BENCH("spring step", function()
|
||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
-- local src = source(0)
|
||||
|
||||
-- root(function()
|
||||
-- for i = 1, N do
|
||||
-- spring(src)
|
||||
-- end
|
||||
|
||||
-- src(1)
|
||||
|
||||
-- START(N)
|
||||
|
||||
-- vide.step(1/60)
|
||||
|
||||
-- return nil
|
||||
-- end)
|
||||
-- end)
|
||||
|
||||
return nil
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
local vide = require "../src/"
|
||||
|
||||
local count = vide.source(0)
|
||||
|
||||
vide.create("TextButton") {
|
||||
BackgroundTransparency = 1,
|
||||
AnchorPoint = "bad value", -- should error
|
||||
InvalidProperty = true, -- should error
|
||||
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end,
|
||||
|
||||
Size = function() -- should error
|
||||
return "bad value"
|
||||
end,
|
||||
|
||||
MouseEnter = function(x, y)
|
||||
|
||||
end,
|
||||
|
||||
Activated = "bad value", -- should error
|
||||
|
||||
vide.create "TextLabel" {},
|
||||
|
||||
function() end,
|
||||
}
|
||||
|
|
@ -86,9 +86,6 @@ local Instance = {} :: any do
|
|||
local proxies = {} :: { [Data]: userdata? }
|
||||
setmetatable(proxies :: any, { __mode = "v" })
|
||||
|
||||
-- allocate variables for the metamethods as __index and get_proxy cross refrences each other
|
||||
local __index, __newindex
|
||||
|
||||
local function get_data(userdata: userdata): Data
|
||||
local function f(userdata: userdata): ProxyMT
|
||||
return getmetatable(userdata :: any)
|
||||
|
|
@ -97,19 +94,6 @@ local Instance = {} :: any do
|
|||
return f(userdata).data
|
||||
end
|
||||
|
||||
local function get_proxy(data: Data): userdata
|
||||
return proxies[data] or (function()
|
||||
local userdata = newproxy(true)
|
||||
local proxy = getmetatable(userdata)
|
||||
proxy.proxy = userdata
|
||||
proxy.data = data
|
||||
proxy.__index = __index
|
||||
proxy.__newindex = __newindex
|
||||
proxies[data] = userdata
|
||||
return userdata
|
||||
end)()
|
||||
end
|
||||
|
||||
local function is_instance(value: unknown): boolean
|
||||
local mt = getmetatable(value :: any)
|
||||
return mt and mt.data and mt.data.type == "Instance"
|
||||
|
|
@ -117,16 +101,16 @@ local Instance = {} :: any do
|
|||
|
||||
local methods = {}
|
||||
|
||||
__index = function(userdata: userdata, property: string): ()
|
||||
local function __index(userdata: userdata, property: string): ()
|
||||
local data = get_data(userdata)
|
||||
return if methods[property] then methods[property]
|
||||
elseif property == "Name" then data.name
|
||||
elseif property == "Parent" then (data.parent and get_proxy(data.parent))
|
||||
elseif property == "Parent" then data.parent
|
||||
elseif property == "Destroying" then data.destroying
|
||||
else data.properties[property]
|
||||
end
|
||||
|
||||
__newindex = function(userdata: userdata, property: string, value: unknown)
|
||||
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||
local data = get_data(userdata)
|
||||
if property == "Name" then
|
||||
if type(value) ~= "string" then error("name must be a string", 2) end
|
||||
|
|
@ -151,6 +135,19 @@ local Instance = {} :: any do
|
|||
end
|
||||
end
|
||||
|
||||
local function get_proxy(data: Data): userdata
|
||||
return proxies[data] or (function()
|
||||
local userdata = newproxy(true)
|
||||
local proxy = getmetatable(userdata)
|
||||
proxy.proxy = userdata
|
||||
proxy.data = data
|
||||
proxy.__index = __index
|
||||
proxy.__newindex = __newindex
|
||||
proxies[data] = userdata
|
||||
return userdata
|
||||
end)()
|
||||
end
|
||||
|
||||
function Instance.new(class: string): Instance
|
||||
local data = {
|
||||
name = "UNNAMED",
|
||||
|
|
@ -260,6 +257,40 @@ local Vector2 = { __type = "Vector2" } :: any do
|
|||
end
|
||||
end
|
||||
|
||||
local Vector3 = { __type = "Vector3" } :: any do
|
||||
local function new(x, y, z)
|
||||
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
||||
end
|
||||
|
||||
function Vector3.new(x, y, z)
|
||||
return new(x or 0, y or 0, z or 0)
|
||||
end
|
||||
|
||||
function Vector3.__add(a, b)
|
||||
return new(a.X + b.X, a.Y + b.Y, a.Z + b.Z)
|
||||
end
|
||||
|
||||
function Vector3.__sub(a, b)
|
||||
return new(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
|
||||
end
|
||||
|
||||
function Vector3.__mul(a, b)
|
||||
return new(a.X * b, a.Y * b, a.Z * b)
|
||||
end
|
||||
|
||||
function Vector3.__unm(v)
|
||||
return new(-v.X, -v.Y, -v.Z)
|
||||
end
|
||||
|
||||
function Vector3.__eq(a, b)
|
||||
return a.X == b.X and a.Y == b.Y
|
||||
end
|
||||
|
||||
function Vector3.__index(v)
|
||||
return (v.X^2 + v.Y^2 + v.Z^2)^0.5
|
||||
end
|
||||
end
|
||||
|
||||
local UDim2 = { __type = "UDim2" } :: any do
|
||||
function UDim2.new(sx, ox, sy, oy)
|
||||
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
||||
|
|
@ -299,6 +330,7 @@ return {
|
|||
Instance = Instance :: typeof(Instance),
|
||||
Color3 = Color3 :: typeof(Color3),
|
||||
Vector2 = Vector2 :: typeof(Vector2),
|
||||
Vector3 = Vector3 :: typeof(Vector3),
|
||||
UDim2 = UDim2 :: typeof(UDim2),
|
||||
Enum = Enum :: typeof(Enum),
|
||||
typeof = typeof :: typeof(typeof)
|
||||
|
|
|
|||
9
test/relative-string.luau
Normal file
9
test/relative-string.luau
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
local function dir(directory: string)
|
||||
return setmetatable({} :: { [string]: any },
|
||||
{ __index = function(_, path) return directory .. path end })
|
||||
end
|
||||
|
||||
local script = dir "src/"
|
||||
script.Parent = dir "src/"
|
||||
|
||||
return script
|
||||
|
|
@ -1,34 +1,25 @@
|
|||
local vide = require "../../vide"
|
||||
local vide = require "src/init"
|
||||
local testkit = require("test/testkit")
|
||||
|
||||
local function system(): (number) -> number
|
||||
local MAX = 40
|
||||
local MIN = 10
|
||||
local program_time = os.clock()
|
||||
|
||||
local _, input, output = vide.root(function()
|
||||
local input = vide.source(MAX)
|
||||
local output = vide.spring(input, 1, .3)
|
||||
return input, output
|
||||
end)
|
||||
local function step(): number
|
||||
local FPS = 60
|
||||
local DT = 1/FPS
|
||||
|
||||
local T = 10
|
||||
local t = 0
|
||||
return function(dt)
|
||||
t += dt
|
||||
if t >= T then
|
||||
t -= T
|
||||
input(input() == MAX and MIN or MAX)
|
||||
end
|
||||
|
||||
vide.step(dt)
|
||||
|
||||
return output()
|
||||
end
|
||||
repeat until os.clock() - program_time >= DT
|
||||
program_time += DT
|
||||
return DT
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function main()
|
||||
local TERMINAL_HEIGHT = 73 --* REDUCE IF BAR DOES NOT FIT IN TERMINAL
|
||||
local MIN_ALPHA = 0.3
|
||||
local MAX_ALPHA = 0.7
|
||||
|
||||
local function redraw_block(h: number)
|
||||
local OFFSET = 70
|
||||
local MIN = TERMINAL_HEIGHT * MIN_ALPHA
|
||||
local MAX = TERMINAL_HEIGHT * MAX_ALPHA
|
||||
local OFFSET = TERMINAL_HEIGHT - MAX
|
||||
|
||||
local BLOCK = "█"
|
||||
|
||||
|
|
@ -44,35 +35,37 @@ local function redraw_block(h: number)
|
|||
else "▁"
|
||||
end
|
||||
|
||||
local h_f = math.floor(h)
|
||||
local reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||
local offset = string.rep("\n", OFFSET - h_f)
|
||||
local bar = remainder_to_block(h - h_f) .. "\n" .. string.rep(BLOCK .. "\n", h_f)
|
||||
--print(reset .. offset .. bar .. "\n" .. string.format("%.1f", h))
|
||||
print(reset .. offset .. bar .. "\n" .. h)
|
||||
local source = vide.source
|
||||
local spring = vide.spring
|
||||
local effect = vide.effect
|
||||
|
||||
local value = source(MAX)
|
||||
local sprung = spring(value, 1, 0.3)
|
||||
|
||||
effect(function()
|
||||
local v = sprung()
|
||||
local fv = math.floor(v)
|
||||
local reset = "\27[H\27[2J" -- ANSI clear terminal
|
||||
local offset = string.rep("\n", MAX - fv + OFFSET)
|
||||
local bar = testkit.color.gray(remainder_to_block(v - fv) .. "\n" .. string.rep(BLOCK .. "\n", fv))
|
||||
print(reset .. offset .. bar .. "\n" .. v)
|
||||
end)
|
||||
|
||||
local T = 3
|
||||
local elapsed = T/1.2
|
||||
repeat local dt = step()
|
||||
vide.step(dt)
|
||||
|
||||
elapsed += dt
|
||||
while elapsed >= T do
|
||||
elapsed -= T
|
||||
value(value() == MAX and MIN or MAX)
|
||||
end
|
||||
|
||||
until false
|
||||
end
|
||||
|
||||
local program_time = os.clock()
|
||||
|
||||
local function step(): number
|
||||
local FPS = 30
|
||||
local DT = 1/FPS
|
||||
|
||||
repeat until os.clock() - program_time >= DT
|
||||
program_time += DT
|
||||
return DT
|
||||
end
|
||||
|
||||
local function loop()
|
||||
local callback = system()
|
||||
|
||||
while true do
|
||||
local dt = step()
|
||||
local x = callback(dt)
|
||||
redraw_block(x)
|
||||
end
|
||||
end
|
||||
|
||||
loop()
|
||||
vide.root(main)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
916
test/tests.luau
916
test/tests.luau
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
name = "centau/vide"
|
||||
description = "A reactive Luau library for creating UI. "
|
||||
license = "MIT"
|
||||
version = "0.4.1"
|
||||
version = "0.3.1"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
include = ["default.project.json", "LICENSE", "src"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue