This commit is contained in:
Aaron Smith 2023-08-03 15:06:40 +01:00
parent 5057b7772b
commit 8e31946dda
9 changed files with 234 additions and 775 deletions

View file

@ -1,56 +1,39 @@
# Animation API # Animation API
<br/>
## spring() ## spring()
Returns a new state with an animated value of the original. Returns a new state with a dynamically animated value of the source.
### Type - ### Type
```lua ```lua
function spring<T>(state: State<T>, period: number, dampingRatio: number = 1): State<T> function spring<T>(
``` source: () -> T & Animatable,
period: number = 1,
damping_ratio: number = 1
): () -> T
### Details type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
```
The output state's value is updated every frame based on the current input state's value. - ### Details
The change is physically simulated according to a [spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion). The output state value is updated every frame based on the source state
value.
`period` is the amount of time in seconds it takes for the spring to complete one full cycle The change is physically simulated according to a
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
`dampingRatio` is relared to the amount of resistant force applied to the spring. `period` is the amount of time in seconds it takes for the spring to
complete one full oscillation.
- \>1 = Overdamped (Not currently supported) `damping_ratio` is the amount of resistance applied to the spring.
- 1 = Critically damped
- <1 = Underdamped
- 0 = Undamped
Velocity is conserved between input state updates for smooth animation. - \>1 = Overdamped (not currently supported).
- 1 = Critically damped - reaches target without any overshoot.
- <1 = Underdamped - reaches target with some overshoot.
- 0 = Undamped - never stabilizes, oscillates forever.
### Example Velocity is conserved between source state updates for smooth animation.
```lua --------------------------------------------------------------------------------
local state = wrap(1)
local springed = spring(state, 1, 1)
```
<details><summary>Example of an animated counter</summary>
```lua
local count = wrap(1000)
local function Counter(props)
local tweenedCount = spring(count, 0.5, 1)
return create("TextLabel") {
Text = "Count: " .. tweenedCount
}
end
```
</details>
-------------------------------------------------------------------

View file

@ -6,320 +6,76 @@
Creates a new UI element, applying any given properties. Creates a new UI element, applying any given properties.
### Type - ### Type
```lua ```lua
function create(classNameOrInstance: string | Instance): (properties: Map<string, any>) -> Instance function create(class: string): (Properties) -> Instance
``` function create(instance: Instace): (Properties) -> Instance
### Details type Properties = Map<string|number, any>
```
The function can take either a `string` or an `Instance` as its first argument. - ### Details
- If given a `string`, a new instance with the string class name will be created with default properties already applied. The function can take either a `string` or an `Instance` as its first argument.
- If given an `Instance`, a new instance that is a clone of the given instance will be created.
This returns another function that is used to apply any properties to the new instance. - If given a `string`, a new instance with the same class name will be created.
- If given an `Instance`, a new instance that is a clone of the given instance
will be created.
### Example This returns another function that is used to apply any properties to the new
instance.
```lua - ### Property setting rules
local frame = create("Frame") {
- If a table value is another table, that nested table is processed so that
any properties inside that table are also applied to the instance just
like the outer table.
- If a table index is a string:
- If its value is a function then it will either bind that property to
a state or connect it if the property type is a `RBXScriptSignal`.
- If the value is not a function then the property will be set to that
value.
- If a table index is a number:
- If its value is a function then it will parent any instances returned by
that function as children.
- If its value is an instance then it will be parented to the instance.
- ### Example
Basic element creation.
```lua
local frame = create "Frame" {
Name = "NewFrame", Name = "NewFrame",
Position = UDim2.fromScale(1, 0) Position = UDim2.fromScale(1, 0)
}
-- creates a clone of `frame` with new properties applied.
local frame2 = create(frame) {
Size = UDim2.fromOffset(50, 100)
}
```
-------------------------------------------------------------------
<br/>
## apply()
Applies any given properties to a given instance.
### Type
```lua
function apply(instance: Instance): (properties: Map<string, any>) -> Instance
```
### Details
Applies properties in the same manner as `create` for already existing existances.
Can use symbols and bind state just like `create`.
### Example
```lua
local frame = Instance.new("Frame")
apply(frame) {
Position = UDim2.fromScale(1, 0)
}
```
-------------------------------------------------------------------
<br/>
## Layout
Symbol used to pass layout properties to elements.
### Type
```lua
type Layout = Symbol
type LayoutProps = {
[Symbol] = {
-- These are all properties considered to be "layout properties"
AnchorPoint: Prop<Vector2>?;
LayoutOrder: Prop<number>?;
Position: Prop<UDim2>?;
Rotation: Prop<number>?;
Size: Prop<UDim2>?;
SizeConstraint: Prop<Enum.SizeConstraint>?;
Visible: Prop<boolean>?;
ZIndex: Prop<number>?;
} }
} ```
type Prop<T> = T | State<T> A component using property nesting/grouping.
```
### Details ```lua
type Layout = {
The primary purpose of this symbol is to enable easy passthrough of layout properties Layout = {
through user-defined component hierarchies. Position: UDim2?,
Size: UDim2?,
It is recommended to only set layout properties using the `Layout` symbol when using AnchorPoint: Vector2?
your own components.
### Example
```lua
local function BlackFrame(props)
return create("Frame") {
BackgroundColor3 = Color3.new(0, 0, 0),
[Layout] = props[Layout]
}
end
BlackFrame {
[Layout] = {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(100, 50)
}
}
```
-------------------------------------------------------------------
<br/>
## Children
Symbol used to pass child instances to elements.
### Type
```lua
type Children = Symbol
type ChildrenProps = {
[Symbol] = ChildrenProp
}
type ChildrenProp = Prop<Instance> | Array<ChildrenProp>
```
### Details
This symbol is flexible in the way that children can be passed in the form of nested arrays.
Children can also be assigned using a state binding.
### Example
```lua
create("Frame") {
-- all of the below are valid methods of assigning children
[Children] = create("TextLabel") {},
[Children] = {
create("TextLabel") {},
create("TextLabel") {},
},
[Children] = {
create("TextLabel") {},
{
create("TextLabel") {},
} }
} }
}
```
------------------------------------------------------------------- type Children = {
Children = Array<Instance>
}
<br/> function Background(props: Layout & Children & {
Color: Color3
## Event })
return create "Frame" {
Symbol used to connect callbacks to instance events. BackgroundColor3 = Color,
props.Layout,
### Type props.Children
}
```lua
type Event = Map<string, Symbol>
type EventProps = {
[Symbol] = Prop<(...unknown) -> ()>
}
```
### Details
The `Event` symbol can be indexed to get symbols to connect to specific events.
Event parameters are passed into the callback.
When callbacks are connected by binding to a state,
connections are automatically disconnected when the state changes.
### Example
```lua
create("TextButton") {
[Event.Activated] = function()
print("Clicked")
end end
} ```
```
------------------------------------------------------------------- --------------------------------------------------------------------------------
<br/>
## Changed
Symbol used to connect callbacks to instance property changed events.
### Type
```lua
type Changed = Map<string, Symbol>
type ChangedProps = {
[Symbol] = Prop<(...unknown) -> ()>
}
```
### Details
The `Changed` symbol can be indexed to get symbols to connect to specific events just like `Event`.
Event parameters are passed into the callback.
When callbacks are connected by binding to a state,
connections are automatically disconnected when the state changes.
### Example
```lua
create("TextBox") {
[Changed.Text] = function()
print("New text entered")
end
}
```
-------------------------------------------------------------------
<br/>
## Bind
Symbol used to bind states to instance properties.
### Type
```lua
type Bind = Map<string, Symbol>
type BindProps = {
[Symbol] = State<any>
}
```
### Details
The `Bind` symbol can be indexed to bind specific properties just like `Event`.
Sets the given state value to the instance property value immediately after instance creation.
When an instance property is changed, the value of the given state will automatically
be set to the new property. Effectively a shorthand for connecting a property changed event
to set state values.
### Example
```lua
local text = wrap()
local box = create("TextBox") {
[Bind.Text] = text
}
box.Text = "New text"
print(text.Value) -- "New text"
```
-------------------------------------------------------------------
<br/>
## Created
Symbol used to run a callback once immediately after instance creation.
### Type
```lua
type Created = Symbol
type CreatedProps = {
[Symbol] = (Instance) -> ()
}
```
### Details
The instance being defined with the `Created` symbol is passed as the first
argument to the callback.
### Example
```lua
local frame
create("Frame") {
Name = "Background",
[Created] = function(instance)
frame = instance
end
}
print(frame.Name) -- "Background"
```
-------------------------------------------------------------------

View file

@ -2,248 +2,167 @@
<br/> <br/>
## wrap() ## source()
Wraps and returns any given values with reactive state objects. Creates a new source state with the given value.
### Type - ### Type
```lua ```lua
function wrap<T>(value: T): State<T> function source<T>(value: T): (T?) -> T
function wrap(value: ...unknown): ...State<any> ```
type State<T> = { - ### Details
Value: T,
value: T
}
```
### Details Calling the returned state with no arguments will return its stored value,
calling with arguments will set a new value.
The state object has a single mutable field `.Value`. Reading from the state from within any reactive scope will cause changes
to that state to be tracked and anything depending on it to update.
Read operations to `.Value` are tracked and write operations can trigger - ### Example
dependency updates and watchers.
### Example ```lua
local count = source(0)
```lua count() -- 0
local count = wrap(0)
print(count.Value) -- 0 count(count() + 1) -- 1
```
count.Value += 1 --------------------------------------------------------------------------------
print(count.Value) -- 1
```
-------------------------------------------------------------------
<br/>
## derive() ## derive()
Derives a new reactive state object from an existing state object. Derives a new state from existing states.
### Type - ### Type
```lua ```lua
function derive<T>( function derive<T>(source: () -> T): () -> T
(from) -> T, ```
cleanup: (value: T) -> ()?
): State<T>
function from<T>(T | State<T>): T - ### Details
```
### Details The derived state will have its value recalculated when any source state it
derives from is updated.
The derived state will have its value recalculated when any state it derives from is updated. Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
Takes a callback that is immediately run to determine what states are being referenced. Only states referenced in the immediate function scope can trigger updates. Takes a callback that is immediately run to determine what states are being
referenced.
The state object returned by this function is readonly. > ⚠️ Non-yielding.
Has an optional cleanup parameter which takes a function that is called with the old value any - ### Example
time the derived state recalculates a value.
An optional utility function is passed as the first argument to the callback, if given a state, the value of the state will be returned (changes to this state still triggers updates unlike `unwrap`), if given a value the value is returned. ```lua
local count = wrap(0)
local text = derive(function() return `count: {count()}` end)
> ⚠️ The callback cannot yield. text() -- "count: 0"
### Example count(1)
```lua text() -- "count: 1"
local count = wrap(0) ```
local text = derive(function() return "Count: "..count.Value end)
print(text.Value) -- "Count: 0" --------------------------------------------------------------------------------
count.Value += 1 ## map()
print(text.Value) -- "Count: 1" Maps each value in a table state to a new table state.
```
```lua - ### Type
local count = wrap(0)
local text = derive(function(from)
return "Count: "..from(count)
end)
```
A shorthand method for deriving states also exists, following example is equivalent to the above: ```lua
function map<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Map<KI, VO>
```lua - ### Details
local count = wrap(0)
local text = "Count: "..count -- all binary operators are supported
```
------------------------------------------------------------------- The transform function is called only ever *once* for each index in the
source table. The first argument is a state containing the index's value and
the second argument is just the index.
<br/> Anytime a new index is added, the transform function will be called again for
that new index.
## foreach() Anytime an existing index changes, the transform function is not rerun,
instead the passed state for that index will update, causing anything
depending on it to update too.
Derives a new state object from an existing state object. Returns a state containing the mapped key-value pairs.
Designed to work specifically with table states.
Also works with non state tables. > ⚠️ Non-yielding.
### Type - ### Example
```lua ```lua
function foreach<KO, VO>( -- number as first arg type Item = {
i: number, name: string,
transform: (key: number) -> (KO, VO), icon: number
cleanup: (KO, VO) -> ()? }
): Map<KO, VO>
function foreach<KI, KO, VI, VO>( -- table as first arg local items = source {} :: () -> Array<Item>
table: Map<KI, VI>,
transform: (key: KI, value: VI) -> (KO, VO),
cleanup: (KO, VO) -> ()?
): Map<KO, VO>
function foreach<KI, KO, VI, VO>( -- state as first arg local displays = map(numbers, function(item, i)
state: State<Map<KI, VI>>, return ItemDisplay {
transform: (key: KI, value: VI) -> (KO, VO), Name = function()
cleanup: (KO, VO) -> ()? return item().name
): State<Map<KO, VO>> end,
```
### Details Image = function()
return "rbxassetid://" .. item().icon
end,
When the state being derived from is updated, the derived state will LayoutOrder = i
recompute by applying its transform function to each key-value pair. }
end)
```
Will only be recomputed if the corresponding key differs between updates. --------------------------------------------------------------------------------
Has an optional cleanup function to cleanup the old key and value.
> ⚠️ The transform function cannot yield.
### Example
```lua
local numbers = wrap { 1, 2, 3 }
local plusOne = foreach(numbers, function(i, v)
return i, v + 1
end)
print(plusOne.Value) -- { [1]: 2, [2]: 3, [3]: 4 }
-- note that assignment must take place to trigger reactive updates.
-- modifying the value without assignment `numbers.Value[2] = 5` will not trigger updates.
numbers.Value = { 1, 5, 3 }
print(plusOne.Value) -- { [1]: 2, [2]: 6, [3]: 4 }
```
-------------------------------------------------------------------
<br/>
## match()
Derives a new state object from an existing state object.
Similar to switch statements in other languages.
### Type
```lua
function match<K, V>(value: K): (transform: Map<K, V>) -> V
function Match<K, V>(state: State<K>): (transform: Map<K, V>) -> State<V>
```
### Details
When the state being derived from is updated, the derived state will
recompute by using the input value as a key to map to an output value.
### Example
```lua
local state = wrap(true)
local matched = match(state) {
[true] = 1,
[false] = 0
}
print(matched.Value) -- 1
state.Value = false
print(matched.Value) -- 0
```
-------------------------------------------------------------------
<br/>
## watch() ## watch()
Runs a callback on state change. Runs a callback on state change.
### Type - ### Type
```lua ```lua
function watch(callback: () -> Cleanup?): Unwatch function watch(callback: () -> ()): Unwatch
type Cleanup = () -> () type Unwatch = () -> ()
type Unwatch = () -> () ```
```
### Details - ### Details
The callback is ran immediately to determine what states to watch. The callback is ran immediately to determine what states are referenced.
Any time a state read in the watch callback is changed, the watcher callback will be deferred Any time a state referenced in the callback is changed, the callback will be
to the end of the resumption cycle and ran. reran.
Only states in the immediate function scope can trigger the watch callback. Also returns a function that when called, stops the watcher immediately.
Watchers are run *before* UI properties are updated. > ⚠️ Non yielding.
The callback can return an optional cleanup function that is run each time the watcher is rerun. - ### Example
Also returns a function that when called, stops the watcher immediately (also runs cleanup if any was given). ```lua
local state = wrap(1)
> ⚠️ The callback cannot yield. watch(function()
### Example
```lua
local state = wrap(1)
watch(function()
print(state.Value) print(state.Value)
end) end)
-- prints 1 -- prints 1
state.Value += 1 state.Value += 1
-- prints 2 -- prints 2
``` ```
------------------------------------------------------------------- --------------------------------------------------------------------------------

View file

@ -1,152 +1,27 @@
# Reactivity API: Utility # Reactivity API: Utility
<br/> ## cleanup()
## isState() Runs a callback anytime a reactive scope is re-ran.
Determines if a given value is a state object or not. - ### Type
### Type ```lua
function cleanup(callback: () -> ())
```
```lua - ### Example
function isState(value: unknown): boolean
```
### Example ```lua
local data = source(1)
```lua watch(function()
local value = wrap() local label = create "TextLabel" { Text = data }
print(isState(value)) -- true cleanup(function()
label:Destroy()
end)
end)
```
value = 0 --------------------------------------------------------------------------------
print(isState(value)) -- false
```
-------------------------------------------------------------------
<br/>
## unwrap()
Unwraps a state and returns its stored value.
### Type
```lua
function unwrap<T>(value: T | State<T>): T
```
### Details
If given a state, the state's stored value will be returned.
Unwrapping a state within a derived callback will not trigger updates.
Can be given a non-state value, in which case the same value will just be returned.
### Example
```lua
local state = wrap(1)
print(unwrap(state)) -- 1
print(unwrap(1)) -- 1
```
-------------------------------------------------------------------
<br/>
## readonly()
Creates a new derived state with the same value as the state being derived from.
Used to create readonly states.
### Type
```lua
function readonly<T>(state: State<T>): State<T>
```
### Example
```lua
local count = wrap(1)
local read = readonly(count)
print(read.Value) -- 1
count.Value += 1
print(read.Value) -- 2
read.Value += 1 -- error
```
-------------------------------------------------------------------
<br/>
## mutate()
Mutates a given state's value and updated any derived states.
### Type
```lua
function mutate<T>(value: T | State<T>): T
```
### Details
Since states only update derived states if a new value is set (tables are compared by reference),
this function serves as a way to trigger derived state updates if a state's value is not changed but
instead mutated.
Can also take non-state as an argument.
### Example
```lua
local state = wrap { Count = 1 }
local derived = derive(function()
return state.Value.Count
end)
mutate(state, function(value)
value.Count += 1
end)
print(derived.Value) -- 2
```
<details><summary>Motivation for this function</summary>
```lua
local state = wrap { Count = 1 }
local derived = derive(function()
return state.Value.Count
end)
state.Value.Count += 1
print(derived.Value) -- still 1 because `state.Value` was never set with a new value so change wasn't detected
local value = state.Value
value.Count += 1
state.Value = value
print(derived.Value) -- still 1 because although `state.Value` was set, when the new value set was compared,
-- it was still the same as the previous (tables are compared by reference not their contents)
-- and so no update was made
```
</details>
-------------------------------------------------------------------

View file

@ -1,57 +1,27 @@
# Strict Mode # Strict Mode
<br/> Vide has a special mode called "strict mode" which is used for debugging.
## strict The purpose of strict mode is to help ensure stateful code is *pure*
(deterministic and free from side effects) or if there are side-effects, that
they are cleaned up correctly.
A flag that users can set to enable or disable strict mode (disabled by default). Vide is set to strict by doing:
### Type ```lua
local vide = require(path_to_vide)
```ts vide.strict = true
boolean strict = false
``` ```
### Details
The purpose of strict mode is to help ensure stateful code is *pure* (deterministic and free from side effects).
Setting this flag is global for all scripts requiring the same instance of the Vide module.
What strict mode will do: What strict mode will do:
1. Run derived callbacks twice when calculating state value. 1. Run derived callbacks twice when re-evaluating.
2. Run watcher callbacks twice each time state changes. 2. Run watcher callbacks twice when a state changes.
3. Throw an error if a derived callback yields. 3. Throw an error if yields occur where they are not allowed.
4. Throw an error if a watcher callback yields. 4. Checks for `map()` returning primitive values.
5. Better error reporting and stack traces.
It is recommend to develop UI with strict mode set to `true` It is recommend to develop UI with strict mode and to disable it when pushing to
and to set it back to false when pushing to production. production.
Using strict mode will help identify potential non-deterministic code and side-effects by running code --------------------------------------------------------------------------------
multiple times in places where it would only run once.
Strict mode will also ensure that watcher side effects are self contained in the sense that they clean themselves up
properly when ran multiple times in quick succession, in case any asynchronous operation is performed.
Yielding within derived or watcher callbacks can cause undefined behavior as the reactive graph is not designed
to work with asynchronous code. Strict mode can identify and throw an error when asynchronous code is detected.
This isn't done during runtime as these checks are computationally expensive.
### Example
```lua
vide.strict = true -- this only needs to be done once, preferably in the first module to require Vide
local state = wrap()
watch(function()
local cleanup = doAsyncOperation(state.Value)
return function()
cleanup()
end
end)
state.Value = 1 -- this will cause the watcher to be ran twice, identifying if cleanup occurs properly
```

View file

@ -1,55 +0,0 @@
# Types API
<br/>
## State\<T>
A type representing a Vide state object.
### Type
```lua
type State<T> = {
Value: T,
value: T
}
```
### Example
```lua
local state: State<number> = wrap(1)
local derived: State<string> = "Count: " .. state
```
-------------------------------------------------------------------
<br/>
## Prop\<T>
A utility type representing a union of a value and a state.
### Type
```lua
type Prop<T> = T | State<T>
```
### Example
```lua
type BackgroundProps = {
Position: Prop<UDim2>,
Size: Prop<UDim2>
}
local function Background(props: { Position: Prop<UDim2> })
return create("Frame") {
Position = props.Position
Size = props.Size
}
end
```
-------------------------------------------------------------------

View file

@ -36,8 +36,14 @@ local vide = {
-- runtime -- runtime
step = function(dt: number) step = function(dt: number)
-- debug.profilebegin("VIDE STEP")
-- debug.profilebegin("VIDE SPRING")
update_springs(dt) update_springs(dt)
-- debug.profileend()
-- debug.profilebegin("VIDE GARBAGE CLEANUP")
clean_garbage() clean_garbage()
-- debug.profileend()
-- debug.profileend()
end end
} }

View file

@ -3,18 +3,20 @@ if not game then script = (require :: any) "test/wrap-require" end
--[[ --[[
Supported datatypes: Supported datatypes:
*number - number
!bool - CFrame
*CFrame - Color3
?Rect - UDim
*Color3 - UDim2
*UDim - Vector2
*UDim2 - Vector3
*Vector2
!Vector2int16 Unsupported datatypes:
*Vector3 - bool
!Vector3int16 - Rect
!EnumItem - Vector2int16
- Vector3int16
- EnumItem
]] ]]

View file

@ -1,5 +1,8 @@
# todo # todo
- strict mode
- warn when map returns primitive
- Implement from solid - Implement from solid
- onCleanup - onCleanup
- Index - Index