This commit is contained in:
Alice 2024-12-29 21:38:00 +01:00
commit 63077355eb
66 changed files with 1741 additions and 1671 deletions

View file

@ -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.
:::

View file

@ -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)
```

View file

@ -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"
```
:::

View 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.
--------------------------------------------------------------------------------

View file

@ -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.
--------------------------------------------------------------------------------

View file

@ -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)
```
--------------------------------------------------------------------------------

View file

@ -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.