mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge branch 'main' of https://github.com/centau/vide
This commit is contained in:
commit
63077355eb
66 changed files with 1741 additions and 1671 deletions
2
.github/workflows/unit-test.yml
vendored
2
.github/workflows/unit-test.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
uses: robinraju/release-downloader@v1.6
|
uses: robinraju/release-downloader@v1.6
|
||||||
with:
|
with:
|
||||||
repository: Roblox/luau
|
repository: Roblox/luau
|
||||||
tag: "0.620"
|
tag: "0.651"
|
||||||
fileName: luau-ubuntu.zip
|
fileName: luau-ubuntu.zip
|
||||||
out-file-path: bin
|
out-file-path: bin
|
||||||
|
|
||||||
|
|
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Aggregate construction when setting properties with `create()`.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
## [0.3.1] - 2024-10-09
|
## [0.3.1] - 2024-10-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//import { defineConfig } from "vitepress"
|
//import { defineConfig } from "vitepress"
|
||||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||||
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"
|
|
||||||
|
|
||||||
// https://vitepress.dev/reference/site-config
|
// https://vitepress.dev/reference/site-config
|
||||||
export default withMermaid({
|
export default withMermaid({
|
||||||
|
|
@ -10,12 +9,6 @@ export default withMermaid({
|
||||||
base: "/vide/",
|
base: "/vide/",
|
||||||
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
|
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
|
||||||
|
|
||||||
markdown: {
|
|
||||||
config(md) {
|
|
||||||
md.use(tabsMarkdownPlugin)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: "/logo.svg",
|
logo: "/logo.svg",
|
||||||
|
|
||||||
|
|
@ -25,7 +18,6 @@ export default withMermaid({
|
||||||
|
|
||||||
footer: {
|
footer: {
|
||||||
message: 'Released under the MIT License.',
|
message: 'Released under the MIT License.',
|
||||||
copyright: 'Copyright © 2023-present centau'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://vitepress.dev/reference/default-theme-config
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
|
|
@ -42,7 +34,7 @@ export default withMermaid({
|
||||||
items: [
|
items: [
|
||||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||||
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
|
||||||
{ text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
|
{ text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
|
||||||
{ text: "Element Creation", link: "/api/creation" },
|
{ text: "Element Creation", link: "/api/creation" },
|
||||||
{ text: "Animation", link: "/api/animation" },
|
{ text: "Animation", link: "/api/animation" },
|
||||||
{ text: "Strict Mode", link: "/api/strict-mode" },
|
{ text: "Strict Mode", link: "/api/strict-mode" },
|
||||||
|
|
@ -60,20 +52,25 @@ export default withMermaid({
|
||||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
|
||||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
||||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||||
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
{ text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
|
||||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
||||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||||
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Advanced Reactivity",
|
text: "Advanced",
|
||||||
|
items: [
|
||||||
|
{ text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Design Patterns",
|
||||||
items: [
|
items: [
|
||||||
{ text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
25
docs/.vitepress/theme/index.css
Normal file
25
docs/.vitepress/theme/index.css
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
.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,14 +1,8 @@
|
||||||
// .vitepress/theme/index.js
|
// .vitepress/theme/index.js
|
||||||
import DefaultTheme from 'vitepress/theme'
|
import DefaultTheme from 'vitepress/theme'
|
||||||
import layout from './layout.vue'
|
|
||||||
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
|
|
||||||
import './vars.css'
|
import './vars.css'
|
||||||
import './home.css'
|
import './index.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: DefaultTheme,
|
extends: DefaultTheme,
|
||||||
enhanceApp({ app }) {
|
|
||||||
enhanceAppWithTabs(app)
|
|
||||||
},
|
|
||||||
Layout: layout
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,39 +1,3 @@
|
||||||
/* Banner Stuff */
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
background-color: var(--vp-button-brand-bg);
|
|
||||||
color: var(--vp-button-brand-text);
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0px;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.advertising {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
|
||||||
.advertising {
|
|
||||||
width: calc(100% - 8rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 960px) {
|
|
||||||
.banner {
|
|
||||||
position: fixed;
|
|
||||||
margin-top: var(--vp-nav-height);
|
|
||||||
bottom: auto;
|
|
||||||
}
|
|
||||||
#VPSidebarNav {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
.VPLocalNav {
|
|
||||||
top: calc(var(--vp-nav-height) + 1.5rem) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Colors */
|
/* Colors */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -67,6 +31,11 @@
|
||||||
--vp-c-divider: #dfe2e6;
|
--vp-c-divider: #dfe2e6;
|
||||||
--vp-c-gutter: #dfe2e6;
|
--vp-c-gutter: #dfe2e6;
|
||||||
--vp-plugin-tabs-tab-bg: var(--vp-c-bg);
|
--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 {
|
.dark {
|
||||||
|
|
@ -81,96 +50,3 @@
|
||||||
--vp-c-gutter: #181d27;
|
--vp-c-gutter: #181d27;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fonts */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold-Italic.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold-Italic.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold-Italic.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold-Italic.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 800;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 800;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Italic.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Italic.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium-Italic.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium-Italic.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "JetBrains Mono";
|
|
||||||
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2")
|
|
||||||
format("woff2"),
|
|
||||||
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff")
|
|
||||||
format("woff");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--vp-font-family-mono: "JetBrains Mono";
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Animation API
|
# Animation
|
||||||
|
|
||||||
## spring()
|
## spring() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||||
|
|
||||||
Returns a new source with a value always moving torwards the input source value.
|
Returns a new source with a value always moving torwards the input source value.
|
||||||
|
|
||||||
|
|
@ -11,15 +11,20 @@ Returns a new source with a value always moving torwards the input source value.
|
||||||
source: () -> T & Animatable,
|
source: () -> T & Animatable,
|
||||||
period: number = 1,
|
period: number = 1,
|
||||||
damping_ratio: number = 1
|
damping_ratio: number = 1
|
||||||
): () -> T
|
): (() -> T, Setter<T>)
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
||||||
|
|
||||||
|
type Setter<T> = ({
|
||||||
|
position: T?,
|
||||||
|
velocity: T?,
|
||||||
|
impulse: T?
|
||||||
|
}) -> ()
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
An effect is created to update the new source every frame based on the input
|
Creates a reactive scope internally to detect source updates.
|
||||||
source value.
|
|
||||||
|
|
||||||
The movement is physically simulated according to a
|
The movement is physically simulated according to a
|
||||||
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
||||||
|
|
@ -39,3 +44,7 @@ Returns a new source with a value always moving torwards the input source value.
|
||||||
You can change when the solver runs by calling `vide.step(dt)`, which will
|
You can change when the solver runs by calling `vide.step(dt)`, which will
|
||||||
advance the simulation time by `dt` seconds and automatically stop the
|
advance the simulation time by `dt` seconds and automatically stop the
|
||||||
solver running in heartbeat.
|
solver running in heartbeat.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Large periods or damping ratios can break the spring.
|
||||||
|
:::
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,4 @@
|
||||||
# Element Creation API
|
# Element Creation
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
## mount()
|
|
||||||
|
|
||||||
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**
|
|
||||||
|
|
||||||
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**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local function App()
|
|
||||||
return create "ScreenGui" {
|
|
||||||
create "TextLabel" { Text = "Vide" }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
mount(App, game.StarterGui)
|
|
||||||
```
|
|
||||||
|
|
||||||
## create()
|
## create()
|
||||||
|
|
||||||
|
|
@ -45,7 +10,7 @@ Creates a new UI element, applying any given properties.
|
||||||
function create(class: string): (Properties) -> Instance
|
function create(class: string): (Properties) -> Instance
|
||||||
function create(instance: Instance): (Properties) -> Instance
|
function create(instance: Instance): (Properties) -> Instance
|
||||||
|
|
||||||
type Properties = Map<string|number, any>
|
type Properties = Map<string|number, unknown>
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
@ -77,42 +42,22 @@ Creates a new UI element, applying any given properties.
|
||||||
Basic element creation.
|
Basic element creation.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local frame = create "Frame" {
|
local frame = create "TextButton" {
|
||||||
Name = "NewFrame",
|
Name = "Button",
|
||||||
Position = UDim2.fromScale(1, 0)
|
Size = UDim2.fromOffset(200, 160),
|
||||||
|
|
||||||
|
Activated = function()
|
||||||
|
print "clicked"
|
||||||
|
end,
|
||||||
|
|
||||||
|
create "UICorner" {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A component using property nesting.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
type Layout = {
|
|
||||||
Layout = {
|
|
||||||
Position: UDim2?,
|
|
||||||
Size: UDim2?,
|
|
||||||
AnchorPoint: Vector2?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Children = {
|
|
||||||
Children = Array<Instance>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Background(props: Layout & Children & {
|
|
||||||
Color: Color3
|
|
||||||
})
|
|
||||||
return create "Frame" {
|
|
||||||
BackgroundColor3 = props.Color,
|
|
||||||
props.Layout,
|
|
||||||
props.Children
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## action()
|
## action()
|
||||||
|
|
||||||
Creates a callback that can be passed to `create()` to invoke custom actions on
|
Creates a special object that can be passed to `create()` to invoke custom
|
||||||
instances.
|
actions on instances.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
|
|
@ -122,27 +67,27 @@ instances.
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
When passed to `create()`, the given callback is called with the instance
|
When passed to `create()`, the function is called with the instance being
|
||||||
being created as the only argument. Actions take precedence over property
|
created as the only argument. Actions take precedence over property and
|
||||||
and child assignments.
|
child assignments.
|
||||||
|
|
||||||
A priority can be optionally specified to ensure certain actions run after
|
A priority can be optionally specified to ensure certain actions run after
|
||||||
other actions. Higher priority numbers are ran after lower priority numbers.
|
other actions. Lower priority values are ran first.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
An action to listen to changed properties:
|
An action to listen to changed properties:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local function changed(property: string, callback: (new) -> ())
|
local function changed(property: string, fn: (new) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
local con - instance:GetPropertyChangedSignal(property):Connect(function()
|
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||||
callback(instance[property])
|
fn(instance[property])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- disconnect on reactive scope destruction to allow gc of instance
|
-- disconnect on scope destruction to allow gc of instance
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
con:Disconnect()
|
cn:Disconnect()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -150,7 +95,7 @@ instances.
|
||||||
local output = source ""
|
local output = source ""
|
||||||
|
|
||||||
create "TextBox" {
|
create "TextBox" {
|
||||||
-- will update the `output` source anytime the text property is changed
|
-- will update the output source anytime the text property is changed
|
||||||
changed("Text", output)
|
changed("Text", output)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -162,15 +107,46 @@ A wrapper for `action()` to listen for property changes.
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
function changed(property: string, callback: (...unknown) -> ()): Action
|
function changed(property: string, fn: (unknown) -> ()): Action
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Will run the given callback any time the property is changed, as well as
|
Will run the given function immediately and whenever the property updates.
|
||||||
when the action is initially run.
|
|
||||||
|
|
||||||
The changed connection is disconnected when the scope the action is ran in
|
The function is called with the updated property value.
|
||||||
is destroyed.
|
|
||||||
|
|
||||||
Runs with an action priority of 1.
|
Runs with an action priority of 1.
|
||||||
|
|
||||||
|
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||||
|
|
||||||
|
Runs a function in a new stable scope and optionally applies its result to a
|
||||||
|
target instance.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```luau
|
||||||
|
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
This is a utility for `root()` when parenting a component to an existing
|
||||||
|
instance.
|
||||||
|
|
||||||
|
The result of the function is applied to a target in the same way
|
||||||
|
properties are using `create()`.
|
||||||
|
|
||||||
|
Returns a function that when called will destroy the stable scope.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local function App()
|
||||||
|
return create "ScreenGui" {
|
||||||
|
create "TextLabel" { Text = "Vide" }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local destroy = mount(App, game.StarterGui)
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,40 @@
|
||||||
# Reactivity API: Core
|
# Reactivity: Core
|
||||||
|
|
||||||
<br/>
|
## 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.
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
||||||
for this.
|
for this.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## root()
|
## root() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||||
|
|
||||||
Creates and runs a function in a new stable scope.
|
Runs a function in a new stable scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
function root<T...>(fn: (() -> ()) -> T...): (() -> (), T...)
|
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
||||||
|
|
||||||
|
type Destructor = () -> ()
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Returns a function to destroy the root scope. Also passes this function as
|
Returns a destructor and any values returned by the callback.
|
||||||
the first argument into its callback.
|
|
||||||
|
|
||||||
All values returned by the callback are also returned following the destructor.
|
|
||||||
|
|
||||||
## source()
|
## source()
|
||||||
|
|
||||||
Creates a new source with the given value.
|
Creates a new source.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
|
|
@ -40,71 +48,64 @@ Creates a new source with the given value.
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Calling the returned source with no argument will return its stored value,
|
Call the returned source with no argument to read its value.
|
||||||
calling with an argument will set a new value.
|
Call the returned source with an argument to set its value.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
print(count())-- 0
|
||||||
count() -- 0
|
count(count() + 1)
|
||||||
|
print(count()) -- 1
|
||||||
count(count() + 1) -- 1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## effect()
|
## effect() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||||
|
|
||||||
Runs a side-effect in a new reactive scope on source update.
|
Runs a function in a new reactive scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
function effect(callback: () -> ())
|
function effect(fn: () -> ())
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Any time a source referenced in the callback is updated, the callback will
|
The function is ran once immediately.
|
||||||
be reran.
|
|
||||||
|
|
||||||
The callback is ran once immediately.
|
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local num = source(1)
|
local count = source(1)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
print(num())
|
print(count())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- prints 1
|
-- prints 1
|
||||||
|
|
||||||
num(num() + 1)
|
count(2)
|
||||||
|
|
||||||
-- prints 2
|
-- prints 2
|
||||||
```
|
```
|
||||||
|
|
||||||
## derive()
|
## derive() <Badge type="tip" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">REACTIVE</a></Badge>
|
||||||
|
|
||||||
Derives a new source in a new reactive scope from existing sources.
|
Runs a function in a new reactive scope to compute a value for new source.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
function derive<T>(source: () -> T): () -> T
|
function derive<T>(fn: () -> T): () -> T
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
The derived source will have its value recalculated when any source source
|
Anytime the reactive scope reruns, the output source value is set to what is
|
||||||
it derives from is updated.
|
returned.
|
||||||
|
|
||||||
Anytime its value is recalculated it is also cached, subsequent calls will
|
The function is ran once immediately.
|
||||||
retun this cached value until it recalculates again.
|
|
||||||
|
|
||||||
The callback is ran once immediately.
|
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
|
|
@ -112,11 +113,43 @@ Derives a new source in a new reactive scope from existing sources.
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
local text = derive(function() return `count: {count()}` end)
|
local text = derive(function() return `count: {count()}` end)
|
||||||
|
|
||||||
text() -- "count: 0"
|
print(text()) -- "count: 0"
|
||||||
|
|
||||||
count(1)
|
count(1)
|
||||||
|
|
||||||
text() -- "count: 1"
|
print(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"
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
|
||||||
212
docs/api/reactivity-dynamic.md
Normal file
212
docs/api/reactivity-dynamic.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Reactivity: Dynamic Scoping
|
||||||
|
|
||||||
|
Dynamic scoping is the act of creating and destroying new scopes in response to
|
||||||
|
source updates. Vide provides functions for some common use-cases to do this.
|
||||||
|
|
||||||
|
## 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: () -> T): () -> T?
|
||||||
|
function show<T, U>(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U
|
||||||
|
```
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## 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, () -> V>): () -> V?
|
||||||
|
```
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
- **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>,
|
||||||
|
transform: (value: () -> VI, index: KI) -> VO
|
||||||
|
): 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 `transform` 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 `transform` function is called with:
|
||||||
|
|
||||||
|
1. A *source containing the index's value*.
|
||||||
|
2. The *index itself*.
|
||||||
|
|
||||||
|
Anytime an existing index's value changes, the `transform` 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.
|
||||||
|
|
||||||
|
- **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>,
|
||||||
|
transform: (value: VI, index: () -> KI) -> VO
|
||||||
|
): 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 `transform` 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 `transform` function is called with:
|
||||||
|
|
||||||
|
1. The *value itself*.
|
||||||
|
2. A *source containing the value's index*.
|
||||||
|
|
||||||
|
Anytime an existing value's index changes, the `transform` 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.
|
||||||
|
|
||||||
|
::: 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.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
# Reactivity API: Control Flow
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
## show()
|
|
||||||
|
|
||||||
Shows one of two components depending on an input source.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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**
|
|
||||||
|
|
||||||
```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()
|
|
||||||
|
|
||||||
Maps each index in a table source to an object.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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.
|
|
||||||
|
|
||||||
```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()
|
|
||||||
|
|
||||||
Maps each value in a table source to an object.
|
|
||||||
|
|
||||||
- **Type**
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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.
|
|
||||||
|
|
||||||
```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 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,16 +1,21 @@
|
||||||
# Reactivity API: Utility
|
# Reactivity: Utility
|
||||||
|
|
||||||
## cleanup()
|
## cleanup()
|
||||||
|
|
||||||
Runs a callback anytime a scope is reran or destroyed.
|
Queues a callback to run when a scope is reran or destroyed.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
|
<<<<<<< HEAD
|
||||||
function cleanup(callback: () -> ())
|
function cleanup(callback: () -> ())
|
||||||
function cleanup(obj: Destroyable)
|
function cleanup(obj: Destroyable)
|
||||||
function cleanup(obj: Disconnectable)
|
function cleanup(obj: Disconnectable)
|
||||||
|
=======
|
||||||
|
function cleanup(v: Function | Disconnectable | Destroyable | thread)
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||||
|
|
||||||
|
type Function = () -> ()
|
||||||
type Destroyable = { destroy: () -> () }
|
type Destroyable = { destroy: () -> () }
|
||||||
type Disconnectable = { disconnect: () -> () }
|
type Disconnectable = { disconnect: () -> () }
|
||||||
```
|
```
|
||||||
|
|
@ -18,20 +23,31 @@ Runs a callback anytime a scope is reran or destroyed.
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
|
<<<<<<< HEAD
|
||||||
local data = source(1)
|
local data = source(1)
|
||||||
|
=======
|
||||||
|
local count = source(0)
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||||
|
|
||||||
effect(function()
|
local destroy = root(function()
|
||||||
local label = create "TextLabel" { Text = data() }
|
effect(function()
|
||||||
|
count()
|
||||||
|
|
||||||
cleanup(function()
|
cleanup(function()
|
||||||
label:Destroy()
|
print "cleaned"
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end
|
||||||
|
|
||||||
|
-- nothing printed yet
|
||||||
|
count(1) -- prints "cleaned"
|
||||||
|
count(2) -- prints "cleaned"
|
||||||
|
destroy() -- prints "cleaned"
|
||||||
```
|
```
|
||||||
|
|
||||||
## untrack()
|
## untrack() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||||
|
|
||||||
Runs a given function in a new stable scope.
|
Runs a function in a new stable scope.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
|
|
@ -55,16 +71,15 @@ Runs a given function in a new stable scope.
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(sum()) -- 0
|
print(sum()) -- 0
|
||||||
b(1)
|
b(1) -- untracked so reactive scope created by derive() does not rerun
|
||||||
print(sum()) -- 0
|
print(sum()) -- 0
|
||||||
a(1)
|
a(1) -- reactive scope created by derive() reruns
|
||||||
print(sum()) -- 2
|
print(sum()) -- 2
|
||||||
```
|
```
|
||||||
|
|
||||||
## read()
|
## read()
|
||||||
|
|
||||||
Utility used to read a value that is either a primitive or a source. Sources
|
Utility used to read a value that is either a primitive or a source.
|
||||||
read can still be tracked inside a reactive scope.
|
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
|
|
@ -74,8 +89,8 @@ read can still be tracked inside a reactive scope.
|
||||||
|
|
||||||
## batch()
|
## batch()
|
||||||
|
|
||||||
Runs a given function where any source updates made within the function do not
|
Runs a function where any source updates made within the function do not
|
||||||
trigger effects until after the function finishes running.
|
trigger effects until after the function ends.
|
||||||
|
|
||||||
- **Type**
|
- **Type**
|
||||||
|
|
||||||
|
|
@ -86,11 +101,29 @@ trigger effects until after the function finishes running.
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Improves performance when an effect depends on multiple sources, and those
|
Improves performance when an effect depends on multiple sources, and those
|
||||||
sources need to be updated. Updating those sources inside a batch call will
|
sources need to be updated.
|
||||||
only cause the effect to run once after the batch call ends instead of after
|
|
||||||
each time a source is updated.
|
|
||||||
|
|
||||||
## context()
|
- **Example**
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local a = source(0)
|
||||||
|
local b = source(0)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
print(a() + b())
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- prints "0"
|
||||||
|
|
||||||
|
batch(function()
|
||||||
|
a(1) -- no print
|
||||||
|
b(2) -- no print
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- prints "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## context() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||||
|
|
||||||
Creates a new context.
|
Creates a new context.
|
||||||
|
|
||||||
|
|
@ -101,15 +134,17 @@ Creates a new context.
|
||||||
|
|
||||||
type Context<T> =
|
type Context<T> =
|
||||||
() -> T -- get
|
() -> T -- get
|
||||||
& (T, () -> ()) -> () -- set
|
& <U>(T, () -> U) -> U -- set
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Details**
|
- **Details**
|
||||||
|
|
||||||
Calling `context()` returns a new context function.
|
Calling `context()` returns a new context function.
|
||||||
Call this function with no arguments to get the context value.
|
Call this function with no arguments to get the context value.
|
||||||
Call this function with a value and a callback to set a new context with the
|
Call this function with a value and a function to create a new context with
|
||||||
given value.
|
the given value.
|
||||||
|
|
||||||
|
The new context is run under a stable scope.
|
||||||
|
|
||||||
- **Example**
|
- **Example**
|
||||||
|
|
||||||
|
|
@ -131,4 +166,3 @@ Creates a new context.
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,24 @@ and identifying improper usage.
|
||||||
|
|
||||||
Currently, strict mode will:
|
Currently, strict mode will:
|
||||||
|
|
||||||
1. Run derived sources twice a source updates.
|
1. Run reactive scopes twice when a source updates.
|
||||||
2. Run effects twice when a source updates.
|
2. Throw an error if yields occur where they are not allowed.
|
||||||
3. Throw an error if yields occur where they are not allowed.
|
3. Checks for `indexes()` and `values()` outputting primitive values.
|
||||||
4. Checks for `indexes()` and `values()` returning primitive values.
|
4. Checks for `values()` input having duplicate values.
|
||||||
5. Checks for `values()` input having duplicate values.
|
5. Checks for duplicate nested properties at same depth.
|
||||||
6. Checks for duplicate nested properties at same depth.
|
6. Checks for destruction of an active scope.
|
||||||
7. Better error reporting and stack traces + creation traces of property bindings.
|
7. Better error reporting and stack traces.
|
||||||
|
|
||||||
By rerunning derived sources and effects twice each time they update, it helps
|
By rerunning reactive scopes twice each time they update, it helps ensure that
|
||||||
ensure that derived source computations are pure, and that any
|
computations are pure, and that any cleanup is done correctly.
|
||||||
cleanups made in derived sources or effects are done correctly.
|
|
||||||
|
|
||||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||||
which strict mode will catch.
|
which strict mode will catch.
|
||||||
|
|
||||||
As well as additional safety checks, Vide will dedicate extra resources to
|
As well as additional safety checks, Vide will dedicate extra resources to
|
||||||
recording and better emitting stack traces where errors occur, particularly
|
recording and better emitting stack traces where errors occur, particularly
|
||||||
when binding properties to sources.
|
when implicit effects are created for instance property updating.
|
||||||
|
|
||||||
It is recommended to develop UI with strict mode and to disable it when pushing to
|
It is recommended to develop UI with strict mode and to disable it when pushing to
|
||||||
production. In Roblox, production code compiles at O2 by default, so you don't
|
production. In Roblox, production code compiles at O2 by default, so you do not
|
||||||
need to worry about disabling strict mode unless you have manually enabled it.
|
need to worry about disabling strict mode unless you have manually enabled it.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ next:
|
||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: "Vide"
|
name: "Vide"
|
||||||
tagline: A reactive UI and state library for Luau.
|
tagline: A reactive UI library for Luau.
|
||||||
image:
|
image:
|
||||||
src: /logo.svg
|
src: /logo.svg
|
||||||
actions:
|
actions:
|
||||||
|
|
@ -22,11 +22,4 @@ hero:
|
||||||
text: API Reference
|
text: API Reference
|
||||||
link: /api/reactivity-core
|
link: /api/reactivity-core
|
||||||
|
|
||||||
features:
|
|
||||||
- title: Reactive State Management
|
|
||||||
details: Built upon Solid and makes building reactive applications simple.
|
|
||||||
- title: Declarative and simple
|
|
||||||
details: Syntax designed to be minimal while also being easy to understand.
|
|
||||||
- title: Fully Luau typecheckable
|
|
||||||
details: Luau's typechecker catches type errors before you even begin testing.
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
"docs:preview": "vitepress preview"
|
"docs:preview": "vitepress preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitepress": "1.4.1",
|
"vitepress": "^1.4.5",
|
||||||
"vitepress-plugin-mermaid": "2.0.17",
|
"vitepress-plugin-mermaid": "2.0.17"
|
||||||
"vitepress-plugin-tabs": "^0.5.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
148
docs/tut/advanced/dynamic-scopes.md
Normal file
148
docs/tut/advanced/dynamic-scopes.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Dynamic Scoping
|
||||||
|
|
||||||
|
Dynamic scoping is the act of creating and destroying new scopes 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.
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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()`:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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()`:
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# show()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# switch()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# indexes()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# values()
|
|
||||||
|
|
@ -6,10 +6,16 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||||
|
|
||||||
## Why Vide?
|
## Why Vide?
|
||||||
|
|
||||||
Vide provides a reactive and declarative API to simplify managing UI.
|
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.
|
||||||
|
|
||||||
Some of the main focuses behind Vide's design choices:
|
Some of Vide's main design choices:
|
||||||
|
|
||||||
- Minimal syntax
|
- Syntax minimal.
|
||||||
- Complete typechecking
|
- Data oriented.
|
||||||
- Independence from instances
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ destroyed, or when a stable scope is destroyed.
|
||||||
local root = vide.root
|
local root = vide.root
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
|
|
||||||
local destroy = root(function()
|
local destroy = root(function()
|
||||||
effect(function()
|
effect(function()
|
||||||
local x = count()
|
local x = count()
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
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.
|
|
||||||
|
|
||||||
```luau
|
|
||||||
local src = source { 1, 2 }
|
|
||||||
local data = src()
|
|
||||||
table.insert(data, 3) -- no effects will run
|
|
||||||
src(data) -- effects will run
|
|
||||||
```
|
|
||||||
158
docs/tut/crash-course/11-dynamic-scopes.md
Normal file
158
docs/tut/crash-course/11-dynamic-scopes.md
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# 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 components 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
|
||||||
|
```
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
# Actions
|
# Actions
|
||||||
|
|
||||||
Actions in Vide are special callbacks that you can pass along with properties,
|
Actions are special callbacks that you can pass along with properties,
|
||||||
to run some code on an instance receiving them.
|
to run some code on an instance receiving them.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
```
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
```luau
|
```luau
|
||||||
|
=======
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||||
create "TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "test",
|
Text = "test",
|
||||||
|
|
||||||
|
|
@ -24,12 +26,13 @@ action used to listen for property changes:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local cleanup = vide.cleanup
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
local function changed(prop: string, callback: (new) -> ())
|
local function changed(property: string, callback: (new) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
|
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||||
callback(instance[property])
|
callback(instance[property])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -49,7 +52,7 @@ effect(function()
|
||||||
print(output())
|
print(output())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
instance.Text = "foo" -- "foo" will be printed from the effect
|
instance.Text = "foo" -- "foo" will be printed by the effect
|
||||||
```
|
```
|
||||||
|
|
||||||
The source `output` will be updated with the new property value any time it is
|
The source `output` will be updated with the new property value any time it is
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ local count = source(0)
|
||||||
|
|
||||||
local ran = 0
|
local ran = 0
|
||||||
effect(function()
|
effect(function()
|
||||||
|
count()
|
||||||
ran += 1
|
ran += 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Created by:
|
||||||
|
|
||||||
- `root()`
|
- `root()`
|
||||||
- `untrack()`
|
- `untrack()`
|
||||||
- `switch()`
|
- `show()`
|
||||||
- `indexes()`
|
- `indexes()`
|
||||||
|
|
||||||
Stable scopes do not track sources and never rerun.
|
Stable scopes do not track sources and never rerun.
|
||||||
|
|
@ -47,23 +47,14 @@ Created by:
|
||||||
Reactive scopes do track sources and will rerun when those sources update.
|
Reactive scopes do track sources and will rerun when those sources update.
|
||||||
|
|
||||||
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
Reactive scopes cannot be created within a reactive scope, but stable scopes
|
||||||
can.
|
can be created within a reactive scope.
|
||||||
|
|
||||||
## Scope Owners
|
## Scope Cleanup
|
||||||
|
|
||||||
A scope created within another scope is *owned* by the other scope, with the
|
When a scope is rerun or destroyed, all scopes created within it are
|
||||||
exception of the scope created by `root()`.
|
automatically destroyed.
|
||||||
|
|
||||||
When a scope is rerun or destroyed, all scopes owned by it are automatically
|
Any functions queued by `cleanup()` are also ran.
|
||||||
destroyed.
|
|
||||||
|
|
||||||
`root()` creates a stable scope with no owner, instead it is destroyed manually.
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
|
|
||||||
|
|
||||||
Queue a function to run using `cleanup()`.
|
|
||||||
|
|
||||||
## Reactive Graph
|
## Reactive Graph
|
||||||
|
|
||||||
|
|
@ -78,7 +69,7 @@ local count = source(0)
|
||||||
|
|
||||||
root(function()
|
root(function()
|
||||||
local text = derive(function()
|
local text = derive(function()
|
||||||
return "count: " .. text()
|
return "count: " .. count()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
|
|
@ -93,12 +84,12 @@ end)
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#1B1B1F",
|
"primaryColor": "#111720",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#111720",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#0d131b",
|
||||||
"tertiaryBorderColor": "#1C1C1F"
|
"tertiaryBorderColor": "#202530"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -118,6 +109,5 @@ Notes:
|
||||||
- An update to `count` will cause `text` to rerun, which
|
- An update to `count` will cause `text` to rerun, which
|
||||||
then causes `effect` to rerun.
|
then causes `effect` to rerun.
|
||||||
- When the root scope is destroyed, `text` and
|
- When the root scope is destroyed, `text` and
|
||||||
`effect` will be destroyed alongside it, since they are
|
`effect` will be destroyed alongside it, since they were created within it.
|
||||||
owned by it. `count` will be untouched and future updates
|
`count` will be untouched and future updates to `count` will have no effect.
|
||||||
to `count` will have no effect.
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Effects
|
# Effects
|
||||||
|
|
||||||
Effects are functions that are ran in response to source updates. They are
|
Effects are functions that are ran in response to source updates.
|
||||||
A source and effect is analogous to a signal and connection.
|
A source and effect is analogous to a signal and connection.
|
||||||
|
|
||||||
Effects are created using `effect()`.
|
Effects are created using `effect()`.
|
||||||
|
|
@ -23,7 +23,10 @@ count(1)
|
||||||
Any source read inside an effect is tracked and will rerun the effect when
|
Any source read inside an effect is tracked and will rerun the effect when
|
||||||
that source is updated.
|
that source is updated.
|
||||||
|
|
||||||
Derived sources are also tracked, it doesn't matter how deeply nested
|
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
|
||||||
inside a function a source is.
|
inside a function a source is.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
|
|
@ -47,3 +50,22 @@ count(2)
|
||||||
|
|
||||||
If a source is updated with the same value it already had, it will not rerun
|
If a source is updated with the same value it already had, it will not rerun
|
||||||
effects depending on it.
|
effects depending on it.
|
||||||
|
|
||||||
|
You can also read from a source within an effect without the effect tracking it.
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
local untrack = vide.untrack
|
||||||
|
|
||||||
|
local a = source(0)
|
||||||
|
local b = source(0)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
print(`a: {a()} b: {untrack(b)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a(1) -- prints "a: 1 b: 0"
|
||||||
|
b(1) -- prints nothing
|
||||||
|
a(2) -- prints "a: 2 b: 1"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -7,38 +7,39 @@ But the disconnecting of many signals and connections is tedious and verbose.
|
||||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||||
API, given that you follow a few rules.
|
API, given that you follow a few rules.
|
||||||
|
|
||||||
Scopes come in two flavors; stable and reactive.
|
Thre are two types of scopes: stable and reactive.
|
||||||
|
|
||||||
- All scopes must be created within another scope with the exception of `root()`
|
- A scope must be created within another scope.
|
||||||
- Stable scopes never rerun
|
- Stable scopes never rerun.
|
||||||
- Reactive scopes can rerun
|
- Reactive scopes can rerun.
|
||||||
- A reactive scope cannot be created within another reactive scope
|
- A reactive scope cannot be created within another reactive scope, only within
|
||||||
|
a stable scope.
|
||||||
|
|
||||||
|
An exception to the first rule is `root()`, which creates the initial scope that
|
||||||
|
you destroy manually with a destructor function it returns.
|
||||||
|
|
||||||
`effect()` creates a reactive scope.
|
|
||||||
`root()` creates a stable scope.
|
`root()` creates a stable scope.
|
||||||
|
`effect()` creates a reactive scope.
|
||||||
|
|
||||||
Whenever a scope is destroyed, any scope created within that scope is also
|
Whenever a scope is destroyed, any scope created within that scope is also
|
||||||
destroyed, and so on. This is why all scopes must be created within another
|
destroyed, and so on.
|
||||||
scope, except `root()` which is used to create the initial scope that you can
|
|
||||||
manually destroy.
|
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local root = vide.root
|
local root = vide.root
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
||||||
local function setup()
|
local count = source(0)
|
||||||
local count = source(0)
|
|
||||||
|
|
||||||
|
local function setup()
|
||||||
effect(function()
|
effect(function()
|
||||||
print(count())
|
print(count())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
|
setup() -- error, effect() tried to create a reactive scope with no stable scope
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
local count = root(setup) -- ok since effect() was called within a stable scope
|
local count = root(setup) -- ok since effect() was called within a stable scope
|
||||||
count(1) -- prints "1"
|
count(1) -- prints "1"
|
||||||
```
|
```
|
||||||
|
|
@ -57,12 +58,16 @@ local function setup()
|
||||||
end
|
end
|
||||||
|
|
||||||
local destroy, count = root(setup)
|
local destroy, count = root(setup)
|
||||||
|
=======
|
||||||
|
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||||
|
|
||||||
count(1) -- prints "1"
|
count(1) -- prints "1"
|
||||||
|
count(2) -- prints "2"
|
||||||
|
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
count(2) -- effect is destroyed; no longer prints
|
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
|
||||||
```
|
```
|
||||||
|
|
||||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||||
|
|
@ -73,12 +78,12 @@ The reactive graph for the above example looks like so:
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#1B1B1F",
|
"primaryColor": "#111720",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#111720",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#0d131b",
|
||||||
"tertiaryBorderColor": "#161618"
|
"tertiaryBorderColor": "#0d131b"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -90,7 +95,7 @@ subgraph root
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
When the stable `root()` is destroyed, the reactive `effect()`
|
When the stable `root()` scope is destroyed, the reactive `effect()`
|
||||||
scope will also be destroyed since it was created within it.
|
scope will also be destroyed since it was created within it.
|
||||||
|
|
||||||
This is important because you may have an effect that updates the property of a
|
This is important because you may have an effect that updates the property of a
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
# Stateful Components
|
# Reactive Components
|
||||||
|
|
||||||
Stateful components in Vide are created using sources and effects - sources to
|
Reactive components in Vide are created using sources and effects - sources to
|
||||||
store the data, and effects to display the data.
|
store the data, and effects to display the data.
|
||||||
|
|
||||||
|
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
|
||||||
## Internal State
|
## Internal State
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b:docs/tut/crash-course/7-reactive-component.md
|
||||||
```luau
|
```luau
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
@ -33,19 +36,17 @@ its internal count, and automatically update its text to reflect that count.
|
||||||
Each instance of `Counter()` will maintain its own independent count, since the
|
Each instance of `Counter()` will maintain its own independent count, since the
|
||||||
count source is created inside the component.
|
count source is created inside the component.
|
||||||
|
|
||||||
## External State
|
|
||||||
|
|
||||||
External sources can also be passed into components for them to use.
|
External sources can also be passed into components for them to use.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
|
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
|
||||||
local function Counter(props: { count: () -> number })
|
local function Counter(props: { count: () -> number })
|
||||||
|
=======
|
||||||
|
local function CountDisplay(props: { count: () -> number })
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b:docs/tut/crash-course/7-reactive-component.md
|
||||||
local count = props.count
|
local count = props.count
|
||||||
|
|
||||||
local instance = create "TextButton" {
|
local instance = create "TextLabel" {}
|
||||||
Activated = function()
|
|
||||||
count(count() + 1)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
instance.Text = "count: " .. count()
|
instance.Text = "count: " .. count()
|
||||||
|
|
@ -56,11 +57,11 @@ end
|
||||||
|
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
Counter {
|
CountDisplay {
|
||||||
count = count
|
count = count
|
||||||
}
|
}
|
||||||
|
|
||||||
count(1) -- the Counter component will update to display this count
|
count(1) -- the CountDisplay component will update to display this count
|
||||||
```
|
```
|
||||||
|
|
||||||
Sources can be created internally or passed in from externally, there are no
|
Sources can be created internally or passed in from externally, there are no
|
||||||
|
|
@ -3,7 +3,13 @@
|
||||||
Explicitly creating effects to update properties is tedious. You can
|
Explicitly creating effects to update properties is tedious. You can
|
||||||
*implicitly* create an effect to update properties instead.
|
*implicitly* create an effect to update properties instead.
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
```luau
|
```luau
|
||||||
|
=======
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```luau [Implicit Effect]
|
||||||
|
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
||||||
|
|
@ -22,6 +28,30 @@ local function Counter()
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```luau [Explicit Effect]
|
||||||
|
local create = vide.create
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
|
||||||
|
local function Counter()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local instance = create "TextButton" {
|
||||||
|
Activated = function()
|
||||||
|
count(count() + 1)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
instance.Text = "count: " .. count()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
This example is equivalent to the example seen on the previous page.
|
This example is equivalent to the example seen on the previous page.
|
||||||
|
|
||||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||||
|
|
@ -46,12 +76,12 @@ local function List(props: { children: () -> { Instance } })
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local list = List { children = items } -- creates a list with a single text label "A"
|
local list = List { children = items } -- creates a list with text label "A"
|
||||||
|
|
||||||
items {
|
items {
|
||||||
create "TextLabel" { Text = "B" },
|
create "TextLabel" { Text = "B" },
|
||||||
create "TextLabel" { Text = "C" }
|
create "TextLabel" { Text = "C" }
|
||||||
}
|
}
|
||||||
|
|
||||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
|
-- this will automatically unparent text label "A", and parent labels "B" and "C"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ effect(function() text() end)
|
||||||
count(1) -- prints "ran" x1
|
count(1) -- prints "ran" x1
|
||||||
```
|
```
|
||||||
|
|
||||||
`derive()` must also be called within a stable scope, just like `effect()`.
|
Because `derive()` creates a reactive scope, it must be called within a stable
|
||||||
|
scope, just like `effect()`.
|
||||||
|
|
||||||
If the recalculated value is the same as the old value, the derived source will
|
If the recalculated value is the same as the old value, the derived source will
|
||||||
not rerun the effects using it.
|
not rerun the effects using it.
|
||||||
|
|
@ -68,12 +69,12 @@ The reactive graph for the above example:
|
||||||
%%{init: {
|
%%{init: {
|
||||||
"theme": "base",
|
"theme": "base",
|
||||||
"themeVariables": {
|
"themeVariables": {
|
||||||
"primaryColor": "#1B1B1F",
|
"primaryColor": "#111720",
|
||||||
"primaryTextColor": "#fff",
|
"primaryTextColor": "#fff",
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#111720",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#0d131b",
|
||||||
"tertiaryBorderColor": "#161618"
|
"tertiaryBorderColor": "#0d131b"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
|
|
@ -86,7 +87,7 @@ end
|
||||||
```
|
```
|
||||||
|
|
||||||
Deriving a source in this manner is similar to creating an effect to update
|
Deriving a source in this manner is similar to creating an effect to update
|
||||||
another source. You should never manually do this using an effect however.
|
another source. You should avoid doing this using an effect however.
|
||||||
Improper usage could accidently create infinite loops in the reactive graph.
|
Improper usage could accidently create infinite loops in the reactive graph.
|
||||||
Always favour deriving when you need one source to update based on another
|
Always favour deriving when you need one source to update based on another
|
||||||
source.
|
source.
|
||||||
|
|
|
||||||
8
init.luau
Normal file
8
init.luau
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
local vide = require "./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>
|
||||||
|
|
||||||
|
return vide
|
||||||
157
src/apply.luau
157
src/apply.luau
|
|
@ -1,24 +1,20 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||||
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(script.Parent.flags)
|
local flags = require "./flags"
|
||||||
local throw = require(script.Parent.throw)
|
local implicit_effect = require "./implicit_effect"
|
||||||
local bind = require(script.Parent.bind)
|
local _, is_action = require "./action"()
|
||||||
local _, is_action = require(script.Parent.action)()
|
local graph = require "./graph"
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
|
|
||||||
type Array<V> = { V }
|
type Array<V> = { V }
|
||||||
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
||||||
type Map<K, V> = { [K]: V }
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
local free_caches: {
|
type Cache = {
|
||||||
-- event listeners to connect after properties are set
|
-- event listeners to connect after properties are set
|
||||||
events: Map<
|
events: Array<
|
||||||
string, -- event name
|
| string -- 1. event name
|
||||||
() -> () -- listener
|
| () -> () -- 2. listener
|
||||||
>,
|
>,
|
||||||
|
|
||||||
-- actions to run after events are connected
|
-- actions to run after events are connected
|
||||||
|
|
@ -33,18 +29,18 @@ local free_caches: {
|
||||||
Map<string, true> -- set of property names
|
Map<string, true> -- set of property names
|
||||||
>,
|
>,
|
||||||
|
|
||||||
-- use stack instead of recursive function to process nesting layers one at time
|
|
||||||
-- deeper-nested properties take precedence over shallower-nested ones
|
|
||||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||||
nested_stack: { {} | number }
|
nested_stack: { {} | number }
|
||||||
}?
|
}
|
||||||
|
|
||||||
local function borrow_caches(): typeof(assert(free_caches))
|
local free_cache: Cache?
|
||||||
if free_caches then
|
|
||||||
local caches = free_caches :: typeof(assert(free_caches))
|
local function borrow_cache(): Cache
|
||||||
free_caches = nil
|
if free_cache then
|
||||||
return caches
|
local cache = free_cache
|
||||||
|
free_cache = nil
|
||||||
|
return cache
|
||||||
else
|
else
|
||||||
return {
|
return {
|
||||||
events = {},
|
events = {},
|
||||||
|
|
@ -59,36 +55,61 @@ local function borrow_caches(): typeof(assert(free_caches))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function return_caches(caches: typeof(free_caches) )
|
local function return_cache(cache: Cache )
|
||||||
free_caches = caches
|
free_cache = cache
|
||||||
end
|
end
|
||||||
|
|
||||||
-- map of datatype names to class default constructor for aggregate init
|
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
||||||
local aggregates = {}
|
for property, value in properties do
|
||||||
for name, class in {
|
if property == "Parent" then continue end
|
||||||
CFrame = CFrame,
|
|
||||||
Color3 = Color3,
|
if type(property) == "string" then
|
||||||
UDim = UDim,
|
if flags.strict then -- check for duplicate property assignment at nesting depth
|
||||||
UDim2 = UDim2,
|
if cache.nested_debug[depth][property] then
|
||||||
Vector2 = Vector2,
|
error(`duplicate property {property} at depth {depth}`, 0)
|
||||||
Vector3 = Vector3,
|
end
|
||||||
Rect = Rect
|
cache.nested_debug[depth][property] = true
|
||||||
} :: Map<string, { [string]: any }> do
|
end
|
||||||
aggregates[name] = class.new
|
|
||||||
|
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
|
||||||
|
else
|
||||||
|
(value :: Instance).Parent = instance -- parent child
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- applies table of nested properties to an instance using full vide semantics
|
-- applies table of nested properties to an instance using full vide semantics
|
||||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||||
if not properties then
|
if not properties then
|
||||||
throw("attempt to call a constructor returned by create() with no properties")
|
error "attempt to call a constructor returned by create() with no properties"
|
||||||
end
|
end
|
||||||
|
|
||||||
local strict = flags.strict
|
|
||||||
|
|
||||||
-- queue parent assignment if any for last
|
-- queue parent assignment if any for last
|
||||||
local parent: unknown = properties.Parent
|
local parent: unknown = properties.Parent
|
||||||
|
|
||||||
local caches = borrow_caches()
|
local caches = borrow_cache()
|
||||||
local events = caches.events
|
local events = caches.events
|
||||||
local actions = caches.actions
|
local actions = caches.actions
|
||||||
local nested_debug = caches.nested_debug
|
local nested_debug = caches.nested_debug
|
||||||
|
|
@ -97,55 +118,15 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
-- process all properties
|
-- process all properties
|
||||||
local depth = 1
|
local depth = 1
|
||||||
repeat
|
repeat
|
||||||
for property, value in properties do
|
process_properties(properties, instance, caches, depth)
|
||||||
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
|
depth = table.remove(nested_stack) :: number
|
||||||
properties = table.remove(nested_stack) :: {}
|
properties = table.remove(nested_stack) :: {}
|
||||||
|
|
||||||
until not properties
|
until not properties
|
||||||
|
|
||||||
for event, listener in next, events do
|
for i = 1, #events, 2 do
|
||||||
(instance :: any)[event]:Connect(listener)
|
local event_name = events[i]
|
||||||
|
local event_listener = events[i + 1]
|
||||||
|
;(instance :: any)[event_name]:Connect(event_listener)
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, queued in next, actions do
|
for _, queued in next, actions do
|
||||||
|
|
@ -154,22 +135,20 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- finally set parent if any
|
|
||||||
if parent then
|
if parent then
|
||||||
if type(parent) == "function" then
|
if type(parent) == "function" then
|
||||||
bind.parent(instance, parent :: () -> Instance)
|
implicit_effect.parent(instance, parent :: () -> Instance)
|
||||||
else
|
else
|
||||||
instance.Parent = parent :: Instance
|
instance.Parent = parent :: Instance
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- clear caches
|
|
||||||
table.clear(events)
|
table.clear(events)
|
||||||
for _, queued in next, actions do table.clear(queued) end
|
for _, queued in next, actions do table.clear(queued) end
|
||||||
if strict then table.clear(nested_debug) end
|
if flags.strict then table.clear(nested_debug) end
|
||||||
table.clear(nested_stack)
|
table.clear(nested_stack)
|
||||||
|
|
||||||
return_caches(caches)
|
return_cache(caches)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local flags = require "./flags"
|
||||||
|
local graph = require "./graph"
|
||||||
local flags = require(script.Parent.flags)
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
|
|
||||||
local function batch(setter: () -> ())
|
local function batch(setter: () -> ())
|
||||||
local already_batching = flags.batch
|
local already_batching = flags.batch
|
||||||
|
|
@ -13,14 +10,14 @@ local function batch(setter: () -> ())
|
||||||
from = graph.get_update_queue_length()
|
from = graph.get_update_queue_length()
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, err: string? = pcall(setter)
|
local ok, err: string? = xpcall(setter, debug.traceback)
|
||||||
|
|
||||||
if not already_batching then
|
if not already_batching then
|
||||||
flags.batch = false
|
flags.batch = false
|
||||||
graph.flush_update_queue(from)
|
graph.flush_update_queue(from)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not ok then throw(`error occured while batching updates: {err}`) end
|
if not ok then error(`error occured while batching updates: {err}`, 0) end
|
||||||
end
|
end
|
||||||
|
|
||||||
return batch
|
return batch
|
||||||
|
|
|
||||||
105
src/bind.luau
105
src/bind.luau
|
|
@ -1,105 +0,0 @@
|
||||||
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,7 +1,5 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local action = require "./action"()
|
||||||
|
local cleanup = require "./cleanup"
|
||||||
local action = require(script.Parent.action)()
|
|
||||||
local cleanup = require(script.Parent.cleanup)
|
|
||||||
|
|
||||||
local function changed<T>(property: string, callback: (T) -> ())
|
local function changed<T>(property: string, callback: (T) -> ())
|
||||||
return action(function(instance)
|
return action(function(instance)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||||
local typeof = game and typeof or require "test/mock".typeof :: never
|
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
local graph = require "./graph"
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
local push_cleanup = graph.push_cleanup
|
local push_cleanup = graph.push_cleanup
|
||||||
|
|
||||||
local function helper(obj: any)
|
local function helper(obj: any)
|
||||||
return
|
return
|
||||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
||||||
|
elseif type(obj) == "thread" then function() task.cancel(obj) end
|
||||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||||
elseif obj.destroy then function() obj:destroy() end
|
elseif obj.destroy then function() obj:destroy() end
|
||||||
elseif obj.disconnect then function() obj:disconnect() end
|
elseif obj.disconnect then function() obj:disconnect() end
|
||||||
elseif obj.Destroy then function() obj:Destroy() end
|
elseif obj.Destroy then function() obj:Destroy() end
|
||||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||||
else throw("cannot cleanup given object")
|
else error "cannot cleanup given object"
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanup(value: unknown)
|
local function cleanup(value: unknown)
|
||||||
local scope = get_scope()
|
local scope = get_scope()
|
||||||
|
|
||||||
if not scope then
|
if not scope then
|
||||||
throw "cannot cleanup outside a stable or reactive scope"
|
error "cannot cleanup outside a stable or reactive scope"
|
||||||
end; assert(scope)
|
end; assert(scope)
|
||||||
|
|
||||||
if type(value) == "function" then
|
if type(value) == "function" then
|
||||||
|
|
@ -36,6 +35,7 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
||||||
|
|
||||||
return cleanup ::
|
return cleanup ::
|
||||||
( (callback: () -> ()) -> () ) &
|
( (callback: () -> ()) -> () ) &
|
||||||
|
( (thread: thread) -> () ) &
|
||||||
( (instance: Destroyable) -> () ) &
|
( (instance: Destroyable) -> () ) &
|
||||||
( (connection: Disconnectable) -> () ) &
|
( (connection: Disconnectable) -> () ) &
|
||||||
( (instance: Instance) -> () ) &
|
( (instance: Instance) -> () ) &
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
|
|
@ -46,10 +43,10 @@ local function context<T>(...: T): Context<T>
|
||||||
if has_default ~= nil then
|
if has_default ~= nil then
|
||||||
return default_value
|
return default_value
|
||||||
else
|
else
|
||||||
throw("attempt to get context when no context is set and no default context is set")
|
error("attempt to get context when no context is set and no default context is set", 0)
|
||||||
end
|
end
|
||||||
else -- set
|
else -- set
|
||||||
if not scope then return throw("attempt to set context outside of a vide scope") end
|
if not scope then return error("attempt to set context outside of a vide scope", 0) end
|
||||||
|
|
||||||
local value, component = ...
|
local value, component = ...
|
||||||
|
|
||||||
|
|
@ -64,7 +61,7 @@ local function context<T>(...: T): Context<T>
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
throw(`error while running context:\n\n{result}`)
|
error(`error while running context:\n\n{result}`, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||||
local typeof = game and typeof or require "test/mock".typeof:: never
|
local Instance = game and Instance or require "../test/mock".Instance :: never
|
||||||
local Instance = game and Instance or require "test/mock".Instance :: never
|
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
local defaults = require "./defaults"
|
||||||
local defaults = require(script.Parent.defaults)
|
local apply = require "./apply"
|
||||||
local apply = require(script.Parent.apply)
|
|
||||||
local r = require(script.Parent.roblox_types)
|
|
||||||
|
|
||||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||||
|
|
||||||
setmetatable(ctor_cache :: any, {
|
setmetatable(ctor_cache :: any, {
|
||||||
__index = function(self, class)
|
__index = function(self, class)
|
||||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||||
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
|
if not ok then error(`invalid class name, could not create instance of class { class }`, 0) end
|
||||||
|
|
||||||
local default: { [string]: unknown }? = defaults[class]
|
local default: { [string]: unknown }? = defaults[class]
|
||||||
if default then
|
if default then
|
||||||
|
|
@ -37,20 +34,25 @@ end
|
||||||
local function clone_instance(instance: Instance)
|
local function clone_instance(instance: Instance)
|
||||||
return function(properties: Props): Instance
|
return function(properties: Props): Instance
|
||||||
local clone = instance:Clone()
|
local clone = instance:Clone()
|
||||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
if not clone then error "attempt to clone a non-archivable instance" end
|
||||||
return apply(clone, properties)
|
return apply(clone, properties)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
local function create(class_or_instance: string | Instance, props: Props?): ((Props) -> Instance) | Instance
|
||||||
if type(class_or_instance) == "string" then
|
local result: (Props) -> Instance
|
||||||
return create_instance(class_or_instance)
|
if type(class_or_instance) == "string" then
|
||||||
elseif typeof(class_or_instance) == "Instance" then
|
result = create_instance(class_or_instance)
|
||||||
return clone_instance(class_or_instance)
|
elseif typeof(class_or_instance) == "Instance" then
|
||||||
else
|
result = clone_instance(class_or_instance)
|
||||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
else
|
||||||
return nil :: never
|
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
||||||
end
|
return nil :: never
|
||||||
|
end
|
||||||
|
if props then
|
||||||
|
return result(props)
|
||||||
|
end
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
type Props = { [any]: any }
|
type Props = { [any]: any }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
local Enum = game and Enum or require "test/mock".Enum :: never
|
local Enum = game and Enum or require "../test/mock".Enum :: never
|
||||||
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
|
||||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Part = {
|
Part = {
|
||||||
Material = Enum.Material.SmoothPlastic,
|
Material = Enum.Material.SmoothPlastic,
|
||||||
Size = Vector3.new(1, 1, 1),
|
Size = vector.create(1, 1, 1),
|
||||||
Anchored = true
|
Anchored = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local push_child_to_scope = graph.push_child_to_scope
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local assert_stable_scope = graph.assert_stable_scope
|
local assert_stable_scope = graph.assert_stable_scope
|
||||||
local evaluate_node = graph.evaluate_node
|
local evaluate_node = graph.evaluate_node
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,8 @@ end
|
||||||
|
|
||||||
local is_O2 = inline_test() ~= "inline_test"
|
local is_O2 = inline_test() ~= "inline_test"
|
||||||
|
|
||||||
return { strict = not is_O2, batch = false }
|
return {
|
||||||
|
strict = not is_O2,
|
||||||
|
batch = false,
|
||||||
|
defer_nested_properties = true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local flags = require "./flags"
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local flags = require(script.Parent.flags)
|
|
||||||
|
|
||||||
export type SourceNode<T> = {
|
export type SourceNode<T> = {
|
||||||
cache: T,
|
cache: T,
|
||||||
|
|
@ -24,9 +21,23 @@ export type Node<T> = {
|
||||||
|
|
||||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||||
|
|
||||||
|
local function efn(err: string)
|
||||||
|
local trace = debug.traceback(err, 2)
|
||||||
|
|
||||||
|
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
||||||
|
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
||||||
|
return "\n "
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
trace ..= "\nsource update stacktrace:"
|
||||||
|
return trace
|
||||||
|
end
|
||||||
|
|
||||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||||
|
|
||||||
local thread = coroutine.create(xpcall)
|
local thread = coroutine.create(xpcall)
|
||||||
local function efn(err: string) return debug.traceback(err, 3) end
|
--local function efn(err: string) return debug.traceback(err, 3) end
|
||||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||||
|
|
||||||
assert(resume_ok)
|
assert(resume_ok)
|
||||||
|
|
@ -47,9 +58,9 @@ local function assert_stable_scope(): Node<unknown>
|
||||||
|
|
||||||
if not scope then
|
if not scope then
|
||||||
local caller_name = debug.info(2, "n")
|
local caller_name = debug.info(2, "n")
|
||||||
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
|
return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
|
||||||
elseif scope.effect then
|
elseif scope.effect then
|
||||||
throw("cannot create a new reactive scope inside another reactive scope")
|
error("cannot create a new reactive scope inside another reactive scope", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
return scope
|
return scope
|
||||||
|
|
@ -83,8 +94,8 @@ end
|
||||||
local function flush_cleanups<T>(node: Node<T>)
|
local function flush_cleanups<T>(node: Node<T>)
|
||||||
if node.cleanups then
|
if node.cleanups then
|
||||||
for _, fn in next, node.cleanups do
|
for _, fn in next, node.cleanups do
|
||||||
local ok, err: string? = pcall(fn)
|
local ok, err: string? = xpcall(fn, debug.traceback)
|
||||||
if not ok then throw(`cleanup error: {err}`) end
|
if not ok then error(`cleanup error: {err}`, 0) end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.clear(node.cleanups)
|
table.clear(node.cleanups)
|
||||||
|
|
@ -108,6 +119,10 @@ local function unparent<T>(node: Node<T>)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function destroy<T>(node: Node<T>)
|
local function destroy<T>(node: Node<T>)
|
||||||
|
if flags.strict and table.find(scopes, node) then
|
||||||
|
error("attempt to destroy an active scope", 0)
|
||||||
|
end
|
||||||
|
|
||||||
flush_cleanups(node)
|
flush_cleanups(node)
|
||||||
unparent(node)
|
unparent(node)
|
||||||
|
|
||||||
|
|
@ -148,7 +163,7 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
table.clear(update_queue)
|
||||||
update_queue.n = 0
|
update_queue.n = 0
|
||||||
throw(`effect stacktrace:\n{new_value :: string}`)
|
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value :: T
|
node.cache = new_value :: T
|
||||||
|
|
@ -168,7 +183,7 @@ local function evaluate_node<T>(node: Node<T>)
|
||||||
if not ok then
|
if not ok then
|
||||||
table.clear(update_queue)
|
table.clear(update_queue)
|
||||||
update_queue.n = 0
|
update_queue.n = 0
|
||||||
throw(`effect stacktrace:\n{new_value}\n`)
|
error(`effect error:\n{new_value}\n`, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
node.cache = new_value
|
node.cache = new_value
|
||||||
|
|
@ -296,5 +311,7 @@ return table.freeze {
|
||||||
flush_update_queue = flush_update_queue,
|
flush_update_queue = flush_update_queue,
|
||||||
get_update_queue_length = get_update_queue_length,
|
get_update_queue_length = get_update_queue_length,
|
||||||
set_context = set_context,
|
set_context = set_context,
|
||||||
scopes = scopes
|
scopes = scopes,
|
||||||
|
|
||||||
|
q = update_queue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
119
src/implicit_effect.luau
Normal file
119
src/implicit_effect.luau
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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 next, 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 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_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
|
||||||
|
}
|
||||||
221
src/init.luau
221
src/init.luau
|
|
@ -1,219 +1,10 @@
|
||||||
--------------------------------------------------------------------------------
|
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
||||||
-- vide.luau
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local version = { major = 0, minor = 3, patch = 1 }
|
local vide = require(script.lib)
|
||||||
|
|
||||||
if not game then script = require "test/relative-string" end
|
export type source<T> = vide.source<T>
|
||||||
|
export type Source<T> = vide.Source<T>
|
||||||
local root = require(script.root)
|
export type context<T> = vide.context<T>
|
||||||
local mount = require(script.mount)
|
export type Context<T> = vide.Context<T>
|
||||||
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)
|
|
||||||
local roblox_types = require(script.roblox_types)
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
-- TYPES HERE
|
|
||||||
export type vCanvasGroup = roblox_types.vCanvasGroup
|
|
||||||
export type vFrame = roblox_types.vFrame
|
|
||||||
export type vImageButton = roblox_types.vImageButton
|
|
||||||
export type vTextButton = roblox_types.vTextButton
|
|
||||||
export type vImageLabel = roblox_types.vImageLabel
|
|
||||||
export type vTextLabel = roblox_types.vTextLabel
|
|
||||||
export type vScrollingFrame = roblox_types.vScrollingFrame
|
|
||||||
export type vTextBox = roblox_types.vTextBox
|
|
||||||
export type vVideoFrame = roblox_types.vVideoFrame
|
|
||||||
export type vViewportFrame = roblox_types.vViewportFrame
|
|
||||||
export type vBillboardGui = roblox_types.vBillboardGui
|
|
||||||
export type vScreenGui = roblox_types.vScreenGui
|
|
||||||
export type vAdGui = roblox_types.vAdGui
|
|
||||||
export type vSurfaceGui = roblox_types.vSurfaceGui
|
|
||||||
export type vSelectionBox = roblox_types.vSelectionBox
|
|
||||||
export type vBoxHandleAdornment = roblox_types.vBoxHandleAdornment
|
|
||||||
export type vConeHandleAdornment = roblox_types.vConeHandleAdornment
|
|
||||||
export type vCylinderHandleAdornment = roblox_types.vCylinderHandleAdornment
|
|
||||||
export type vImageHandleAdornment = roblox_types.vImageHandleAdornment
|
|
||||||
export type vLineHandleAdornment = roblox_types.vLineHandleAdornment
|
|
||||||
export type vSphereHandleAdornment = roblox_types.vSphereHandleAdornment
|
|
||||||
export type vWireframeHandleAdornment = roblox_types.vWireframeHandleAdornment
|
|
||||||
export type vParabolaAdornment = roblox_types.vParabolaAdornment
|
|
||||||
export type vSelectionSphere = roblox_types.vSelectionSphere
|
|
||||||
export type vArcHandles = roblox_types.vArcHandles
|
|
||||||
export type vHandles = roblox_types.vHandles
|
|
||||||
export type vSurfaceSelection = roblox_types.vSurfaceSelection
|
|
||||||
export type vPath2D = roblox_types.vPath2D
|
|
||||||
export type vUIAspectRatioConstraint = roblox_types.vUIAspectRatioConstraint
|
|
||||||
export type vUISizeConstraint = roblox_types.vUISizeConstraint
|
|
||||||
export type vUITextSizeConstraint = roblox_types.vUITextSizeConstraint
|
|
||||||
export type vUICorner = roblox_types.vUICorner
|
|
||||||
export type vUIDragDetector = roblox_types.vUIDragDetector
|
|
||||||
export type vUIFlexItem = roblox_types.vUIFlexItem
|
|
||||||
export type vUIGradient = roblox_types.vUIGradient
|
|
||||||
export type vUIListLayout = roblox_types.vUIListLayout
|
|
||||||
export type vUIGridLayout = roblox_types.vUIGridLayout
|
|
||||||
export type vUIPageLayout = roblox_types.vUIPageLayout
|
|
||||||
export type vUITableLayout = roblox_types.vUITableLayout
|
|
||||||
export type vUIPadding = roblox_types.vUIPadding
|
|
||||||
export type vUIScale = roblox_types.vUIScale
|
|
||||||
export type vUIStroke = roblox_types.vUIStroke
|
|
||||||
export type vWorldModel = roblox_types.vWorldModel
|
|
||||||
export type vCamera = roblox_types.vCamera
|
|
||||||
export type vPart = roblox_types.vPart
|
|
||||||
export type vModel = roblox_types.vModel
|
|
||||||
export type vMeshPart = roblox_types.vMeshPart
|
|
||||||
export type vHighlight = roblox_types.vHighlight
|
|
||||||
export type vFrame = roblox_types.vFrame
|
|
||||||
export type vImageButton = roblox_types.vImageButton
|
|
||||||
export type vTextButton = roblox_types.vTextButton
|
|
||||||
export type vImageLabel = roblox_types.vImageLabel
|
|
||||||
export type vTextLabel = roblox_types.vTextLabel
|
|
||||||
export type vScrollingFrame = roblox_types.vScrollingFrame
|
|
||||||
export type vTextBox = roblox_types.vTextBox
|
|
||||||
export type vVideoFrame = roblox_types.vVideoFrame
|
|
||||||
export type vViewportFrame = roblox_types.vViewportFrame
|
|
||||||
export type vBillboardGui = roblox_types.vBillboardGui
|
|
||||||
export type vScreenGui = roblox_types.vScreenGui
|
|
||||||
export type vAdGui = roblox_types.vAdGui
|
|
||||||
export type vSurfaceGui = roblox_types.vSurfaceGui
|
|
||||||
export type vSelectionBox = roblox_types.vSelectionBox
|
|
||||||
export type vBoxHandleAdornment = roblox_types.vBoxHandleAdornment
|
|
||||||
export type vConeHandleAdornment = roblox_types.vConeHandleAdornment
|
|
||||||
export type vCylinderHandleAdornment = roblox_types.vCylinderHandleAdornment
|
|
||||||
export type vImageHandleAdornment = roblox_types.vImageHandleAdornment
|
|
||||||
export type vLineHandleAdornment = roblox_types.vLineHandleAdornment
|
|
||||||
export type vSphereHandleAdornment = roblox_types.vSphereHandleAdornment
|
|
||||||
export type vWireframeHandleAdornment = roblox_types.vWireframeHandleAdornment
|
|
||||||
export type vParabolaAdornment = roblox_types.vParabolaAdornment
|
|
||||||
export type vSelectionSphere = roblox_types.vSelectionSphere
|
|
||||||
export type vArcHandles = roblox_types.vArcHandles
|
|
||||||
export type vHandles = roblox_types.vHandles
|
|
||||||
export type vSurfaceSelection = roblox_types.vSurfaceSelection
|
|
||||||
export type vPath2D = roblox_types.vPath2D
|
|
||||||
export type vUIAspectRatioConstraint = roblox_types.vUIAspectRatioConstraint
|
|
||||||
export type vUISizeConstraint = roblox_types.vUISizeConstraint
|
|
||||||
export type vUITextSizeConstraint = roblox_types.vUITextSizeConstraint
|
|
||||||
export type vUICorner = roblox_types.vUICorner
|
|
||||||
export type vUIDragDetector = roblox_types.vUIDragDetector
|
|
||||||
export type vUIFlexItem = roblox_types.vUIFlexItem
|
|
||||||
export type vUIGradient = roblox_types.vUIGradient
|
|
||||||
export type vUIListLayout = roblox_types.vUIListLayout
|
|
||||||
export type vUIGridLayout = roblox_types.vUIGridLayout
|
|
||||||
export type vUIPageLayout = roblox_types.vUIPageLayout
|
|
||||||
export type vUITableLayout = roblox_types.vUITableLayout
|
|
||||||
export type vUIPadding = roblox_types.vUIPadding
|
|
||||||
export type vUIScale = roblox_types.vUIScale
|
|
||||||
export type vUIStroke = roblox_types.vUIStroke
|
|
||||||
export type vWorldModel = roblox_types.vWorldModel
|
|
||||||
export type vCamera = roblox_types.vCamera
|
|
||||||
export type vPart = roblox_types.vPart
|
|
||||||
export type vModel = roblox_types.vModel
|
|
||||||
export type vMeshPart = roblox_types.vMeshPart
|
|
||||||
export type vHighlight = roblox_types.vHighlight
|
|
||||||
|
|
||||||
return vide
|
return vide
|
||||||
115
src/lib.luau
Normal file
115
src/lib.luau
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
local version = { major = 0, minor = 3, patch = 1 }
|
||||||
|
|
||||||
|
local root = require "./root"
|
||||||
|
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, values = require "./maps"()
|
||||||
|
local spring, update_springs = require "./spring"()
|
||||||
|
local action = require "./action"()
|
||||||
|
local changed = require "./changed"
|
||||||
|
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>
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local flags = require "./flags"
|
||||||
|
local graph = require "./graph"
|
||||||
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 Node<T> = graph.Node<T>
|
||||||
type SourceNode<T> = graph.SourceNode<T>
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
|
|
@ -22,7 +19,7 @@ local function check_primitives(t: {})
|
||||||
|
|
||||||
for _, v in next, t do
|
for _, v in next, t do
|
||||||
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||||
throw("table source map cannot return primitives")
|
error("table source map cannot return primitives", 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -63,6 +60,8 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
||||||
local cv = input_cache[i]
|
local cv = input_cache[i]
|
||||||
|
|
||||||
if cv ~= v then
|
if cv ~= v then
|
||||||
|
input_cache[i] = v
|
||||||
|
|
||||||
if cv == nil then -- create new scope and run transform
|
if cv == nil then -- create new scope and run transform
|
||||||
local scope = create_node(subowner, false, false)
|
local scope = create_node(subowner, false, false)
|
||||||
scopes[i] = scope :: Node<any>
|
scopes[i] = scope :: Node<any>
|
||||||
|
|
@ -71,7 +70,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
||||||
|
|
||||||
push_scope(scope)
|
push_scope(scope)
|
||||||
|
|
||||||
local ok, result = pcall(transform, function()
|
local ok, result = xpcall(transform, debug.traceback, function()
|
||||||
push_child_to_scope(node)
|
push_child_to_scope(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end, i)
|
end, i)
|
||||||
|
|
@ -89,8 +88,6 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
||||||
input_nodes[i].cache = v
|
input_nodes[i].cache = v
|
||||||
update_descendants(input_nodes[i])
|
update_descendants(input_nodes[i])
|
||||||
end
|
end
|
||||||
|
|
||||||
input_cache[i] = v
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -134,7 +131,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
||||||
local cache = {}
|
local cache = {}
|
||||||
for _, v in next, data do
|
for _, v in next, data do
|
||||||
if cache[v] ~= nil then
|
if cache[v] ~= nil then
|
||||||
throw "duplicate table value detected"
|
error "duplicate table value detected"
|
||||||
end
|
end
|
||||||
cache[v] = true
|
cache[v] = true
|
||||||
end
|
end
|
||||||
|
|
@ -156,7 +153,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
||||||
|
|
||||||
push_scope(scope)
|
push_scope(scope)
|
||||||
|
|
||||||
local ok, result = pcall(transform, v, function()
|
local ok, result = xpcall(transform, debug.traceback, v, function()
|
||||||
push_child_to_scope(node)
|
push_child_to_scope(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local root = require "./root"
|
||||||
|
local apply = require "./apply"
|
||||||
local root = require(script.Parent.root)
|
|
||||||
local apply = require(script.Parent.apply)
|
|
||||||
|
|
||||||
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||||
return root(function()
|
return root(function()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
if not game then script = require "test/relative-string" end
|
|
||||||
|
|
||||||
local function read<T>(value: T | () -> T): T
|
local function read<T>(value: T | () -> T): T
|
||||||
return if type(value) == "function" then value() else value
|
return if type(value) == "function" then value() else value
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
local push_scope = graph.push_scope
|
local push_scope = graph.push_scope
|
||||||
|
|
@ -16,21 +13,20 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
|
||||||
refs[node] = true -- prevent gc of root node
|
refs[node] = true -- prevent gc of root node
|
||||||
|
|
||||||
local destroy = function()
|
local destroy = function()
|
||||||
if not refs[node] then throw "root already destroyed" end
|
if not refs[node] then error "root already destroyed" end
|
||||||
refs[node] = nil
|
refs[node] = nil
|
||||||
destroy(node)
|
destroy(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
push_scope(node)
|
push_scope(node)
|
||||||
|
|
||||||
local function efn(err: string) return debug.traceback(err, 3) end
|
local result = { xpcall(fn, debug.traceback, destroy) }
|
||||||
local result = { xpcall(fn, efn, destroy) }
|
|
||||||
|
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
if not result[1] then
|
if not result[1] then
|
||||||
destroy()
|
destroy()
|
||||||
throw(`error while running root():\n\n{result[2]}`)
|
error(`error while running root():\n\n{result[2]}`, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
return destroy, unpack(result :: any, 2)
|
return destroy, unpack(result :: any, 2)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local switch = require "./switch"
|
||||||
|
|
||||||
local switch = require(script.Parent.switch)
|
|
||||||
|
|
||||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||||
local function truthy()
|
local function truthy()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local create_source_node = graph.create_source_node
|
local create_source_node = graph.create_source_node
|
||||||
local push_child_to_scope = graph.push_child_to_scope
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
|
|
@ -11,7 +9,7 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||||
local function source<T>(initial_value: T): Source<T>
|
local function source<T>(initial_value: T): Source<T>
|
||||||
local node = create_source_node(initial_value)
|
local node = create_source_node(initial_value)
|
||||||
|
|
||||||
return function(...): T
|
local function update_source(...): T
|
||||||
if select("#", ...) == 0 then -- no args were given
|
if select("#", ...) == 0 then -- no args were given
|
||||||
push_child_to_scope(node)
|
push_child_to_scope(node)
|
||||||
return node.cache
|
return node.cache
|
||||||
|
|
@ -26,6 +24,8 @@ local function source<T>(initial_value: T): Source<T>
|
||||||
update_descendants(node)
|
update_descendants(node)
|
||||||
return v
|
return v
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return update_source
|
||||||
end
|
end
|
||||||
|
|
||||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||||
|
|
|
||||||
164
src/spring.luau
164
src/spring.luau
|
|
@ -1,28 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
|
||||||
|
|
||||||
--[[
|
|
||||||
|
|
||||||
Supported datatypes:
|
|
||||||
- number
|
|
||||||
- CFrame
|
|
||||||
- Color3
|
|
||||||
- UDim
|
|
||||||
- UDim2
|
|
||||||
- Vector2
|
|
||||||
- Vector3
|
|
||||||
- Rect
|
|
||||||
|
|
||||||
Unsupported datatypes:
|
|
||||||
- bool
|
|
||||||
- Vector2int16
|
|
||||||
- Vector3int16
|
|
||||||
- EnumItem
|
|
||||||
|
|
||||||
]]
|
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
type SourceNode<T> = graph.SourceNode<T>
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
|
|
@ -33,70 +9,71 @@ local update_descendants = graph.update_descendants
|
||||||
local push_child_to_scope = graph.push_child_to_scope
|
local push_child_to_scope = graph.push_child_to_scope
|
||||||
|
|
||||||
local UPDATE_RATE = 120
|
local UPDATE_RATE = 120
|
||||||
local TOLERANCE = 0.0001
|
local TOLERANCE = 0.001
|
||||||
|
local TOLERANCE_VECTOR = vector.create(TOLERANCE, TOLERANCE, TOLERANCE)
|
||||||
type Vec3 = Vector3
|
|
||||||
|
|
||||||
local function Vec3(x: number?, y: number?, z: number?)
|
|
||||||
return Vector3.new(x, y, z)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ZERO = Vec3(0, 0, 0)
|
|
||||||
|
|
||||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||||
|
|
||||||
type SpringData<T> = {
|
--[[
|
||||||
|
Unsupported datatypes:
|
||||||
|
- bool
|
||||||
|
- Vector2int16
|
||||||
|
- Vector3int16
|
||||||
|
- EnumItem
|
||||||
|
]]
|
||||||
|
|
||||||
|
type SpringState<T> = {
|
||||||
k: number, -- spring constant
|
k: number, -- spring constant
|
||||||
c: number, -- damping coeff
|
c: number, -- damping coeff
|
||||||
|
|
||||||
-- dimensions 1-3
|
x0_123: vector, x0_456: vector, -- current position
|
||||||
x0_123: Vec3,
|
x1_123: vector, x1_456: vector, -- target position
|
||||||
x1_123: Vec3,
|
v_123: vector, v_456: vector, -- current velocity
|
||||||
v_123: Vec3,
|
|
||||||
|
|
||||||
-- dimensions 4-6
|
|
||||||
x0_456: Vec3,
|
|
||||||
x1_456: Vec3,
|
|
||||||
v_456: Vec3,
|
|
||||||
|
|
||||||
source_value: T -- current value of spring input source
|
source_value: T -- current value of spring input source
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
type SpringSettings<T> = ({
|
||||||
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
position: T?,
|
||||||
|
velocity: T?,
|
||||||
|
impulse: T?
|
||||||
|
}) -> ()
|
||||||
|
|
||||||
|
type TypeToVec6<T> = (T) -> (vector, vector)
|
||||||
|
type Vec6ToType<T> = (vector, vector) -> T
|
||||||
|
|
||||||
local type_to_vec6 = {
|
local type_to_vec6 = {
|
||||||
number = function(v)
|
number = function(v)
|
||||||
return Vec3(v, 0, 0), ZERO
|
return vector.create(v, 0, 0), vector.zero
|
||||||
end :: TypeToVec6<number>,
|
end :: TypeToVec6<number>,
|
||||||
|
|
||||||
CFrame = function(v)
|
CFrame = function(v)
|
||||||
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
return v.Position, vector.create(v:ToEulerAnglesXYZ())
|
||||||
end :: TypeToVec6<CFrame>,
|
end :: TypeToVec6<CFrame>,
|
||||||
|
|
||||||
Color3 = function(v)
|
Color3 = function(v)
|
||||||
-- todo: hsv, oklab?
|
-- todo: hsv, oklab?
|
||||||
return Vec3(v.R, v.G, v.B), ZERO
|
return vector.create(v.R, v.G, v.B), vector.zero
|
||||||
end :: TypeToVec6<Color3>,
|
end :: TypeToVec6<Color3>,
|
||||||
|
|
||||||
UDim = function(v)
|
UDim = function(v)
|
||||||
return Vec3(v.Scale, v.Offset, 0), ZERO
|
return vector.create(v.Scale, v.Offset, 0), vector.zero
|
||||||
end :: TypeToVec6<UDim>,
|
end :: TypeToVec6<UDim>,
|
||||||
|
|
||||||
UDim2 = function(v)
|
UDim2 = function(v)
|
||||||
return Vec3(v.X.Scale, v.X.Offset, v.Y.Scale), Vec3(v.Y.Offset, 0, 0)
|
return vector.create(v.X.Scale, v.X.Offset, v.Y.Scale), vector.create(v.Y.Offset, 0, 0)
|
||||||
end :: TypeToVec6<UDim2>,
|
end :: TypeToVec6<UDim2>,
|
||||||
|
|
||||||
Vector2 = function(v)
|
Vector2 = function(v)
|
||||||
return Vec3(v.X, v.Y, 0), ZERO
|
return vector.create(v.X, v.Y, 0), vector.zero
|
||||||
end :: TypeToVec6<Vector2>,
|
end :: TypeToVec6<Vector2>,
|
||||||
|
|
||||||
Vector3 = function(v)
|
Vector3 = function(v)
|
||||||
return v, ZERO
|
return v, vector.zero
|
||||||
end :: TypeToVec6<Vector3>,
|
end :: TypeToVec6<Vector3>,
|
||||||
|
|
||||||
Rect = function(v)
|
Rect = function(v)
|
||||||
return Vec3(v.Min.X, v.Min.Y, v.Max.X), Vec3(v.Max.Y, 0, 0)
|
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
|
||||||
end :: TypeToVec6<Rect>
|
end :: TypeToVec6<Rect>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +113,7 @@ local vec6_to_type = {
|
||||||
|
|
||||||
local invalid_type = {
|
local invalid_type = {
|
||||||
__index = function(_, t: string)
|
__index = function(_, t: string)
|
||||||
throw(`cannot spring type {t}`)
|
error(`cannot spring type {t}`, 0)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,10 +122,10 @@ setmetatable(vec6_to_type, invalid_type)
|
||||||
|
|
||||||
-- maps spring data to its corresponding output node
|
-- maps spring data to its corresponding output node
|
||||||
-- lifetime of spring data is tied to output node
|
-- lifetime of spring data is tied to output node
|
||||||
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
|
||||||
setmetatable(springs, { __mode = "v" })
|
setmetatable(springs :: any, { __mode = "v" })
|
||||||
|
|
||||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): (() -> T, SpringSettings<T>)
|
||||||
local owner = assert_stable_scope()
|
local owner = assert_stable_scope()
|
||||||
|
|
||||||
-- https://en.wikipedia.org/wiki/Damping
|
-- https://en.wikipedia.org/wiki/Damping
|
||||||
|
|
@ -163,20 +140,20 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
-- todo: is there a solution other than reducing step size?
|
-- todo: is there a solution other than reducing step size?
|
||||||
-- todo: this does not catch all solver exploding cases
|
-- todo: this does not catch all solver exploding cases
|
||||||
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
if c > UPDATE_RATE*2 then -- solver will explode if this is true
|
||||||
throw("spring damping too high, consider reducing damping or increasing period")
|
error("spring damping too high, consider reducing damping or increasing period", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
local data: SpringData<T> = {
|
local data: SpringState<T> = {
|
||||||
k = k,
|
k = k,
|
||||||
c = c,
|
c = c,
|
||||||
|
|
||||||
x0_123 = ZERO,
|
x0_123 = vector.zero,
|
||||||
x1_123 = ZERO,
|
x1_123 = vector.zero,
|
||||||
v_123 = ZERO,
|
v_123 = vector.zero,
|
||||||
|
|
||||||
x0_456 = ZERO,
|
x0_456 = vector.zero,
|
||||||
x1_456 = ZERO,
|
x1_456 = vector.zero,
|
||||||
v_456 = ZERO,
|
v_456 = vector.zero,
|
||||||
|
|
||||||
source_value = false :: any,
|
source_value = false :: any,
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +164,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
local value = source()
|
local value = source()
|
||||||
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
||||||
data.source_value = value
|
data.source_value = value
|
||||||
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
|
springs[data] = output
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -201,6 +178,28 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
-- set output to goal
|
-- set output to goal
|
||||||
output.cache = data.source_value
|
output.cache = data.source_value
|
||||||
|
|
||||||
|
local setter = function(p)
|
||||||
|
local x = p.position
|
||||||
|
local v = p.velocity
|
||||||
|
local dv = p.impulse
|
||||||
|
|
||||||
|
if x then
|
||||||
|
data.x0_123, data.x0_456 = type_to_vec6[typeof(x)](x)
|
||||||
|
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
|
||||||
|
|
||||||
|
springs[data] = output
|
||||||
|
end :: SpringSettings<T>
|
||||||
|
|
||||||
return function(...)
|
return function(...)
|
||||||
if select("#", ...) == 0 then -- no args were given
|
if select("#", ...) == 0 then -- no args were given
|
||||||
push_child_to_scope(output)
|
push_child_to_scope(output)
|
||||||
|
|
@ -212,8 +211,8 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
|
data.x0_123, data.x0_456 = type_to_vec6[typeof(v)](v)
|
||||||
|
|
||||||
-- reset velocity
|
-- reset velocity
|
||||||
data.v_123 = ZERO
|
data.v_123 = vector.zero
|
||||||
data.v_456 = ZERO
|
data.v_456 = vector.zero
|
||||||
|
|
||||||
-- schedule spring
|
-- schedule spring
|
||||||
springs[data] = output
|
springs[data] = output
|
||||||
|
|
@ -222,7 +221,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
||||||
output.cache = v
|
output.cache = v
|
||||||
|
|
||||||
return v
|
return v
|
||||||
end
|
end, setter
|
||||||
end
|
end
|
||||||
|
|
||||||
local function step_springs(dt: number)
|
local function step_springs(dt: number)
|
||||||
|
|
@ -263,23 +262,24 @@ local function step_springs(dt: number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local remove_queue = {}
|
|
||||||
|
|
||||||
local function update_spring_sources()
|
local function update_spring_sources()
|
||||||
for data, output in next, springs do
|
for data, output in springs do
|
||||||
local x0_123, x1_123, v_123,
|
local x0_123, x1_123, v_123,
|
||||||
x0_456, x1_456, v_456 =
|
x0_456, x1_456, v_456 =
|
||||||
data.x0_123, data.x1_123, data.v_123,
|
data.x0_123, data.x1_123, data.v_123,
|
||||||
data.x0_456, data.x1_456, data.v_456
|
data.x0_456, data.x1_456, data.v_456
|
||||||
|
|
||||||
local dx_123, dx_456 =
|
local max_difference = vector.max(
|
||||||
x0_123 - x1_123,
|
vector.abs(x0_123 - x1_123 :: any),
|
||||||
x0_456 - x1_456
|
vector.abs(x0_456 - x1_456 :: any),
|
||||||
|
vector.abs(v_123 :: any),
|
||||||
|
vector.abs(v_456 :: any),
|
||||||
|
TOLERANCE_VECTOR
|
||||||
|
)
|
||||||
|
|
||||||
-- todo: can this false positive?
|
if max_difference == TOLERANCE_VECTOR then
|
||||||
if (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
|
||||||
-- close enough to target, unshedule spring and set value to target
|
-- close enough to target, unshedule spring and set value to target
|
||||||
table.insert(remove_queue, data)
|
springs[data] = nil
|
||||||
output.cache = data.source_value
|
output.cache = data.source_value
|
||||||
else
|
else
|
||||||
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
|
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
|
||||||
|
|
@ -287,12 +287,6 @@ local function update_spring_sources()
|
||||||
|
|
||||||
update_descendants(output)
|
update_descendants(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, data in next, remove_queue do
|
|
||||||
springs[data] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
table.clear(remove_queue)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local throw = require(script.Parent.throw)
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
type SourceNode<T> = graph.SourceNode<T>
|
type SourceNode<T> = graph.SourceNode<T>
|
||||||
local create_node = graph.create_node
|
local create_node = graph.create_node
|
||||||
|
|
@ -34,7 +31,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
||||||
if component == nil then return nil end
|
if component == nil then return nil end
|
||||||
|
|
||||||
if type(component) ~= "function" then
|
if type(component) ~= "function" then
|
||||||
throw "map must map a value to a function"
|
error "map must map a value to a function"
|
||||||
end
|
end
|
||||||
|
|
||||||
local new_scope = create_node(owner, false, false)
|
local new_scope = create_node(owner, false, false)
|
||||||
|
|
@ -42,7 +39,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
||||||
|
|
||||||
push_scope(new_scope)
|
push_scope(new_scope)
|
||||||
|
|
||||||
local ok, result = pcall(component)
|
local ok, result = xpcall(component, debug.traceback)
|
||||||
|
|
||||||
pop_scope()
|
pop_scope()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
if not game then script = require "test/relative-string" end
|
|
||||||
|
|
||||||
local function VIDE_ASSERT(msg): any
|
|
||||||
error(msg, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return VIDE_ASSERT
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
if not game then script = require "test/relative-string" end
|
local graph = require "./graph"
|
||||||
|
|
||||||
local graph = require(script.Parent.graph)
|
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
local get_scope = graph.get_scope
|
local get_scope = graph.get_scope
|
||||||
|
|
||||||
|
|
@ -12,13 +10,13 @@ local function untrack<T>(source: () -> T): T
|
||||||
local effect = scope.effect
|
local effect = scope.effect
|
||||||
scope.effect = false
|
scope.effect = false
|
||||||
|
|
||||||
local ok, result = pcall(source)
|
local ok, result = xpcall(source, debug.traceback)
|
||||||
|
|
||||||
scope.effect = effect :: () -> ()
|
scope.effect = effect :: () -> ()
|
||||||
|
|
||||||
if not ok then error(result, 0) end
|
if not ok then error(result, 0) end
|
||||||
|
|
||||||
return result
|
return result :: T
|
||||||
else
|
else
|
||||||
return source()
|
return source()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
local testkit = require("test/testkit")
|
local testkit = require("./testkit")
|
||||||
local BENCH, START = testkit.benchmark()
|
local BENCH, START = testkit.benchmark()
|
||||||
|
|
||||||
local vide = require "src/init"
|
local vide = require "../../vide"
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
|
|
@ -200,7 +200,7 @@ end)
|
||||||
TITLE "property apply"
|
TITLE "property apply"
|
||||||
|
|
||||||
ROOT_BENCH("apply 0 properties", function()
|
ROOT_BENCH("apply 0 properties", function()
|
||||||
local apply = require "src/apply"
|
local apply = require "../src/apply"
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -209,7 +209,7 @@ ROOT_BENCH("apply 0 properties", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("apply 8 properties", function()
|
ROOT_BENCH("apply 8 properties", function()
|
||||||
local apply = require "src/apply"
|
local apply = require "../src/apply"
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
|
|
||||||
for i = 1, START(N) do
|
for i = 1, START(N) do
|
||||||
|
|
@ -227,7 +227,7 @@ ROOT_BENCH("apply 8 properties", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("bind property", function()
|
ROOT_BENCH("bind property", function()
|
||||||
local apply = require "src/apply"
|
local apply = require "../src/apply"
|
||||||
|
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
@ -242,7 +242,7 @@ ROOT_BENCH("bind property", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("update binding", function()
|
ROOT_BENCH("update binding", function()
|
||||||
local apply = require "src/apply"
|
local apply = require "../src/apply"
|
||||||
|
|
||||||
local instance = create("Frame") {}
|
local instance = create("Frame") {}
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
@ -504,85 +504,44 @@ ROOT_BENCH(`get context (depth={depth})`, function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
N *= 1024
|
TITLE "spring()"
|
||||||
|
|
||||||
TITLE "aggregate"
|
ROOT_BENCH("spring update", function()
|
||||||
|
local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
|
|
||||||
do
|
local src = source(0)
|
||||||
-- 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
|
|
||||||
|
|
||||||
local label = create "TextLabel" {
|
root(function()
|
||||||
AnchorPoint = Vector2.new(1, 1)
|
for i = 1, N do
|
||||||
}
|
spring(src)
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
apply(label, {
|
|
||||||
AnchorPoint = Vector2.new(i, i)
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
START(N)
|
||||||
|
|
||||||
|
src(1)
|
||||||
|
|
||||||
|
return nil
|
||||||
end)
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
ROOT_BENCH("set aggregate mock vector2", function()
|
ROOT_BENCH("spring step", function()
|
||||||
local apply = require "src/apply"
|
local root, source, spring = vide.root, vide.source, vide.spring
|
||||||
local Vector2 = require "test/mock".Vector2
|
|
||||||
|
|
||||||
local label = create "TextLabel" {
|
local src = source(0)
|
||||||
AnchorPoint = Vector2.new(1, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
root(function()
|
||||||
apply(label, {
|
for i = 1, N do
|
||||||
AnchorPoint = { i, i }
|
spring(src)
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
src(1)
|
||||||
|
|
||||||
|
START(N)
|
||||||
|
|
||||||
|
vide.step(1/60)
|
||||||
|
|
||||||
|
return nil
|
||||||
end)
|
end)
|
||||||
end
|
end)
|
||||||
|
|
||||||
-- innacurate due to no Vector3 in vanilla Luau
|
|
||||||
-- mock vector is 200x slower than native vector
|
|
||||||
|
|
||||||
-- ROOT_BENCH("spring update", function()
|
|
||||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
|
||||||
|
|
||||||
-- local src = source(0)
|
|
||||||
|
|
||||||
-- root(function()
|
|
||||||
-- for i = 1, N do
|
|
||||||
-- spring(src)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- START(N)
|
|
||||||
|
|
||||||
-- src(1)
|
|
||||||
|
|
||||||
-- return nil
|
|
||||||
-- end)
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
-- N /= 1024
|
|
||||||
|
|
||||||
-- ROOT_BENCH("spring step", function()
|
|
||||||
-- local root, source, spring = vide.root, vide.source, vide.spring
|
|
||||||
|
|
||||||
-- local src = source(0)
|
|
||||||
|
|
||||||
-- root(function()
|
|
||||||
-- for i = 1, N do
|
|
||||||
-- spring(src)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- src(1)
|
|
||||||
|
|
||||||
-- START(N)
|
|
||||||
|
|
||||||
-- vide.step(1/60)
|
|
||||||
|
|
||||||
-- return nil
|
|
||||||
-- end)
|
|
||||||
-- end)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -257,40 +257,6 @@ local Vector2 = { __type = "Vector2" } :: any do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local Vector3 = { __type = "Vector3" } :: any do
|
|
||||||
local function new(x, y, z)
|
|
||||||
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.new(x, y, z)
|
|
||||||
return new(x or 0, y or 0, z or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__add(a, b)
|
|
||||||
return new(a.X + b.X, a.Y + b.Y, a.Z + b.Z)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__sub(a, b)
|
|
||||||
return new(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__mul(a, b)
|
|
||||||
return new(a.X * b, a.Y * b, a.Z * b)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__unm(v)
|
|
||||||
return new(-v.X, -v.Y, -v.Z)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__eq(a, b)
|
|
||||||
return a.X == b.X and a.Y == b.Y
|
|
||||||
end
|
|
||||||
|
|
||||||
function Vector3.__index(v)
|
|
||||||
return (v.X^2 + v.Y^2 + v.Z^2)^0.5
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local UDim2 = { __type = "UDim2" } :: any do
|
local UDim2 = { __type = "UDim2" } :: any do
|
||||||
function UDim2.new(sx, ox, sy, oy)
|
function UDim2.new(sx, ox, sy, oy)
|
||||||
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
||||||
|
|
@ -330,7 +296,6 @@ return {
|
||||||
Instance = Instance :: typeof(Instance),
|
Instance = Instance :: typeof(Instance),
|
||||||
Color3 = Color3 :: typeof(Color3),
|
Color3 = Color3 :: typeof(Color3),
|
||||||
Vector2 = Vector2 :: typeof(Vector2),
|
Vector2 = Vector2 :: typeof(Vector2),
|
||||||
Vector3 = Vector3 :: typeof(Vector3),
|
|
||||||
UDim2 = UDim2 :: typeof(UDim2),
|
UDim2 = UDim2 :: typeof(UDim2),
|
||||||
Enum = Enum :: typeof(Enum),
|
Enum = Enum :: typeof(Enum),
|
||||||
typeof = typeof :: typeof(typeof)
|
typeof = typeof :: typeof(typeof)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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,5 +1,5 @@
|
||||||
local vide = require "src/init"
|
local vide = require "../../vide"
|
||||||
local testkit = require("test/testkit")
|
local testkit = require("../test/testkit")
|
||||||
|
|
||||||
local program_time = os.clock()
|
local program_time = os.clock()
|
||||||
|
|
||||||
|
|
|
||||||
110
test/stacktrace-test.luau
Normal file
110
test/stacktrace-test.luau
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
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
|
||||||
187
test/tests.luau
187
test/tests.luau
|
|
@ -1,12 +1,12 @@
|
||||||
local testkit = require("test/testkit")
|
local testkit = require "./testkit"
|
||||||
local TEST, CASE, CHECK, FINISH = testkit.test()
|
local TEST, CASE, CHECK, FINISH = testkit.test()
|
||||||
|
|
||||||
local mock = require "test/mock"
|
local mock = require "./mock"
|
||||||
local Instance, Signal = mock.Instance, mock.Signal
|
local Instance, Signal = mock.Instance, mock.Signal
|
||||||
local Vector2, UDim2 = mock.Vector2, mock.UDim2
|
local Vector2, UDim2 = mock.Vector2, mock.UDim2
|
||||||
|
|
||||||
local vide = require "src/init"
|
local vide = require "../../vide"
|
||||||
local graph = require "src/graph"
|
local graph = require "../../vide/src/graph"
|
||||||
type Node<T> = graph.Node<T>
|
type Node<T> = graph.Node<T>
|
||||||
|
|
||||||
type Map<K, V> = { [K] : V }
|
type Map<K, V> = { [K] : V }
|
||||||
|
|
@ -721,8 +721,21 @@ TEST("create()", wrap_root(function()
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local cleanup = vide.cleanup
|
local cleanup = vide.cleanup
|
||||||
|
|
||||||
|
do CASE "create(\"ClassName\", props) syntax"
|
||||||
|
local frame = create("Frame", { BackgroundTransparency = 0.5, Name = "Foo" })
|
||||||
|
CHECK(frame.BackgroundTransparency == 0.5)
|
||||||
|
CHECK(frame.Name == "Foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "create(Instance, props) syntax"
|
||||||
|
local frame0 = create("Frame", { BackgroundTransparency = 0.5, Name = "Foo" })
|
||||||
|
local frame = create(frame0, { BackgroundTransparency = 1 })
|
||||||
|
CHECK(frame.BackgroundTransparency == 1)
|
||||||
|
CHECK(frame.Name == "Foo")
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "apply default properties"
|
do CASE "apply default properties"
|
||||||
local defaults = require("src/defaults")
|
local defaults = require "../src/defaults"
|
||||||
local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any }
|
local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any }
|
||||||
CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel)
|
CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel)
|
||||||
CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3)
|
CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3)
|
||||||
|
|
@ -746,33 +759,35 @@ TEST("create()", wrap_root(function()
|
||||||
CHECK(text.Text == "test")
|
CHECK(text.Text == "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "aggregate construction"
|
do CASE "nested deferred"
|
||||||
local template = create "TextLabel" {
|
|
||||||
AnchorPoint = Vector2.new(),
|
|
||||||
Position = UDim2.new()
|
|
||||||
}
|
|
||||||
|
|
||||||
local text = create(template) {
|
|
||||||
AnchorPoint = { 1, 2 },
|
|
||||||
Position = { 3, 4 }
|
|
||||||
}
|
|
||||||
|
|
||||||
CHECK(text.AnchorPoint == Vector2.new(1, 2))
|
|
||||||
CHECK(text.Position == UDim2.new(3, 4))
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "nested precedence"
|
|
||||||
local text = create "TextLabel" {
|
local text = create "TextLabel" {
|
||||||
{
|
{
|
||||||
|
{ Text = "2" },
|
||||||
Text = "1",
|
Text = "1",
|
||||||
|
|
||||||
{ Text = "2" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CHECK(text.Text == "2")
|
CHECK(text.Text == "2")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "nested not deferred"
|
||||||
|
vide.defer_nested_properties = false
|
||||||
|
|
||||||
|
local t = {}
|
||||||
|
|
||||||
|
create "TextLabel" {
|
||||||
|
{
|
||||||
|
{ function() table.insert(t, 1) end } :: any,
|
||||||
|
function() table.insert(t, 2) end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(t[1] == 1)
|
||||||
|
CHECK(t[2] == 2)
|
||||||
|
|
||||||
|
vide.defer_nested_properties = true
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "independent"
|
do CASE "independent"
|
||||||
local frame = create "Frame"
|
local frame = create "Frame"
|
||||||
CHECK(frame {} ~= frame {})
|
CHECK(frame {} ~= frame {})
|
||||||
|
|
@ -981,6 +996,48 @@ TEST("create()", wrap_root(function()
|
||||||
CHECK(not obj:FindFirstChild("e"))
|
CHECK(not obj:FindFirstChild("e"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "nested children source effect"
|
||||||
|
local a = create "Frame" { Name = "a" } :: Instance
|
||||||
|
local b = create "Frame" { Name = "b" } :: Instance
|
||||||
|
local c = create "Frame" { Name = "c" } :: Instance
|
||||||
|
|
||||||
|
local nested_children = source { b, c }
|
||||||
|
local children = source { a :: Instance | () -> { Instance }, nested_children }
|
||||||
|
|
||||||
|
local parent = create "Frame" {
|
||||||
|
Name = "parent",
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(parent:FindFirstChild "b")
|
||||||
|
CHECK(parent:FindFirstChild "c")
|
||||||
|
nested_children {}
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(not parent:FindFirstChild "b")
|
||||||
|
CHECK(not parent:FindFirstChild "c")
|
||||||
|
nested_children { b }
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(parent:FindFirstChild "b")
|
||||||
|
CHECK(not parent:FindFirstChild "c")
|
||||||
|
children { a }
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(not parent:FindFirstChild "b")
|
||||||
|
CHECK(not parent:FindFirstChild "c")
|
||||||
|
nested_children { b, c }
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(not parent:FindFirstChild "b")
|
||||||
|
CHECK(not parent:FindFirstChild "c")
|
||||||
|
children { a :: Instance | () -> { Instance }, nested_children }
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(parent:FindFirstChild "b")
|
||||||
|
CHECK(parent:FindFirstChild "c")
|
||||||
|
nested_children { c }
|
||||||
|
CHECK(parent:FindFirstChild "a")
|
||||||
|
CHECK(not parent:FindFirstChild "b")
|
||||||
|
CHECK(parent:FindFirstChild "c")
|
||||||
|
end
|
||||||
|
|
||||||
do CASE "garbage collection test"
|
do CASE "garbage collection test"
|
||||||
local wref
|
local wref
|
||||||
|
|
||||||
|
|
@ -1169,7 +1226,7 @@ TEST("switch()", wrap_root(function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "reactive stack resets after error"
|
do CASE "reactive stack resets after error"
|
||||||
local scopes = require "src/graph".scopes
|
local scopes = require "../src/graph".scopes
|
||||||
local input = source(1)
|
local input = source(1)
|
||||||
|
|
||||||
local n0 = scopes.n
|
local n0 = scopes.n
|
||||||
|
|
@ -1328,7 +1385,7 @@ TEST("indexes()", wrap_root(function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "reactive stack resets after error"
|
do CASE "reactive stack resets after error"
|
||||||
local scopes = require "src/graph".scopes
|
local scopes = require "../src/graph".scopes
|
||||||
|
|
||||||
local input = source { 1 }
|
local input = source { 1 }
|
||||||
|
|
||||||
|
|
@ -1507,7 +1564,7 @@ TEST("values()", wrap_root(function()
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "reactive stack resets after error"
|
do CASE "reactive stack resets after error"
|
||||||
local scopes = require "src/graph".scopes
|
local scopes = require "../src/graph".scopes
|
||||||
|
|
||||||
local input = source { 1 }
|
local input = source { 1 }
|
||||||
|
|
||||||
|
|
@ -2260,7 +2317,7 @@ TEST("context()", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("nested effects cases", function()
|
TEST("nested effects cases", function()
|
||||||
local vide = require "src/init"
|
local vide = require "../../vide"
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local untrack = vide.untrack
|
local untrack = vide.untrack
|
||||||
|
|
@ -2483,11 +2540,14 @@ end))
|
||||||
TEST("strict", wrap_root(function()
|
TEST("strict", wrap_root(function()
|
||||||
vide.strict = true
|
vide.strict = true
|
||||||
|
|
||||||
|
local root = vide.root
|
||||||
|
local show = vide.show
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
local effect = vide.effect
|
local effect = vide.effect
|
||||||
local indexes, values = vide.indexes, vide.values
|
local indexes, values = vide.indexes, vide.values
|
||||||
|
local untrack = vide.untrack
|
||||||
|
|
||||||
do CASE "error on derived callback yield"
|
do CASE "error on derived callback yield"
|
||||||
local src = source(1)
|
local src = source(1)
|
||||||
|
|
@ -2627,6 +2687,79 @@ TEST("strict", wrap_root(function()
|
||||||
|
|
||||||
CHECK(count == 4)
|
CHECK(count == 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do CASE "destruction of active scope"
|
||||||
|
local src = source(false)
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
show(src, function()
|
||||||
|
src(false)
|
||||||
|
vide.cleanup(function() count += 1 end)
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ok = pcall(function()
|
||||||
|
src(true)
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "destruction of active scope in indexes"
|
||||||
|
local src = source {}
|
||||||
|
|
||||||
|
local count_1 = 0
|
||||||
|
local count_2 = 0
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
effect(function()
|
||||||
|
untrack(function()
|
||||||
|
indexes(src, function()
|
||||||
|
vide.cleanup(function() count_1 += 1 end)
|
||||||
|
src {}
|
||||||
|
vide.cleanup(function() count_2 += 1 end)
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ok = pcall(function()
|
||||||
|
src { 1 }
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "destruction of active scope in values"
|
||||||
|
local src = source {}
|
||||||
|
|
||||||
|
local count_1 = 0
|
||||||
|
local count_2 = 0
|
||||||
|
|
||||||
|
root(function()
|
||||||
|
effect(function()
|
||||||
|
untrack(function()
|
||||||
|
values(src, function()
|
||||||
|
vide.cleanup(function() count_1 += 1 end)
|
||||||
|
src {}
|
||||||
|
vide.cleanup(function() count_2 += 1 end)
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ok = pcall(function()
|
||||||
|
src { {} }
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(not ok)
|
||||||
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
local ok = FINISH()
|
local ok = FINISH()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue