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
|
||||
with:
|
||||
repository: Roblox/luau
|
||||
tag: "0.620"
|
||||
tag: "0.651"
|
||||
fileName: luau-ubuntu.zip
|
||||
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
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
//import { defineConfig } from "vitepress"
|
||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid({
|
||||
|
|
@ -10,12 +9,6 @@ export default withMermaid({
|
|||
base: "/vide/",
|
||||
head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
|
||||
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(tabsMarkdownPlugin)
|
||||
}
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
logo: "/logo.svg",
|
||||
|
||||
|
|
@ -25,7 +18,6 @@ export default withMermaid({
|
|||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2023-present centau'
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
|
|
@ -42,7 +34,7 @@ export default withMermaid({
|
|||
items: [
|
||||
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
|
||||
{ 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: "Animation", link: "/api/animation" },
|
||||
{ text: "Strict Mode", link: "/api/strict-mode" },
|
||||
|
|
@ -60,20 +52,25 @@ export default withMermaid({
|
|||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||
{ text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
|
||||
{ text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "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: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||
{ 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: [
|
||||
{ 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
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import layout from './layout.vue'
|
||||
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
|
||||
import './vars.css'
|
||||
import './home.css'
|
||||
import './index.css'
|
||||
|
||||
export default {
|
||||
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 */
|
||||
|
||||
:root {
|
||||
|
|
@ -67,6 +31,11 @@
|
|||
--vp-c-divider: #dfe2e6;
|
||||
--vp-c-gutter: #dfe2e6;
|
||||
--vp-plugin-tabs-tab-bg: var(--vp-c-bg);
|
||||
|
||||
--vp-badge-info-bg: #122d26;
|
||||
--vp-badge-info-text: #6bdbbd;
|
||||
--vp-badge-tip-bg: #132741;
|
||||
--vp-badge-tip-text: #70abfa;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -81,96 +50,3 @@
|
|||
--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.
|
||||
|
||||
|
|
@ -11,15 +11,20 @@ Returns a new source with a value always moving torwards the input source value.
|
|||
source: () -> T & Animatable,
|
||||
period: number = 1,
|
||||
damping_ratio: number = 1
|
||||
): () -> T
|
||||
): (() -> T, Setter<T>)
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
|
||||
|
||||
type Setter<T> = ({
|
||||
position: T?,
|
||||
velocity: T?,
|
||||
impulse: T?
|
||||
}) -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
An effect is created to update the new source every frame based on the input
|
||||
source value.
|
||||
Creates a reactive scope internally to detect source updates.
|
||||
|
||||
The movement is physically simulated according to a
|
||||
[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
|
||||
advance the simulation time by `dt` seconds and automatically stop the
|
||||
solver running in heartbeat.
|
||||
|
||||
::: warning
|
||||
Large periods or damping ratios can break the spring.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,39 +1,4 @@
|
|||
# Element Creation API
|
||||
|
||||
<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)
|
||||
```
|
||||
# Element Creation
|
||||
|
||||
## create()
|
||||
|
||||
|
|
@ -45,7 +10,7 @@ Creates a new UI element, applying any given properties.
|
|||
function create(class: string): (Properties) -> Instance
|
||||
function create(instance: Instance): (Properties) -> Instance
|
||||
|
||||
type Properties = Map<string|number, any>
|
||||
type Properties = Map<string|number, unknown>
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
|
@ -77,42 +42,22 @@ Creates a new UI element, applying any given properties.
|
|||
Basic element creation.
|
||||
|
||||
```luau
|
||||
local frame = create "Frame" {
|
||||
Name = "NewFrame",
|
||||
Position = UDim2.fromScale(1, 0)
|
||||
local frame = create "TextButton" {
|
||||
Name = "Button",
|
||||
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()
|
||||
|
||||
Creates a callback that can be passed to `create()` to invoke custom actions on
|
||||
instances.
|
||||
Creates a special object that can be passed to `create()` to invoke custom
|
||||
actions on instances.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -122,27 +67,27 @@ instances.
|
|||
|
||||
- **Details**
|
||||
|
||||
When passed to `create()`, the given callback is called with the instance
|
||||
being created as the only argument. Actions take precedence over property
|
||||
and child assignments.
|
||||
When passed to `create()`, the function is called with the instance being
|
||||
created as the only argument. Actions take precedence over property and
|
||||
child assignments.
|
||||
|
||||
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**
|
||||
|
||||
An action to listen to changed properties:
|
||||
|
||||
```luau
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
local function changed(property: string, fn: (new) -> ())
|
||||
return action(function(instance)
|
||||
local con - instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
local cn = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
fn(instance[property])
|
||||
end)
|
||||
|
||||
-- disconnect on reactive scope destruction to allow gc of instance
|
||||
-- disconnect on scope destruction to allow gc of instance
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
cn:Disconnect()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
|
@ -150,7 +95,7 @@ instances.
|
|||
local output = source ""
|
||||
|
||||
create "TextBox" {
|
||||
-- will update the `output` source anytime the text property is changed
|
||||
-- will update the output source anytime the text property is changed
|
||||
changed("Text", output)
|
||||
}
|
||||
```
|
||||
|
|
@ -162,15 +107,46 @@ A wrapper for `action()` to listen for property changes.
|
|||
- **Type**
|
||||
|
||||
```luau
|
||||
function changed(property: string, callback: (...unknown) -> ()): Action
|
||||
function changed(property: string, fn: (unknown) -> ()): Action
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Will run the given callback any time the property is changed, as well as
|
||||
when the action is initially run.
|
||||
Will run the given function immediately and whenever the property updates.
|
||||
|
||||
The changed connection is disconnected when the scope the action is ran in
|
||||
is destroyed.
|
||||
The function is called with the updated property value.
|
||||
|
||||
Runs with an action priority of 1.
|
||||
|
||||
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||
|
||||
Runs a function in a new stable scope and optionally applies its result to a
|
||||
target instance.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
This is a utility for `root()` when parenting a component to an existing
|
||||
instance.
|
||||
|
||||
The result of the function is applied to a target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
Returns a function that when called will destroy the stable scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "Vide" }
|
||||
}
|
||||
end
|
||||
|
||||
local destroy = mount(App, game.StarterGui)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,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
|
||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
||||
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**
|
||||
|
||||
```luau
|
||||
function root<T...>(fn: (() -> ()) -> T...): (() -> (), T...)
|
||||
function root<T...>(fn: (Destructor) -> T...): (Destructor, T...)
|
||||
|
||||
type Destructor = () -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Returns a function to destroy the root scope. Also passes this function as
|
||||
the first argument into its callback.
|
||||
|
||||
All values returned by the callback are also returned following the destructor.
|
||||
Returns a destructor and any values returned by the callback.
|
||||
|
||||
## source()
|
||||
|
||||
Creates a new source with the given value.
|
||||
Creates a new source.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -40,71 +48,64 @@ Creates a new source with the given value.
|
|||
|
||||
- **Details**
|
||||
|
||||
Calling the returned source with no argument will return its stored value,
|
||||
calling with an argument will set a new value.
|
||||
Call the returned source with no argument to read its value.
|
||||
Call the returned source with an argument to set its value.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local count = source(0)
|
||||
|
||||
count() -- 0
|
||||
|
||||
count(count() + 1) -- 1
|
||||
print(count())-- 0
|
||||
count(count() + 1)
|
||||
print(count()) -- 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**
|
||||
|
||||
```luau
|
||||
function effect(callback: () -> ())
|
||||
function effect(fn: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Any time a source referenced in the callback is updated, the callback will
|
||||
be reran.
|
||||
|
||||
The callback is ran once immediately.
|
||||
The function is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
```luau
|
||||
local num = source(1)
|
||||
local count = source(1)
|
||||
|
||||
effect(function()
|
||||
print(num())
|
||||
print(count())
|
||||
end)
|
||||
|
||||
-- prints 1
|
||||
|
||||
num(num() + 1)
|
||||
count(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**
|
||||
|
||||
```luau
|
||||
function derive<T>(source: () -> T): () -> T
|
||||
function derive<T>(fn: () -> T): () -> T
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The derived source will have its value recalculated when any source source
|
||||
it derives from is updated.
|
||||
Anytime the reactive scope reruns, the output source value is set to what is
|
||||
returned.
|
||||
|
||||
Anytime its value is recalculated it is also cached, subsequent calls will
|
||||
retun this cached value until it recalculates again.
|
||||
|
||||
The callback is ran once immediately.
|
||||
The function is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -112,11 +113,43 @@ Derives a new source in a new reactive scope from existing sources.
|
|||
local count = source(0)
|
||||
local text = derive(function() return `count: {count()}` end)
|
||||
|
||||
text() -- "count: 0"
|
||||
print(text()) -- "count: 0"
|
||||
|
||||
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()
|
||||
|
||||
Runs a callback anytime a scope is reran or destroyed.
|
||||
Queues a callback to run when a scope is reran or destroyed.
|
||||
|
||||
- **Type**
|
||||
|
||||
```luau
|
||||
<<<<<<< HEAD
|
||||
function cleanup(callback: () -> ())
|
||||
function cleanup(obj: Destroyable)
|
||||
function cleanup(obj: Disconnectable)
|
||||
=======
|
||||
function cleanup(v: Function | Disconnectable | Destroyable | thread)
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||
|
||||
type Function = () -> ()
|
||||
type Destroyable = { destroy: () -> () }
|
||||
type Disconnectable = { disconnect: () -> () }
|
||||
```
|
||||
|
|
@ -18,20 +23,31 @@ Runs a callback anytime a scope is reran or destroyed.
|
|||
- **Example**
|
||||
|
||||
```luau
|
||||
<<<<<<< HEAD
|
||||
local data = source(1)
|
||||
=======
|
||||
local count = source(0)
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||
|
||||
effect(function()
|
||||
local label = create "TextLabel" { Text = data() }
|
||||
local destroy = root(function()
|
||||
effect(function()
|
||||
count()
|
||||
|
||||
cleanup(function()
|
||||
label:Destroy()
|
||||
cleanup(function()
|
||||
print "cleaned"
|
||||
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**
|
||||
|
||||
|
|
@ -55,16 +71,15 @@ Runs a given function in a new stable scope.
|
|||
end)
|
||||
|
||||
print(sum()) -- 0
|
||||
b(1)
|
||||
b(1) -- untracked so reactive scope created by derive() does not rerun
|
||||
print(sum()) -- 0
|
||||
a(1)
|
||||
a(1) -- reactive scope created by derive() reruns
|
||||
print(sum()) -- 2
|
||||
```
|
||||
|
||||
## read()
|
||||
|
||||
Utility used to read a value that is either a primitive or a source. Sources
|
||||
read can still be tracked inside a reactive scope.
|
||||
Utility used to read a value that is either a primitive or a source.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -74,8 +89,8 @@ read can still be tracked inside a reactive scope.
|
|||
|
||||
## batch()
|
||||
|
||||
Runs a given function where any source updates made within the function do not
|
||||
trigger effects until after the function finishes running.
|
||||
Runs a function where any source updates made within the function do not
|
||||
trigger effects until after the function ends.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -86,11 +101,29 @@ trigger effects until after the function finishes running.
|
|||
- **Details**
|
||||
|
||||
Improves performance when an effect depends on multiple sources, and those
|
||||
sources need to be updated. Updating those sources inside a batch call will
|
||||
only cause the effect to run once after the batch call ends instead of after
|
||||
each time a source is updated.
|
||||
sources need to be 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.
|
||||
|
||||
|
|
@ -101,15 +134,17 @@ Creates a new context.
|
|||
|
||||
type Context<T> =
|
||||
() -> T -- get
|
||||
& (T, () -> ()) -> () -- set
|
||||
& <U>(T, () -> U) -> U -- set
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Calling `context()` returns a new context function.
|
||||
Call this function with no arguments to get the context value.
|
||||
Call this function with a value and a callback to set a new context with the
|
||||
given value.
|
||||
Call this function with a value and a function to create a new context with
|
||||
the given value.
|
||||
|
||||
The new context is run under a stable scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -131,4 +166,3 @@ Creates a new context.
|
|||
end)
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -14,25 +14,24 @@ and identifying improper usage.
|
|||
|
||||
Currently, strict mode will:
|
||||
|
||||
1. Run derived sources twice a source updates.
|
||||
2. Run effects twice when a source updates.
|
||||
3. Throw an error if yields occur where they are not allowed.
|
||||
4. Checks for `indexes()` and `values()` returning primitive values.
|
||||
5. Checks for `values()` input having duplicate values.
|
||||
6. Checks for duplicate nested properties at same depth.
|
||||
7. Better error reporting and stack traces + creation traces of property bindings.
|
||||
1. Run reactive scopes twice when a source updates.
|
||||
2. Throw an error if yields occur where they are not allowed.
|
||||
3. Checks for `indexes()` and `values()` outputting primitive values.
|
||||
4. Checks for `values()` input having duplicate values.
|
||||
5. Checks for duplicate nested properties at same depth.
|
||||
6. Checks for destruction of an active scope.
|
||||
7. Better error reporting and stack traces.
|
||||
|
||||
By rerunning derived sources and effects twice each time they update, it helps
|
||||
ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
By rerunning reactive scopes twice each time they update, it helps ensure that
|
||||
computations are pure, and that any cleanup is done correctly.
|
||||
|
||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||
which strict mode will catch.
|
||||
|
||||
As well as additional safety checks, Vide will dedicate extra resources to
|
||||
recording and better emitting stack traces where errors occur, particularly
|
||||
when 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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ next:
|
|||
|
||||
hero:
|
||||
name: "Vide"
|
||||
tagline: A reactive UI and state library for Luau.
|
||||
tagline: A reactive UI library for Luau.
|
||||
image:
|
||||
src: /logo.svg
|
||||
actions:
|
||||
|
|
@ -22,11 +22,4 @@ hero:
|
|||
text: API Reference
|
||||
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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "1.4.1",
|
||||
"vitepress-plugin-mermaid": "2.0.17",
|
||||
"vitepress-plugin-tabs": "^0.5.0"
|
||||
"vitepress": "^1.4.5",
|
||||
"vitepress-plugin-mermaid": "2.0.17"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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?
|
||||
|
||||
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
|
||||
- Complete typechecking
|
||||
- Independence from instances
|
||||
- Syntax minimal.
|
||||
- Data oriented.
|
||||
- Typechecking compatible.
|
||||
- Instance independent.
|
||||
|
||||
Vide's reactivity operates with the concept
|
||||
of scopes which carries a learning curve, though is what makes Vide's minimal
|
||||
syntax possible. The crash course will introduce these concepts gradually.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ destroyed, or when a stable scope is destroyed.
|
|||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local count = source(0)
|
||||
|
||||
|
||||
local destroy = root(function()
|
||||
effect(function()
|
||||
local x = count()
|
||||
|
|
|
|||
|
|
@ -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 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.
|
||||
|
||||
```luau
|
||||
local action = vide.action
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
```luau
|
||||
=======
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||
create "TextLabel" {
|
||||
Text = "test",
|
||||
|
||||
|
|
@ -24,12 +26,13 @@ action used to listen for property changes:
|
|||
|
||||
```luau
|
||||
local action = vide.action
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function changed(prop: string, callback: (new) -> ())
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
|
||||
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
|
|
@ -49,7 +52,7 @@ effect(function()
|
|||
print(output())
|
||||
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
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ local count = source(0)
|
|||
|
||||
local ran = 0
|
||||
effect(function()
|
||||
count()
|
||||
ran += 1
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Created by:
|
|||
|
||||
- `root()`
|
||||
- `untrack()`
|
||||
- `switch()`
|
||||
- `show()`
|
||||
- `indexes()`
|
||||
|
||||
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 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
|
||||
exception of the scope created by `root()`.
|
||||
When a scope is rerun or destroyed, all scopes created within it are
|
||||
automatically destroyed.
|
||||
|
||||
When a scope is rerun or destroyed, all scopes owned by it are automatically
|
||||
destroyed.
|
||||
|
||||
`root()` creates a stable scope with no owner, instead it is destroyed manually.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
|
||||
|
||||
Queue a function to run using `cleanup()`.
|
||||
Any functions queued by `cleanup()` are also ran.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
|
|
@ -78,7 +69,7 @@ local count = source(0)
|
|||
|
||||
root(function()
|
||||
local text = derive(function()
|
||||
return "count: " .. text()
|
||||
return "count: " .. count()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
|
|
@ -93,12 +84,12 @@ end)
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#202530"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -118,6 +109,5 @@ Notes:
|
|||
- An update to `count` will cause `text` to rerun, which
|
||||
then causes `effect` to rerun.
|
||||
- When the root scope is destroyed, `text` and
|
||||
`effect` will be destroyed alongside it, since they are
|
||||
owned by it. `count` will be untouched and future updates
|
||||
to `count` will have no effect.
|
||||
`effect` will be destroyed alongside it, since they were created within it.
|
||||
`count` will be untouched and future updates to `count` will have no effect.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
```luau
|
||||
|
|
@ -47,3 +50,22 @@ count(2)
|
|||
|
||||
If a source is updated with the same value it already had, it will not rerun
|
||||
effects depending on it.
|
||||
|
||||
You can also read from a source within an effect without the effect tracking it.
|
||||
|
||||
```luau
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
||||
local a = source(0)
|
||||
local b = source(0)
|
||||
|
||||
effect(function()
|
||||
print(`a: {a()} b: {untrack(b)}`)
|
||||
end)
|
||||
|
||||
a(1) -- prints "a: 1 b: 0"
|
||||
b(1) -- prints nothing
|
||||
a(2) -- prints "a: 2 b: 1"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,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
|
||||
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()`
|
||||
- Stable scopes never rerun
|
||||
- Reactive scopes can rerun
|
||||
- A reactive scope cannot be created within another reactive scope
|
||||
- A scope must be created within another scope.
|
||||
- Stable scopes never rerun.
|
||||
- Reactive scopes can rerun.
|
||||
- A reactive scope cannot be created within another reactive scope, only within
|
||||
a stable scope.
|
||||
|
||||
An exception to the first rule is `root()`, which creates the initial scope that
|
||||
you destroy manually with a destructor function it returns.
|
||||
|
||||
`effect()` creates a reactive scope.
|
||||
`root()` creates a stable scope.
|
||||
`effect()` creates a reactive scope.
|
||||
|
||||
Whenever a scope is destroyed, any scope created within that scope is also
|
||||
destroyed, and so on. This is why all scopes must be created within another
|
||||
scope, except `root()` which is used to create the initial scope that you can
|
||||
manually destroy.
|
||||
destroyed, and so on.
|
||||
|
||||
```luau
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function setup()
|
||||
local count = source(0)
|
||||
local count = source(0)
|
||||
|
||||
local function setup()
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count
|
||||
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
|
||||
count(1) -- prints "1"
|
||||
```
|
||||
|
|
@ -57,12 +58,16 @@ local function setup()
|
|||
end
|
||||
|
||||
local destroy, count = root(setup)
|
||||
=======
|
||||
local destroy = root(setup) -- ok since effect() was called in a stable scope
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||
|
||||
count(1) -- prints "1"
|
||||
count(2) -- prints "2"
|
||||
|
||||
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*.
|
||||
|
|
@ -73,12 +78,12 @@ The reactive graph for the above example looks like so:
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -90,7 +95,7 @@ subgraph root
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
|
||||
## Internal State
|
||||
|
||||
=======
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b:docs/tut/crash-course/7-reactive-component.md
|
||||
```luau
|
||||
local create = vide.create
|
||||
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
|
||||
count source is created inside the component.
|
||||
|
||||
## External State
|
||||
|
||||
External sources can also be passed into components for them to use.
|
||||
|
||||
```luau
|
||||
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
|
||||
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 instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
local instance = create "TextLabel" {}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
|
|
@ -56,11 +57,11 @@ end
|
|||
|
||||
local count = source(0)
|
||||
|
||||
Counter {
|
||||
CountDisplay {
|
||||
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
|
||||
|
|
@ -3,7 +3,13 @@
|
|||
Explicitly creating effects to update properties is tedious. You can
|
||||
*implicitly* create an effect to update properties instead.
|
||||
|
||||
<<<<<<< HEAD
|
||||
```luau
|
||||
=======
|
||||
::: code-group
|
||||
|
||||
```luau [Implicit Effect]
|
||||
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
|
|
@ -22,6 +28,30 @@ local function Counter()
|
|||
end
|
||||
```
|
||||
|
||||
```luau [Explicit Effect]
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
end)
|
||||
|
||||
return instance
|
||||
end
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This example is equivalent to the example seen on the previous page.
|
||||
|
||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||
|
|
@ -46,12 +76,12 @@ local function List(props: { children: () -> { Instance } })
|
|||
}
|
||||
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 {
|
||||
create "TextLabel" { Text = "B" },
|
||||
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
|
||||
```
|
||||
|
||||
`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
|
||||
not rerun the effects using it.
|
||||
|
|
@ -68,12 +69,12 @@ The reactive graph for the above example:
|
|||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryColor": "#111720",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"primaryBorderColor": "#111720",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
"tertiaryColor": "#0d131b",
|
||||
"tertiaryBorderColor": "#0d131b"
|
||||
}
|
||||
}}%%
|
||||
|
||||
|
|
@ -86,7 +87,7 @@ end
|
|||
```
|
||||
|
||||
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.
|
||||
Always favour deriving when you need one source to update based on another
|
||||
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 Vector2 = game and Vector2 or require "test/mock".Vector2 :: never
|
||||
local UDim2 = game and UDim2 or require "test/mock".UDim2 :: never
|
||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local _, is_action = require(script.Parent.action)()
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local implicit_effect = require "./implicit_effect"
|
||||
local _, is_action = require "./action"()
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
type Array<V> = { V }
|
||||
type ArrayOrV<V> = {ArrayOrV<V>} | V
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local free_caches: {
|
||||
type Cache = {
|
||||
-- event listeners to connect after properties are set
|
||||
events: Map<
|
||||
string, -- event name
|
||||
() -> () -- listener
|
||||
events: Array<
|
||||
| string -- 1. event name
|
||||
| () -> () -- 2. listener
|
||||
>,
|
||||
|
||||
-- actions to run after events are connected
|
||||
|
|
@ -33,18 +29,18 @@ local free_caches: {
|
|||
Map<string, true> -- set of property names
|
||||
>,
|
||||
|
||||
-- use stack instead of recursive function to process nesting layers one at time
|
||||
-- deeper-nested properties take precedence over shallower-nested ones
|
||||
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
|
||||
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
|
||||
nested_stack: { {} | number }
|
||||
}?
|
||||
}
|
||||
|
||||
local function borrow_caches(): typeof(assert(free_caches))
|
||||
if free_caches then
|
||||
local caches = free_caches :: typeof(assert(free_caches))
|
||||
free_caches = nil
|
||||
return caches
|
||||
local free_cache: Cache?
|
||||
|
||||
local function borrow_cache(): Cache
|
||||
if free_cache then
|
||||
local cache = free_cache
|
||||
free_cache = nil
|
||||
return cache
|
||||
else
|
||||
return {
|
||||
events = {},
|
||||
|
|
@ -59,36 +55,61 @@ local function borrow_caches(): typeof(assert(free_caches))
|
|||
end
|
||||
end
|
||||
|
||||
local function return_caches(caches: typeof(free_caches) )
|
||||
free_caches = caches
|
||||
local function return_cache(cache: Cache )
|
||||
free_cache = cache
|
||||
end
|
||||
|
||||
-- map of datatype names to class default constructor for aggregate init
|
||||
local aggregates = {}
|
||||
for name, class in {
|
||||
CFrame = CFrame,
|
||||
Color3 = Color3,
|
||||
UDim = UDim,
|
||||
UDim2 = UDim2,
|
||||
Vector2 = Vector2,
|
||||
Vector3 = Vector3,
|
||||
Rect = Rect
|
||||
} :: Map<string, { [string]: any }> do
|
||||
aggregates[name] = class.new
|
||||
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if flags.strict then -- check for duplicate property assignment at nesting depth
|
||||
if cache.nested_debug[depth][property] then
|
||||
error(`duplicate property {property} at depth {depth}`, 0)
|
||||
end
|
||||
cache.nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if 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
|
||||
|
||||
-- applies table of nested properties to an instance using full vide semantics
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
if not properties then
|
||||
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
|
||||
|
||||
local strict = flags.strict
|
||||
|
||||
-- queue parent assignment if any for last
|
||||
local parent: unknown = properties.Parent
|
||||
|
||||
local caches = borrow_caches()
|
||||
local caches = borrow_cache()
|
||||
local events = caches.events
|
||||
local actions = caches.actions
|
||||
local nested_debug = caches.nested_debug
|
||||
|
|
@ -97,55 +118,15 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
-- process all properties
|
||||
local depth = 1
|
||||
repeat
|
||||
for property, value in properties do
|
||||
if property == "Parent" then continue end
|
||||
|
||||
if type(property) == "string" then
|
||||
if strict then -- check for duplicate prop assignment at nesting depth
|
||||
if nested_debug[depth][property] then
|
||||
throw(`duplicate property {property} at depth {depth}`)
|
||||
end
|
||||
nested_debug[depth][property] = true
|
||||
end
|
||||
|
||||
if type(value) == "table" then -- attempt aggregate init
|
||||
local ctor = aggregates[typeof((instance :: any)[property])]
|
||||
if ctor == nil then
|
||||
throw(`cannot aggregate type {typeof(value)} for property {property}`)
|
||||
end
|
||||
(instance :: any)[property] = ctor(unpack(value :: {}))
|
||||
elseif type(value) == "function" then
|
||||
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
|
||||
events[property] = value :: () -> () -- add event to buffer
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ()) -- bind property
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value -- set property
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
bind.children(instance, value :: () -> ArrayOrV<Instance>) -- bind children
|
||||
elseif type(value) == "table" then
|
||||
if is_action(value) then
|
||||
table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
|
||||
else
|
||||
table.insert(nested_stack, value :: {})
|
||||
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
|
||||
end
|
||||
else
|
||||
(value :: Instance).Parent = instance -- parent child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_properties(properties, instance, caches, depth)
|
||||
depth = table.remove(nested_stack) :: number
|
||||
properties = table.remove(nested_stack) :: {}
|
||||
|
||||
until not properties
|
||||
|
||||
for event, listener in next, events do
|
||||
(instance :: any)[event]:Connect(listener)
|
||||
for i = 1, #events, 2 do
|
||||
local event_name = events[i]
|
||||
local event_listener = events[i + 1]
|
||||
;(instance :: any)[event_name]:Connect(event_listener)
|
||||
end
|
||||
|
||||
for _, queued in next, actions do
|
||||
|
|
@ -154,22 +135,20 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
|
|||
end
|
||||
end
|
||||
|
||||
-- finally set parent if any
|
||||
if parent then
|
||||
if type(parent) == "function" then
|
||||
bind.parent(instance, parent :: () -> Instance)
|
||||
implicit_effect.parent(instance, parent :: () -> Instance)
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
-- clear caches
|
||||
table.clear(events)
|
||||
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)
|
||||
|
||||
return_caches(caches)
|
||||
return_cache(caches)
|
||||
|
||||
return instance
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local graph = require "./graph"
|
||||
|
||||
local function batch(setter: () -> ())
|
||||
local already_batching = flags.batch
|
||||
|
|
@ -13,14 +10,14 @@ local function batch(setter: () -> ())
|
|||
from = graph.get_update_queue_length()
|
||||
end
|
||||
|
||||
local ok, err: string? = pcall(setter)
|
||||
local ok, err: string? = xpcall(setter, debug.traceback)
|
||||
|
||||
if not already_batching then
|
||||
flags.batch = false
|
||||
graph.flush_update_queue(from)
|
||||
end
|
||||
|
||||
if not ok then throw(`error occured while batching updates: {err}`) end
|
||||
if not ok then error(`error occured while batching updates: {err}`, 0) end
|
||||
end
|
||||
|
||||
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(script.Parent.action)()
|
||||
local cleanup = require(script.Parent.cleanup)
|
||||
local action = require "./action"()
|
||||
local cleanup = require "./cleanup"
|
||||
|
||||
local function changed<T>(property: string, callback: (T) -> ())
|
||||
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(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local get_scope = graph.get_scope
|
||||
local push_cleanup = graph.push_cleanup
|
||||
|
||||
local function helper(obj: any)
|
||||
return
|
||||
if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
|
||||
elseif type(obj) == "thread" then function() task.cancel(obj) end
|
||||
elseif typeof(obj) == "Instance" then function() obj:Destroy() end
|
||||
elseif obj.destroy then function() obj:destroy() end
|
||||
elseif obj.disconnect then function() obj:disconnect() end
|
||||
elseif obj.Destroy then function() obj:Destroy() end
|
||||
elseif obj.Disconnect then function() obj:Disconnect() end
|
||||
else throw("cannot cleanup given object")
|
||||
else error "cannot cleanup given object"
|
||||
end
|
||||
|
||||
local function cleanup(value: unknown)
|
||||
local scope = get_scope()
|
||||
|
||||
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)
|
||||
|
||||
if type(value) == "function" then
|
||||
|
|
@ -36,6 +35,7 @@ type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
|
|||
|
||||
return cleanup ::
|
||||
( (callback: () -> ()) -> () ) &
|
||||
( (thread: thread) -> () ) &
|
||||
( (instance: Destroyable) -> () ) &
|
||||
( (connection: Disconnectable) -> () ) &
|
||||
( (instance: Instance) -> () ) &
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local get_scope = graph.get_scope
|
||||
|
|
@ -46,10 +43,10 @@ local function context<T>(...: T): Context<T>
|
|||
if has_default ~= nil then
|
||||
return default_value
|
||||
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
|
||||
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 = ...
|
||||
|
||||
|
|
@ -64,7 +61,7 @@ local function context<T>(...: T): Context<T>
|
|||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
throw(`error while running context:\n\n{result}`)
|
||||
error(`error while running context:\n\n{result}`, 0)
|
||||
end
|
||||
|
||||
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 Instance = game and Instance or require "test/mock".Instance :: never
|
||||
local typeof = game and typeof or require "../test/mock".typeof :: never
|
||||
local Instance = game and Instance or require "../test/mock".Instance :: never
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local apply = require(script.Parent.apply)
|
||||
local r = require(script.Parent.roblox_types)
|
||||
local defaults = require "./defaults"
|
||||
local apply = require "./apply"
|
||||
|
||||
local ctor_cache = {} :: { [string]: () -> Instance }
|
||||
|
||||
setmetatable(ctor_cache :: any, {
|
||||
__index = function(self, class)
|
||||
local ok, instance: Instance = pcall(Instance.new, class :: any)
|
||||
if not ok then 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]
|
||||
if default then
|
||||
|
|
@ -37,20 +34,25 @@ end
|
|||
local function clone_instance(instance: Instance)
|
||||
return function(properties: Props): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then throw "attempt to clone a non-archivable instance" end
|
||||
if not clone then error "attempt to clone a non-archivable instance" end
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
local function create(class_or_instance: string|Instance): (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
return create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
return clone_instance(class_or_instance)
|
||||
else
|
||||
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
|
||||
return nil :: never
|
||||
end
|
||||
local function create(class_or_instance: string | Instance, props: Props?): ((Props) -> Instance) | Instance
|
||||
local result: (Props) -> Instance
|
||||
if type(class_or_instance) == "string" then
|
||||
result = create_instance(class_or_instance)
|
||||
elseif typeof(class_or_instance) == "Instance" then
|
||||
result = clone_instance(class_or_instance)
|
||||
else
|
||||
error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
|
||||
return nil :: never
|
||||
end
|
||||
if props then
|
||||
return result(props)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
type Props = { [any]: any }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
local Enum = game and Enum or require "test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "test/mock".Color3 :: never
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
local Enum = game and Enum or require "../test/mock".Enum :: never
|
||||
local Color3 = game and Color3 or require "../test/mock".Color3 :: never
|
||||
|
||||
return {
|
||||
Part = {
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
Size = Vector3.new(1, 1, 1),
|
||||
Size = vector.create(1, 1, 1),
|
||||
Anchored = true
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local create_node = graph.create_node
|
||||
local push_child_to_scope = graph.push_child_to_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(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
local create_node = graph.create_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@ end
|
|||
|
||||
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 throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local flags = require "./flags"
|
||||
|
||||
export type SourceNode<T> = {
|
||||
cache: T,
|
||||
|
|
@ -24,9 +21,23 @@ export type Node<T> = {
|
|||
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||
|
||||
local function efn(err: string)
|
||||
local trace = debug.traceback(err, 2)
|
||||
|
||||
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
|
||||
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
|
||||
return "\n "
|
||||
end)
|
||||
end
|
||||
|
||||
trace ..= "\nsource update stacktrace:"
|
||||
return trace
|
||||
end
|
||||
|
||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||
|
||||
local thread = coroutine.create(xpcall)
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
--local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||
|
||||
assert(resume_ok)
|
||||
|
|
@ -47,9 +58,9 @@ local function assert_stable_scope(): Node<unknown>
|
|||
|
||||
if not scope then
|
||||
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
|
||||
throw("cannot create a new reactive scope inside another reactive scope")
|
||||
error("cannot create a new reactive scope inside another reactive scope", 0)
|
||||
end
|
||||
|
||||
return scope
|
||||
|
|
@ -83,8 +94,8 @@ end
|
|||
local function flush_cleanups<T>(node: Node<T>)
|
||||
if node.cleanups then
|
||||
for _, fn in next, node.cleanups do
|
||||
local ok, err: string? = pcall(fn)
|
||||
if not ok then throw(`cleanup error: {err}`) end
|
||||
local ok, err: string? = xpcall(fn, debug.traceback)
|
||||
if not ok then error(`cleanup error: {err}`, 0) end
|
||||
end
|
||||
|
||||
table.clear(node.cleanups)
|
||||
|
|
@ -108,6 +119,10 @@ local function unparent<T>(node: Node<T>)
|
|||
end
|
||||
|
||||
local function destroy<T>(node: Node<T>)
|
||||
if flags.strict and table.find(scopes, node) then
|
||||
error("attempt to destroy an active scope", 0)
|
||||
end
|
||||
|
||||
flush_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
|
|
@ -148,7 +163,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`effect stacktrace:\n{new_value :: string}`)
|
||||
error(`effect error stacktrace\n{new_value :: string}`, 0)
|
||||
end
|
||||
|
||||
node.cache = new_value :: T
|
||||
|
|
@ -168,7 +183,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`effect stacktrace:\n{new_value}\n`)
|
||||
error(`effect error:\n{new_value}\n`, 0)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
|
@ -296,5 +311,7 @@ return table.freeze {
|
|||
flush_update_queue = flush_update_queue,
|
||||
get_update_queue_length = get_update_queue_length,
|
||||
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 @@
|
|||
--------------------------------------------------------------------------------
|
||||
-- vide.luau
|
||||
--------------------------------------------------------------------------------
|
||||
assert(game, "when using vide outside of Roblox, require lib.luau instead")
|
||||
|
||||
local version = { major = 0, minor = 3, patch = 1 }
|
||||
local vide = require(script.lib)
|
||||
|
||||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.root)
|
||||
local mount = require(script.mount)
|
||||
local create = require(script.create)
|
||||
local apply = require(script.apply)
|
||||
local source = require(script.source)
|
||||
local effect = require(script.effect)
|
||||
local derive = require(script.derive)
|
||||
local cleanup = require(script.cleanup)
|
||||
local untrack = require(script.untrack)
|
||||
local read = require(script.read)
|
||||
local batch = require(script.batch)
|
||||
local context = require(script.context)
|
||||
local switch = require(script.switch)
|
||||
local show = require(script.show)
|
||||
local indexes, values = require(script.maps)()
|
||||
local spring, update_springs = require(script.spring)()
|
||||
local action = require(script.action)()
|
||||
local changed = require(script.changed)
|
||||
local throw = require(script.throw)
|
||||
local flags = require(script.flags)
|
||||
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
|
||||
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
|
||||
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 throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
local flags = require "./flags"
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
|
|
@ -22,7 +19,7 @@ local function check_primitives(t: {})
|
|||
|
||||
for _, v in next, t do
|
||||
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||
throw("table source map cannot return primitives")
|
||||
error("table source map cannot return primitives", 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -63,6 +60,8 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
local cv = input_cache[i]
|
||||
|
||||
if cv ~= v then
|
||||
input_cache[i] = v
|
||||
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[i] = scope :: Node<any>
|
||||
|
|
@ -71,7 +70,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, function()
|
||||
local ok, result = xpcall(transform, debug.traceback, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end, i)
|
||||
|
|
@ -89,8 +88,6 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
input_nodes[i].cache = v
|
||||
update_descendants(input_nodes[i])
|
||||
end
|
||||
|
||||
input_cache[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -134,7 +131,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
local cache = {}
|
||||
for _, v in next, data do
|
||||
if cache[v] ~= nil then
|
||||
throw "duplicate table value detected"
|
||||
error "duplicate table value detected"
|
||||
end
|
||||
cache[v] = true
|
||||
end
|
||||
|
|
@ -156,7 +153,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, v, function()
|
||||
local ok, result = xpcall(transform, debug.traceback, v, function()
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local root = require(script.Parent.root)
|
||||
local apply = require(script.Parent.apply)
|
||||
local root = require "./root"
|
||||
local apply = require "./apply"
|
||||
|
||||
local function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
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
|
||||
return if type(value) == "function" then value() else value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
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
|
||||
|
||||
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
|
||||
destroy(node)
|
||||
end
|
||||
|
||||
push_scope(node)
|
||||
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local result = { xpcall(fn, efn, destroy) }
|
||||
local result = { xpcall(fn, debug.traceback, destroy) }
|
||||
|
||||
pop_scope()
|
||||
|
||||
if not result[1] then
|
||||
destroy()
|
||||
throw(`error while running root():\n\n{result[2]}`)
|
||||
error(`error while running root():\n\n{result[2]}`, 0)
|
||||
end
|
||||
|
||||
return destroy, unpack(result :: any, 2)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local switch = require(script.Parent.switch)
|
||||
local switch = require "./switch"
|
||||
|
||||
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T?
|
||||
local function truthy()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_source_node = graph.create_source_node
|
||||
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 node = create_source_node(initial_value)
|
||||
|
||||
return function(...): T
|
||||
local function update_source(...): T
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
|
|
@ -26,6 +24,8 @@ local function source<T>(initial_value: T): Source<T>
|
|||
update_descendants(node)
|
||||
return v
|
||||
end
|
||||
|
||||
return update_source
|
||||
end
|
||||
|
||||
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)
|
||||
|
|
|
|||
164
src/spring.luau
164
src/spring.luau
|
|
@ -1,28 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
|
||||
|
||||
--[[
|
||||
|
||||
Supported datatypes:
|
||||
- number
|
||||
- CFrame
|
||||
- Color3
|
||||
- UDim
|
||||
- UDim2
|
||||
- Vector2
|
||||
- Vector3
|
||||
- Rect
|
||||
|
||||
Unsupported datatypes:
|
||||
- bool
|
||||
- Vector2int16
|
||||
- Vector3int16
|
||||
- EnumItem
|
||||
|
||||
]]
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
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 UPDATE_RATE = 120
|
||||
local TOLERANCE = 0.0001
|
||||
|
||||
type Vec3 = Vector3
|
||||
|
||||
local function Vec3(x: number?, y: number?, z: number?)
|
||||
return Vector3.new(x, y, z)
|
||||
end
|
||||
|
||||
local ZERO = Vec3(0, 0, 0)
|
||||
local TOLERANCE = 0.001
|
||||
local TOLERANCE_VECTOR = vector.create(TOLERANCE, TOLERANCE, TOLERANCE)
|
||||
|
||||
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
|
||||
c: number, -- damping coeff
|
||||
|
||||
-- dimensions 1-3
|
||||
x0_123: Vec3,
|
||||
x1_123: Vec3,
|
||||
v_123: Vec3,
|
||||
|
||||
-- dimensions 4-6
|
||||
x0_456: Vec3,
|
||||
x1_456: Vec3,
|
||||
v_456: Vec3,
|
||||
x0_123: vector, x0_456: vector, -- current position
|
||||
x1_123: vector, x1_456: vector, -- target position
|
||||
v_123: vector, v_456: vector, -- current velocity
|
||||
|
||||
source_value: T -- current value of spring input source
|
||||
}
|
||||
|
||||
type TypeToVec6<T> = (T) -> (Vec3, Vec3)
|
||||
type Vec6ToType<T> = (Vec3, Vec3) -> T
|
||||
type SpringSettings<T> = ({
|
||||
position: T?,
|
||||
velocity: T?,
|
||||
impulse: T?
|
||||
}) -> ()
|
||||
|
||||
type TypeToVec6<T> = (T) -> (vector, vector)
|
||||
type Vec6ToType<T> = (vector, vector) -> T
|
||||
|
||||
local type_to_vec6 = {
|
||||
number = function(v)
|
||||
return Vec3(v, 0, 0), ZERO
|
||||
return vector.create(v, 0, 0), vector.zero
|
||||
end :: TypeToVec6<number>,
|
||||
|
||||
CFrame = function(v)
|
||||
return v.Position, Vec3(v:ToEulerAnglesXYZ())
|
||||
return v.Position, vector.create(v:ToEulerAnglesXYZ())
|
||||
end :: TypeToVec6<CFrame>,
|
||||
|
||||
Color3 = function(v)
|
||||
-- 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>,
|
||||
|
||||
UDim = function(v)
|
||||
return Vec3(v.Scale, v.Offset, 0), ZERO
|
||||
return vector.create(v.Scale, v.Offset, 0), vector.zero
|
||||
end :: TypeToVec6<UDim>,
|
||||
|
||||
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>,
|
||||
|
||||
Vector2 = function(v)
|
||||
return Vec3(v.X, v.Y, 0), ZERO
|
||||
return vector.create(v.X, v.Y, 0), vector.zero
|
||||
end :: TypeToVec6<Vector2>,
|
||||
|
||||
Vector3 = function(v)
|
||||
return v, ZERO
|
||||
return v, vector.zero
|
||||
end :: TypeToVec6<Vector3>,
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +113,7 @@ local vec6_to_type = {
|
|||
|
||||
local invalid_type = {
|
||||
__index = function(_, t: string)
|
||||
throw(`cannot spring type {t}`)
|
||||
error(`cannot spring type {t}`, 0)
|
||||
end
|
||||
}
|
||||
|
||||
|
|
@ -145,10 +122,10 @@ setmetatable(vec6_to_type, invalid_type)
|
|||
|
||||
-- maps spring data to its corresponding output node
|
||||
-- lifetime of spring data is tied to output node
|
||||
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
||||
setmetatable(springs, { __mode = "v" })
|
||||
local springs: { [SpringState<unknown>]: SourceNode<unknown> } = {}
|
||||
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()
|
||||
|
||||
-- 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: this does not catch all solver exploding cases
|
||||
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
|
||||
|
||||
local data: SpringData<T> = {
|
||||
local data: SpringState<T> = {
|
||||
k = k,
|
||||
c = c,
|
||||
|
||||
x0_123 = ZERO,
|
||||
x1_123 = ZERO,
|
||||
v_123 = ZERO,
|
||||
x0_123 = vector.zero,
|
||||
x1_123 = vector.zero,
|
||||
v_123 = vector.zero,
|
||||
|
||||
x0_456 = ZERO,
|
||||
x1_456 = ZERO,
|
||||
v_456 = ZERO,
|
||||
x0_456 = vector.zero,
|
||||
x1_456 = vector.zero,
|
||||
v_456 = vector.zero,
|
||||
|
||||
source_value = false :: any,
|
||||
}
|
||||
|
|
@ -187,7 +164,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
local value = source()
|
||||
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
|
||||
data.source_value = value
|
||||
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
|
||||
springs[data] = output
|
||||
return value
|
||||
end
|
||||
|
||||
|
|
@ -201,6 +178,28 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
-- set output to goal
|
||||
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(...)
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
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)
|
||||
|
||||
-- reset velocity
|
||||
data.v_123 = ZERO
|
||||
data.v_456 = ZERO
|
||||
data.v_123 = vector.zero
|
||||
data.v_456 = vector.zero
|
||||
|
||||
-- schedule spring
|
||||
springs[data] = output
|
||||
|
|
@ -222,7 +221,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
output.cache = v
|
||||
|
||||
return v
|
||||
end
|
||||
end, setter
|
||||
end
|
||||
|
||||
local function step_springs(dt: number)
|
||||
|
|
@ -263,23 +262,24 @@ local function step_springs(dt: number)
|
|||
end
|
||||
end
|
||||
|
||||
local remove_queue = {}
|
||||
|
||||
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,
|
||||
x0_456, x1_456, v_456 =
|
||||
data.x0_123, data.x1_123, data.v_123,
|
||||
data.x0_456, data.x1_456, data.v_456
|
||||
|
||||
local dx_123, dx_456 =
|
||||
x0_123 - x1_123,
|
||||
x0_456 - x1_456
|
||||
local max_difference = vector.max(
|
||||
vector.abs(x0_123 - x1_123 :: any),
|
||||
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 (v_123 + v_456 + dx_123 + dx_456).Magnitude < TOLERANCE then
|
||||
if max_difference == TOLERANCE_VECTOR then
|
||||
-- close enough to target, unshedule spring and set value to target
|
||||
table.insert(remove_queue, data)
|
||||
springs[data] = nil
|
||||
output.cache = data.source_value
|
||||
else
|
||||
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)
|
||||
end
|
||||
|
||||
for _, data in next, remove_queue do
|
||||
springs[data] = nil
|
||||
end
|
||||
|
||||
table.clear(remove_queue)
|
||||
end
|
||||
|
||||
return function()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
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 type(component) ~= "function" then
|
||||
throw "map must map a value to a function"
|
||||
error "map must map a value to a function"
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
local ok, result = pcall(component)
|
||||
local ok, result = xpcall(component, debug.traceback)
|
||||
|
||||
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(script.Parent.graph)
|
||||
local graph = require "./graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
local get_scope = graph.get_scope
|
||||
|
||||
|
|
@ -12,13 +10,13 @@ local function untrack<T>(source: () -> T): T
|
|||
local effect = scope.effect
|
||||
scope.effect = false
|
||||
|
||||
local ok, result = pcall(source)
|
||||
local ok, result = xpcall(source, debug.traceback)
|
||||
|
||||
scope.effect = effect :: () -> ()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
return result :: T
|
||||
else
|
||||
return source()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local testkit = require("test/testkit")
|
||||
local testkit = require("./testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
|
||||
local vide = require "src/init"
|
||||
local vide = require "../../vide"
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
|
|
@ -200,7 +200,7 @@ end)
|
|||
TITLE "property apply"
|
||||
|
||||
ROOT_BENCH("apply 0 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local apply = require "../src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -209,7 +209,7 @@ ROOT_BENCH("apply 0 properties", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("apply 8 properties", function()
|
||||
local apply = require "src/apply"
|
||||
local apply = require "../src/apply"
|
||||
local instance = create("Frame") {}
|
||||
|
||||
for i = 1, START(N) do
|
||||
|
|
@ -227,7 +227,7 @@ ROOT_BENCH("apply 8 properties", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("bind property", function()
|
||||
local apply = require "src/apply"
|
||||
local apply = require "../src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
local src = source(1)
|
||||
|
|
@ -242,7 +242,7 @@ ROOT_BENCH("bind property", function()
|
|||
end)
|
||||
|
||||
ROOT_BENCH("update binding", function()
|
||||
local apply = require "src/apply"
|
||||
local apply = require "../src/apply"
|
||||
|
||||
local instance = create("Frame") {}
|
||||
local src = source(1)
|
||||
|
|
@ -504,85 +504,44 @@ ROOT_BENCH(`get context (depth={depth})`, function()
|
|||
end)
|
||||
end)
|
||||
|
||||
N *= 1024
|
||||
TITLE "spring()"
|
||||
|
||||
TITLE "aggregate"
|
||||
ROOT_BENCH("spring update", function()
|
||||
local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
do
|
||||
-- the purpose of the two following benchmarks is to measure the overhead of
|
||||
-- aggregate construction
|
||||
ROOT_BENCH("set explicit mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
local src = source(0)
|
||||
|
||||
local label = create "TextLabel" {
|
||||
AnchorPoint = Vector2.new(1, 1)
|
||||
}
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(label, {
|
||||
AnchorPoint = Vector2.new(i, i)
|
||||
})
|
||||
root(function()
|
||||
for i = 1, N do
|
||||
spring(src)
|
||||
end
|
||||
|
||||
START(N)
|
||||
|
||||
src(1)
|
||||
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
|
||||
ROOT_BENCH("set aggregate mock vector2", function()
|
||||
local apply = require "src/apply"
|
||||
local Vector2 = require "test/mock".Vector2
|
||||
ROOT_BENCH("spring step", function()
|
||||
local root, source, spring = vide.root, vide.source, vide.spring
|
||||
|
||||
local label = create "TextLabel" {
|
||||
AnchorPoint = Vector2.new(1, 1)
|
||||
}
|
||||
local src = source(0)
|
||||
|
||||
for i = 1, START(N) do
|
||||
apply(label, {
|
||||
AnchorPoint = { i, i }
|
||||
})
|
||||
root(function()
|
||||
for i = 1, N do
|
||||
spring(src)
|
||||
end
|
||||
|
||||
src(1)
|
||||
|
||||
START(N)
|
||||
|
||||
vide.step(1/60)
|
||||
|
||||
return nil
|
||||
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)
|
||||
end)
|
||||
|
||||
return nil
|
||||
|
|
@ -257,40 +257,6 @@ local Vector2 = { __type = "Vector2" } :: any do
|
|||
end
|
||||
end
|
||||
|
||||
local Vector3 = { __type = "Vector3" } :: any do
|
||||
local function new(x, y, z)
|
||||
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
||||
end
|
||||
|
||||
function Vector3.new(x, y, z)
|
||||
return new(x or 0, y or 0, z or 0)
|
||||
end
|
||||
|
||||
function Vector3.__add(a, b)
|
||||
return new(a.X + b.X, a.Y + b.Y, a.Z + b.Z)
|
||||
end
|
||||
|
||||
function Vector3.__sub(a, b)
|
||||
return new(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
|
||||
end
|
||||
|
||||
function Vector3.__mul(a, b)
|
||||
return new(a.X * b, a.Y * b, a.Z * b)
|
||||
end
|
||||
|
||||
function Vector3.__unm(v)
|
||||
return new(-v.X, -v.Y, -v.Z)
|
||||
end
|
||||
|
||||
function Vector3.__eq(a, b)
|
||||
return a.X == b.X and a.Y == b.Y
|
||||
end
|
||||
|
||||
function Vector3.__index(v)
|
||||
return (v.X^2 + v.Y^2 + v.Z^2)^0.5
|
||||
end
|
||||
end
|
||||
|
||||
local UDim2 = { __type = "UDim2" } :: any do
|
||||
function UDim2.new(sx, ox, sy, oy)
|
||||
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
||||
|
|
@ -330,7 +296,6 @@ return {
|
|||
Instance = Instance :: typeof(Instance),
|
||||
Color3 = Color3 :: typeof(Color3),
|
||||
Vector2 = Vector2 :: typeof(Vector2),
|
||||
Vector3 = Vector3 :: typeof(Vector3),
|
||||
UDim2 = UDim2 :: typeof(UDim2),
|
||||
Enum = Enum :: typeof(Enum),
|
||||
typeof = typeof :: typeof(typeof)
|
||||
|
|
|
|||
|
|
@ -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 testkit = require("test/testkit")
|
||||
local vide = require "../../vide"
|
||||
local testkit = require("../test/testkit")
|
||||
|
||||
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 mock = require "test/mock"
|
||||
local mock = require "./mock"
|
||||
local Instance, Signal = mock.Instance, mock.Signal
|
||||
local Vector2, UDim2 = mock.Vector2, mock.UDim2
|
||||
|
||||
local vide = require "src/init"
|
||||
local graph = require "src/graph"
|
||||
local vide = require "../../vide"
|
||||
local graph = require "../../vide/src/graph"
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
type Map<K, V> = { [K] : V }
|
||||
|
|
@ -721,8 +721,21 @@ TEST("create()", wrap_root(function()
|
|||
local source = vide.source
|
||||
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"
|
||||
local defaults = require("src/defaults")
|
||||
local defaults = require "../src/defaults"
|
||||
local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any }
|
||||
CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel)
|
||||
CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3)
|
||||
|
|
@ -746,33 +759,35 @@ TEST("create()", wrap_root(function()
|
|||
CHECK(text.Text == "test")
|
||||
end
|
||||
|
||||
do CASE "aggregate construction"
|
||||
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"
|
||||
do CASE "nested deferred"
|
||||
local text = create "TextLabel" {
|
||||
{
|
||||
{ Text = "2" },
|
||||
Text = "1",
|
||||
|
||||
{ Text = "2" }
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(text.Text == "2")
|
||||
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"
|
||||
local frame = create "Frame"
|
||||
CHECK(frame {} ~= frame {})
|
||||
|
|
@ -981,6 +996,48 @@ TEST("create()", wrap_root(function()
|
|||
CHECK(not obj:FindFirstChild("e"))
|
||||
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"
|
||||
local wref
|
||||
|
||||
|
|
@ -1169,7 +1226,7 @@ TEST("switch()", wrap_root(function()
|
|||
end
|
||||
|
||||
do CASE "reactive stack resets after error"
|
||||
local scopes = require "src/graph".scopes
|
||||
local scopes = require "../src/graph".scopes
|
||||
local input = source(1)
|
||||
|
||||
local n0 = scopes.n
|
||||
|
|
@ -1328,7 +1385,7 @@ TEST("indexes()", wrap_root(function()
|
|||
end
|
||||
|
||||
do CASE "reactive stack resets after error"
|
||||
local scopes = require "src/graph".scopes
|
||||
local scopes = require "../src/graph".scopes
|
||||
|
||||
local input = source { 1 }
|
||||
|
||||
|
|
@ -1507,7 +1564,7 @@ TEST("values()", wrap_root(function()
|
|||
end
|
||||
|
||||
do CASE "reactive stack resets after error"
|
||||
local scopes = require "src/graph".scopes
|
||||
local scopes = require "../src/graph".scopes
|
||||
|
||||
local input = source { 1 }
|
||||
|
||||
|
|
@ -2260,7 +2317,7 @@ TEST("context()", function()
|
|||
end)
|
||||
|
||||
TEST("nested effects cases", function()
|
||||
local vide = require "src/init"
|
||||
local vide = require "../../vide"
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
local untrack = vide.untrack
|
||||
|
|
@ -2483,11 +2540,14 @@ end))
|
|||
TEST("strict", wrap_root(function()
|
||||
vide.strict = true
|
||||
|
||||
local root = vide.root
|
||||
local show = vide.show
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local indexes, values = vide.indexes, vide.values
|
||||
local untrack = vide.untrack
|
||||
|
||||
do CASE "error on derived callback yield"
|
||||
local src = source(1)
|
||||
|
|
@ -2627,6 +2687,79 @@ TEST("strict", wrap_root(function()
|
|||
|
||||
CHECK(count == 4)
|
||||
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))
|
||||
|
||||
local ok = FINISH()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue