mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Initial commit
This commit is contained in:
commit
e76234feb5
52 changed files with 5235 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.luau linguist-language=Lua
|
||||||
30
.github/workflows/unit-test.yml
vendored
Normal file
30
.github/workflows/unit-test.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
name: unit-test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- src/**
|
||||||
|
- test/**
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- src/**
|
||||||
|
- test/**
|
||||||
|
jobs:
|
||||||
|
unit-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Luau zip
|
||||||
|
uses: robinraju/release-downloader@v1.6
|
||||||
|
with:
|
||||||
|
repository: Roblox/luau
|
||||||
|
latest: true
|
||||||
|
fileName: luau-ubuntu.zip
|
||||||
|
out-file-path: bin
|
||||||
|
|
||||||
|
- name: Unzip Luau
|
||||||
|
run: unzip bin/luau-ubuntu.zip -d bin
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: bin/luau test/tests.luau
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
sourcemap.json
|
||||||
|
.vscode
|
||||||
|
_local
|
||||||
6
.luaurc
Normal file
6
.luaurc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"languageMode": "strict",
|
||||||
|
"lint": { "BuiltinGlobalWrite": false, "UnknownGlobal": false },
|
||||||
|
"globals": [ "aa" ]
|
||||||
|
}
|
||||||
|
|
||||||
78
CHANGELOG.md
Normal file
78
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.4.0] - 2023-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Component grouping and `Registry:group()`.
|
||||||
|
- Method `View:use()`.
|
||||||
|
- Constant `ecr.null`.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Method `View:include()` and `Observer:include()`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `Registry:add()` not firing `Registry:added()` signals.
|
||||||
|
- Mismatch between argument list and values returned in multi-typed observers.
|
||||||
|
- Observers not garbage collecting after calling `Observer:disconnect()`.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Connection firing speed by ~70%.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.3.0] - 2023-01-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Overload for `Registry:create()` to create an entity with a given identifier.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Registry signals no longer pass the registry as the first argument to listeners.
|
||||||
|
- Observers no longer track entities with removed components.
|
||||||
|
- Method `Registry:entities()` now creates and returns an array of only valid entities.
|
||||||
|
- Function `ecr.registry()` can no longer pre-allocate memory.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Method `Registry:capacity()`.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Double-type view iteration speed by ~100%.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.0] - 2022-12-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Method `View:include()` and `Observer:include()`.
|
||||||
|
- Method `Registry:patch()`.
|
||||||
|
- Method `Registry:add()` and optional default parameter for `ecr.component()`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Behavior `for ... in View do` now behaves the same as `for ... in View:each() do`.
|
||||||
|
- Signal diconnect API (Signal now returns a connection object to call disconnect on).
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Entity creation and release speed by ~100%.
|
||||||
|
- Multi-type view iteration speed by ~60%.
|
||||||
|
|
||||||
|
## [0.1.0] - 2022-11-16
|
||||||
|
|
||||||
|
- Initial release
|
||||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 centau
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
48
README.md
Normal file
48
README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Vide
|
||||||
|
|
||||||
|
Vide is a declarative, reactive Luau library for building user interfaces on Roblox.
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### [Reactivity: Core](docs/api/reactivity-core)
|
||||||
|
|
||||||
|
- [wrap()](docs/api/reactivity-core#wrap)
|
||||||
|
- [derive()](docs/api/reactivity-core#derive)
|
||||||
|
- [foreach()](docs/api/reactivity-core#wrap)
|
||||||
|
- [match()](docs/api/reactivity-core#match)
|
||||||
|
- [watch()](docs/api/reactivity-core#watch)
|
||||||
|
|
||||||
|
### [Reactivity: Utility](docs/api/reactivity-utility)
|
||||||
|
|
||||||
|
- [isState()](docs/api/reactivity-utility#isState)
|
||||||
|
- [unwrap()](docs/api/reactivity-utility#unwrap)
|
||||||
|
- [readonly()](docs/api/reactivity-utility#readonly)
|
||||||
|
- [mutate()](docs/api/reactivity-utility#mutate)
|
||||||
|
|
||||||
|
### [Element Creation](docs/api/creation)
|
||||||
|
|
||||||
|
- [create()](docs/api/creation#create)
|
||||||
|
- [apply()](docs/api/creation#apply)
|
||||||
|
- [Layout](docs/api/creation#Layout)
|
||||||
|
- [Children](docs/api/creation#Children)
|
||||||
|
- [Event](docs/api/creation#Event)
|
||||||
|
- [Changed](docs/api/creation#Changed)
|
||||||
|
- [Bind](docs/api/creation#Bind)
|
||||||
|
- [Created](docs/api/creation#Created)
|
||||||
|
|
||||||
|
### [Animation](docs/api/animation)
|
||||||
|
|
||||||
|
- [spring()](docs/api/animation#spring)
|
||||||
|
|
||||||
|
### [Types](docs/api/types)
|
||||||
|
|
||||||
|
- [State<T>](docs/api/types#State)
|
||||||
|
- [Prop<T>](docs/api/types#Prop)
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## Tutorials
|
||||||
|
|
||||||
|
### [Crash Course](docs/tutorials/crash-course)
|
||||||
|
|
||||||
|
### [Reactive Graph](docs/tutorials/reactive-graph)
|
||||||
7
aftman.toml
Normal file
7
aftman.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This file lists tools managed by Aftman, a cross-platform toolchain manager.
|
||||||
|
# For more information, see https://github.com/LPGhatguy/aftman
|
||||||
|
|
||||||
|
# To add a new tool, add an entry to this table.
|
||||||
|
[tools]
|
||||||
|
rojo = "rojo-rbx/rojo@7.2.1"
|
||||||
|
# rojo = "rojo-rbx/rojo@6.2.0"
|
||||||
4
default.project.json
Normal file
4
default.project.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "vide",
|
||||||
|
"tree": { "$path": "src" }
|
||||||
|
}
|
||||||
56
docs/api/animation.md
Normal file
56
docs/api/animation.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Animation API
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## spring()
|
||||||
|
|
||||||
|
Returns a new state with an animated value of the original.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function spring<T>(state: State<T>, period: number, dampingRatio: number = 1): State<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The output state's value is updated every frame based on the current input state's value.
|
||||||
|
|
||||||
|
The change is physically simulated according to a [spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
||||||
|
|
||||||
|
`period` is the amount of time in seconds it takes for the spring to complete one full cycle
|
||||||
|
|
||||||
|
`dampingRatio` is relared to the amount of resistant force applied to the spring.
|
||||||
|
|
||||||
|
- \>1 = Overdamped (Not currently supported)
|
||||||
|
- 1 = Critically damped
|
||||||
|
- <1 = Underdamped
|
||||||
|
- 0 = Undamped
|
||||||
|
|
||||||
|
Velocity is conserved between input state updates for smooth animation.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```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>
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
325
docs/api/creation.md
Normal file
325
docs/api/creation.md
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Element Creation API
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## create()
|
||||||
|
|
||||||
|
Creates a new UI element, applying any given properties.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function create(classNameOrInstance: string | Instance): (properties: Map<string, any>) -> Instance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The function can take either a `string` or an `Instance` as its first argument.
|
||||||
|
|
||||||
|
- If given a `string`, a new instance with the string class name will be created with default properties already applied.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local frame = create("Frame") {
|
||||||
|
Name = "NewFrame",
|
||||||
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The primary purpose of this symbol is to enable easy passthrough of layout properties
|
||||||
|
through user-defined component hierarchies.
|
||||||
|
|
||||||
|
It is recommended to only set layout properties using the `Layout` symbol when using
|
||||||
|
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") {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## Event
|
||||||
|
|
||||||
|
Symbol used to connect callbacks to instance events.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
<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"
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
249
docs/api/reactivity-core.md
Normal file
249
docs/api/reactivity-core.md
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
# Reactivity API: Core
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## wrap()
|
||||||
|
|
||||||
|
Wraps and returns any given values with reactive state objects.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function wrap<T>(value: T): State<T>
|
||||||
|
function wrap(value: ...unknown): ...State<any>
|
||||||
|
|
||||||
|
type State<T> = {
|
||||||
|
Value: T,
|
||||||
|
value: T
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The state object has a single mutable field `.Value`.
|
||||||
|
|
||||||
|
Read operations to `.Value` are tracked and write operations can trigger
|
||||||
|
dependency updates and watchers.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local count = wrap(0)
|
||||||
|
|
||||||
|
print(count.Value) -- 0
|
||||||
|
|
||||||
|
count.Value += 1
|
||||||
|
|
||||||
|
print(count.Value) -- 1
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## derive()
|
||||||
|
|
||||||
|
Derives a new reactive state object from an existing state object.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function derive<T>(
|
||||||
|
(from) -> T,
|
||||||
|
cleanup: (value: T) -> ()?
|
||||||
|
): State<T>
|
||||||
|
|
||||||
|
function from<T>(T | State<T>): T
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The derived state will have its value recalculated when any state it derives from is updated.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The state object returned by this function is readonly.
|
||||||
|
|
||||||
|
Has an optional cleanup parameter which takes a function that is called with the old value any
|
||||||
|
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.
|
||||||
|
|
||||||
|
> ⚠️ The callback cannot yield.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local count = wrap(0)
|
||||||
|
local text = derive(function() return "Count: "..count.Value end)
|
||||||
|
|
||||||
|
print(text.Value) -- "Count: 0"
|
||||||
|
|
||||||
|
count.Value += 1
|
||||||
|
|
||||||
|
print(text.Value) -- "Count: 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
local count = wrap(0)
|
||||||
|
local text = "Count: "..count -- all binary operators are supported
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## foreach()
|
||||||
|
|
||||||
|
Derives a new state object from an existing state object.
|
||||||
|
Designed to work specifically with table states.
|
||||||
|
|
||||||
|
Also works with non state tables.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function foreach<KO, VO>( -- number as first arg
|
||||||
|
i: number,
|
||||||
|
transform: (key: number) -> (KO, VO),
|
||||||
|
cleanup: (KO, VO) -> ()?
|
||||||
|
): Map<KO, VO>
|
||||||
|
|
||||||
|
function foreach<KI, KO, VI, VO>( -- table as first arg
|
||||||
|
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
|
||||||
|
state: State<Map<KI, VI>>,
|
||||||
|
transform: (key: KI, value: VI) -> (KO, VO),
|
||||||
|
cleanup: (KO, VO) -> ()?
|
||||||
|
): State<Map<KO, VO>>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
When the state being derived from is updated, the derived state will
|
||||||
|
recompute by applying its transform function to each key-value pair.
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
Runs a callback on state change.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function watch(callback: () -> Cleanup?): Unwatch
|
||||||
|
|
||||||
|
type Cleanup = () -> ()
|
||||||
|
type Unwatch = () -> ()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
The callback is ran immediately to determine what states to watch.
|
||||||
|
|
||||||
|
Any time a state read in the watch callback is changed, the watcher callback will be deferred
|
||||||
|
to the end of the resumption cycle and ran.
|
||||||
|
|
||||||
|
Only states in the immediate function scope can trigger the watch callback.
|
||||||
|
|
||||||
|
Watchers are run *before* UI properties are updated.
|
||||||
|
|
||||||
|
The callback can return an optional cleanup function that is run each time the watcher is rerun.
|
||||||
|
|
||||||
|
Also returns a function that when called, stops the watcher immediately (also runs cleanup if any was given).
|
||||||
|
|
||||||
|
> ⚠️ The callback cannot yield.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local state = wrap(1)
|
||||||
|
|
||||||
|
watch(function()
|
||||||
|
print(state.Value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- prints 1
|
||||||
|
|
||||||
|
state.Value += 1
|
||||||
|
|
||||||
|
-- prints 2
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
152
docs/api/reactivity-utility.md
Normal file
152
docs/api/reactivity-utility.md
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
# Reactivity API: Utility
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## isState()
|
||||||
|
|
||||||
|
Determines if a given value is a state object or not.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function isState(value: unknown): boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local value = wrap()
|
||||||
|
|
||||||
|
print(isState(value)) -- true
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
57
docs/api/strict.md
Normal file
57
docs/api/strict.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Strict Mode
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## strict
|
||||||
|
|
||||||
|
A flag that users can set to enable or disable strict mode (disabled by default).
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
|
||||||
|
1. Run derived callbacks twice when calculating state value.
|
||||||
|
2. Run watcher callbacks twice each time state changes.
|
||||||
|
3. Throw an error if a derived callback yields.
|
||||||
|
4. Throw an error if a watcher callback yields.
|
||||||
|
|
||||||
|
It is recommend to develop UI with strict mode set to `true`
|
||||||
|
and to set it back to false when pushing to 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
|
||||||
|
```
|
||||||
55
docs/api/types.md
Normal file
55
docs/api/types.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
337
docs/tut/crash-course.md
Normal file
337
docs/tut/crash-course.md
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
# Vide Crash Course
|
||||||
|
|
||||||
|
Hello! This is a brief tutorial designed to give you a quick runthrough of the usage of Vide.
|
||||||
|
|
||||||
|
Vide is inspired by the popular libraries Vue and Fusion.
|
||||||
|
|
||||||
|
- Note that this tutorial assumes that you are familiar with Luau and the Roblox UI system.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Creating UI Instances
|
||||||
|
|
||||||
|
In Vide, it is intended to create all UI instances through code.
|
||||||
|
|
||||||
|
Instances are created using [`vide.create`](../api/creation#create).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local vide = require(...)
|
||||||
|
local create = vide.create
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local frame = create("Frame") {
|
||||||
|
Name = "Background",
|
||||||
|
Position = UDim2.fromScale(0.5, 0.5)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The function returns a constructor for a given class which then takes a table of properties to assign to create a new instance for that class.
|
||||||
|
|
||||||
|
Sometimes you want to do more than setting properties, such as setting children or connecting to events.
|
||||||
|
Vide uses special keys called *symbols* which provide unique functionality like the above mentioned.
|
||||||
|
|
||||||
|
Children can be assigned to instances using the `Children` symbol.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local Children = vide.Children
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local screenGui = create("ScreenGui") {
|
||||||
|
Parent = game.StarterGui,
|
||||||
|
|
||||||
|
[Children] = create("Frame") {
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
Position = UDim2.fromScale(0.5, 0.5),
|
||||||
|
Size = UDim2.fromScale(0.4, 0.7),
|
||||||
|
|
||||||
|
[Children] = {
|
||||||
|
create("TextLabel") {
|
||||||
|
Text = "Hi"
|
||||||
|
},
|
||||||
|
|
||||||
|
create("TextLabel") {
|
||||||
|
Text = "Bye"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we import the symbol [`vide.Children`](../api/creation#Children).
|
||||||
|
|
||||||
|
This symbol can accept an instance, an array of instances and nested arrays of instances.
|
||||||
|
All given instances will be parented to the instance the symbol was used on.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Connecting To Events
|
||||||
|
|
||||||
|
Built-in instance events and property changed events can be connected to using two other symbols, [`vide.Event`](../api/creation#Event) and [`vide.Changed`](../api/creation#Changed).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local Event = vide.Event
|
||||||
|
local Changed = vide.Changed
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local textBox = create("TextBox") {
|
||||||
|
PlaceholderText = "Enter text",
|
||||||
|
|
||||||
|
[Event.Focused] = function(...)
|
||||||
|
print("User is focusing on text box")
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Changed.Text] = function(newText)
|
||||||
|
print("New text: " .. newText)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of these symbols can be indexed into to get a specific symbol for an event to connect to.
|
||||||
|
The callback function for `Event` receives any event-specific arguments and the callback function for
|
||||||
|
`Changed` receives the new property value as the only argument (unlike `Instance:GetPropertyChangedSignal()`).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## State
|
||||||
|
|
||||||
|
*State* is the condition something is in at a specific time. The state of a program is simply the data it contains at some timepoint.
|
||||||
|
|
||||||
|
The purpose of all UI is to take some state and reflect that state visually.
|
||||||
|
|
||||||
|
In Vide, UI state is represented using special objects simply called *state*.
|
||||||
|
|
||||||
|
A state object in Vide can be created using [`vide.wrap`](../api/reactivity-core#wrap).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local wrap = vide.wrap
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local isVisible = wrap(false)
|
||||||
|
|
||||||
|
local image = create("ImageLabel") {
|
||||||
|
Image = "rbxassetid://xxx",
|
||||||
|
Visible = isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
while true do
|
||||||
|
wait(1)
|
||||||
|
isVisible.Value = not isVisible.Value
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The function `wrap` will *wrap* any given value with a state object of type `State<T>` which can be read from/wrote to through its `.value` property.
|
||||||
|
|
||||||
|
In the above code, the `ImageLabel.Visible` property is assigned a state. Now any time that state's value is assigned to, `ImageLabel.Visible` will also update with the new value assigned, without you having to explicitly set the property. The above code gives the effect of the image label toggling visibility at a 1 second interval forever.
|
||||||
|
|
||||||
|
|
||||||
|
There are a few reasons why we use state objects instead of plain variables:
|
||||||
|
|
||||||
|
1. Vide detects when you assign a state object as a property value. This is known as *binding* and doing so will cause the property to *automatically* update whenever that state object's value is changed.
|
||||||
|
2. We can create new state objects that derive from other state objects, which again, *automatically* update when the derived state objects change.
|
||||||
|
|
||||||
|
The reason why this is useful, is that you as the programmer do not have to worry about manually updating variables or UI instances, you can just focus on defining how the data maps to UI and everything will automatically update when changes occur.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Derived State
|
||||||
|
|
||||||
|
You can create new state from other states. This is known as *deriving state*.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local derive = vide.derive
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local count = wrap(0)
|
||||||
|
|
||||||
|
local text = derive(function(from)
|
||||||
|
return "Count: " .. from(count)
|
||||||
|
end)
|
||||||
|
|
||||||
|
print(text.value) -- "Count: 0"
|
||||||
|
|
||||||
|
count.value += 1
|
||||||
|
|
||||||
|
print(text.value) -- "Count: 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we use [`vide.derive`](../api/reactivity-core#derive) to *derive* a new state `text` which depends on `count`.
|
||||||
|
|
||||||
|
A function is used to transform the value of `count`, where the value returned becomes the new value of `text`. The function receives an argument named `from` which is used to *capture* dependent states. This is used to link `count` to `text`, so that whenever `count` is updated, `text` will be too.
|
||||||
|
|
||||||
|
Whenever `count`'s value is changed, `text` will recompute its value and update anything dependent on `text`, such as UI.
|
||||||
|
|
||||||
|
States can be derived in a more concise manner when doing single operations such as concatenation:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local text = "Count: " .. count
|
||||||
|
```
|
||||||
|
|
||||||
|
You can derive new states using any Luau operator in this manner.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
*Components* in UI are just custom-made reusable pieces of UI made from other pieces of UI.
|
||||||
|
|
||||||
|
The recommended way to create components is to use functions that take a table of properties as an argument and return the new UI instance.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function Background(args)
|
||||||
|
return create("Frame") {
|
||||||
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||||
|
Position = args.Position,
|
||||||
|
Size = args.Size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local background = Background {
|
||||||
|
Position = UDim2.new(),
|
||||||
|
Size = UDim2.new()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Above is a simple example of a frame component with its background color set to black.
|
||||||
|
|
||||||
|
A single parameter `args` is used to pass properties to the component.
|
||||||
|
|
||||||
|
Components allow you to *encapsulate* behavior. You can only modify the component in ways that are defined in the component.
|
||||||
|
Looking at the above example, the only properties you are allowed to modify is `Position` and `Size`.
|
||||||
|
This is a good approach to use for organised code.
|
||||||
|
|
||||||
|
However, properties concering layout (positional and size properties) aren't usually intrinsinc to the component. In most cases the user would want to be able to pass these properties without having to manually pass each one in the component.
|
||||||
|
|
||||||
|
For these cases, the [`vide.Layout`](../api/creation#Layout) symbol can be used.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local Layout = vide.Layout
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function Background(props)
|
||||||
|
return create("Frame") {
|
||||||
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||||
|
[Layout] = props[Layout],
|
||||||
|
[Children] = props[Children]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local background = Background {
|
||||||
|
[Layout] = {
|
||||||
|
AnchorPoint = Vector2.new(),
|
||||||
|
Position = UDim2.new(),
|
||||||
|
Size = UDim2.new(),
|
||||||
|
},
|
||||||
|
|
||||||
|
[Children] = {
|
||||||
|
create("TextLabel") {},
|
||||||
|
create("ImageLabel") {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the `Layout` symbol automatically assigns those layout-specific properties without having to explicitly assign each one in the component definition. This is a very common case and for this reason it is recommended to only assign layout properties using the `Layout` symbol for consistency when dealing with components.
|
||||||
|
|
||||||
|
This allows you to pass through layout properties without breaking encapsulation.
|
||||||
|
|
||||||
|
Additionally, the above example shows how children can be passed to components in a similar manner.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Stateful components
|
||||||
|
|
||||||
|
Often, you need components that maintain their own internal state, such as a toggle button or a counter.
|
||||||
|
|
||||||
|
Below you can see how a simple counter component can be implemented.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function Counter(args)
|
||||||
|
-- create internal state unique to each component instance
|
||||||
|
local count = wrap(0)
|
||||||
|
|
||||||
|
return create("TextButton") {
|
||||||
|
Name = "Counter",
|
||||||
|
|
||||||
|
Text = "Count: " .. count
|
||||||
|
|
||||||
|
[Event.Activated] = function()
|
||||||
|
count.value += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
[Layout] = args[Layout]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
create "ScreenGui" {
|
||||||
|
Parent = game.StarterGui,
|
||||||
|
|
||||||
|
[Children] = {
|
||||||
|
Counter {
|
||||||
|
[Layout] = {
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
|
Position = UDim2.fromScale(0.5, 0),
|
||||||
|
Size = UDim2.fromScale(0.3, 0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here a reusable counter component is created, that when clicked on will increase its count and display it independent from other counter instances.
|
||||||
|
|
||||||
|
## Tables of data
|
||||||
|
|
||||||
|
basic inventory
|
||||||
|
|
||||||
|
```lua
|
||||||
|
type Item = {
|
||||||
|
Name: string,
|
||||||
|
Icon: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local items = wrap({} :: Array<Item>)
|
||||||
|
|
||||||
|
local function ItemSlot(args)
|
||||||
|
return create("Frame") {
|
||||||
|
[Layout] = args[Layout]
|
||||||
|
|
||||||
|
[Children] = {
|
||||||
|
create("TextLabel") {
|
||||||
|
Name = args.Name,
|
||||||
|
[Layout] = ...
|
||||||
|
},
|
||||||
|
|
||||||
|
create("ImageLabel") {
|
||||||
|
Image = "rbxassetid://" .. args.Icon,
|
||||||
|
[Layout] = ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Inventory(args)
|
||||||
|
return create("Frame") {
|
||||||
|
[Layout] = args[Layout],
|
||||||
|
|
||||||
|
[Children] = {
|
||||||
|
create("UIListLayout") {},
|
||||||
|
|
||||||
|
map(items, function(i, item)
|
||||||
|
return ItemSlot {
|
||||||
|
Name = item.Name,
|
||||||
|
Icon = item.Icon,
|
||||||
|
[Layout] = { LayoutOrder = i, ... }
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
More comprehensive tutorials are in the works. To find out more refer to the [`API documentation`](../../README#API).
|
||||||
99
docs/tut/reactive-graph.md
Normal file
99
docs/tut/reactive-graph.md
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Vide Reactive Graph
|
||||||
|
|
||||||
|
Details on how Vide's reactive graph works.
|
||||||
|
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
A "node" refers to a point on the reactive graph.
|
||||||
|
|
||||||
|
- Nodes can have parents and children.
|
||||||
|
- Updating a node will mark all descendant nodes for update.
|
||||||
|
- Each Vide state object acts as a node on the reactive graph.
|
||||||
|
|
||||||
|
Vide's reactive graph uses a *lazy evaluation* model, meaning that
|
||||||
|
if a node with children is updated, the new value for the child node
|
||||||
|
is not recalculated immediately. Only when something attempts to access
|
||||||
|
the child's value is it recalculated.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Below is an (*overengineered*) example to demonstrate how the reactive graph functions.
|
||||||
|
States are used here to model the various transforms done on two inputs, `health` and `maxHealth`
|
||||||
|
to represent player health for UI.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local health = wrap(90)
|
||||||
|
local maxHealth = wrap(100)
|
||||||
|
|
||||||
|
local healthTweened = spring(health, 0.5)
|
||||||
|
local text = "Health: " .. healthTweened
|
||||||
|
|
||||||
|
local ratio = health / maxHealth
|
||||||
|
|
||||||
|
local barSize = derive(function(from)
|
||||||
|
return UDim2.fromScale(from(ratio), 1)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Below is a graphical representation of the reactive graph formed by the above code.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A(( ))
|
||||||
|
B(( ))
|
||||||
|
A --> health
|
||||||
|
B --> maxHealth
|
||||||
|
|
||||||
|
health --> healthTweened
|
||||||
|
healthTweened --> text
|
||||||
|
|
||||||
|
health --> ratio
|
||||||
|
maxHealth --> ratio
|
||||||
|
ratio --> barSize
|
||||||
|
```
|
||||||
|
|
||||||
|
When states are initially derived, all values are known.
|
||||||
|
|
||||||
|
Say if the player is damaged, and the `health` node changes value.
|
||||||
|
All descendant nodes from `health` will be marked as updated.
|
||||||
|
The nodes marked as updated are represented by the broken lines below.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A(( ))
|
||||||
|
B(( ))
|
||||||
|
A --> health
|
||||||
|
B --> maxHealth
|
||||||
|
|
||||||
|
health .-x healthTweened
|
||||||
|
healthTweened .-x text
|
||||||
|
|
||||||
|
health .-x ratio
|
||||||
|
maxHealth --> ratio
|
||||||
|
ratio .-x barSize
|
||||||
|
```
|
||||||
|
|
||||||
|
When something tries to read the value of the node `text`, a recalculation occurs.
|
||||||
|
While `text` is being recalculated, `healthTweened` will be read from, causing it to be recalculated as well.
|
||||||
|
This results in a chain that propogates up the reactive graph until all ancestors are up to date.
|
||||||
|
|
||||||
|
Below is what the graph will look like after `text` has been recalculated.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A(( ))
|
||||||
|
B(( ))
|
||||||
|
A --> health
|
||||||
|
B --> maxHealth
|
||||||
|
|
||||||
|
health --> healthTweened
|
||||||
|
healthTweened --> text
|
||||||
|
|
||||||
|
health .-x ratio
|
||||||
|
maxHealth --> ratio
|
||||||
|
ratio .-x barSize
|
||||||
|
```
|
||||||
|
|
||||||
|
Lazy evaluation is a useful model as it saves unecessary calculation, only calculating when needed.
|
||||||
|
|
||||||
|
Looking at stateful code as a reactive graph is a good way to mentally picture how your data maps to UI.
|
||||||
20
docs/tut/tmp.md
Normal file
20
docs/tut/tmp.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
```lua
|
||||||
|
local function Text(args)
|
||||||
|
return create("TextLabel") {
|
||||||
|
[Layout] = {
|
||||||
|
Size = scale(1),
|
||||||
|
args[Layout]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Text {
|
||||||
|
[Layout] = {
|
||||||
|
Position = scale(0.5, 0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
a
|
||||||
|
```
|
||||||
55
src/Change.lua
Normal file
55
src/Change.lua
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Change.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local memoize = require(script.Parent.memoize)
|
||||||
|
local bind = require(script.Parent.bind)
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
local create = graph.create
|
||||||
|
local get = graph.get
|
||||||
|
local link = graph.link
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
type Listener = (unknown) -> ()
|
||||||
|
|
||||||
|
local getChangeSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
|
||||||
|
return {
|
||||||
|
priority = 2,
|
||||||
|
run = function(instance: Instance, listener: MaybeState<Listener>)
|
||||||
|
local event: RBXScriptSignal<nil> = instance:GetPropertyChangedSignal(name)
|
||||||
|
|
||||||
|
if type(listener) == "function" then
|
||||||
|
event:Connect(function()
|
||||||
|
listener( (instance :: any)[name] )
|
||||||
|
end)
|
||||||
|
elseif wrapped(listener) then
|
||||||
|
local state = create(nil)
|
||||||
|
|
||||||
|
link(listener :: State<Listener>, state, function()
|
||||||
|
local newListener = get(listener :: State<Listener>)
|
||||||
|
return newListener and function()
|
||||||
|
newListener( (instance :: any)[name] )
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
state.updated = true
|
||||||
|
bind.event(state :: State<any>, instance, event)
|
||||||
|
else
|
||||||
|
error("Attempt to connect non-function to changed event", 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
local Changed = table.freeze(setmetatable({}, {__index = function(_, index: string)
|
||||||
|
return getChangeSymbol(index)
|
||||||
|
end})) :: any
|
||||||
|
|
||||||
|
return Changed :: { [string]: unknown }
|
||||||
39
src/Children.lua
Normal file
39
src/Children.lua
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Children.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then
|
||||||
|
script = (require :: any) "test/wrap-require"
|
||||||
|
typeof = require "test/mock".typeof
|
||||||
|
end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local bind = require(script.Parent.bind)
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
type Children = Types.Children
|
||||||
|
|
||||||
|
local function setChildren(instance: Instance, children: Children)
|
||||||
|
if typeof(children) == "Instance" then
|
||||||
|
if children.Parent then throw(`Cannot parent instance { instance.Name }, instance already parented`) end
|
||||||
|
children.Parent = instance
|
||||||
|
elseif wrapped(children) then
|
||||||
|
bind.children(children :: any, instance )
|
||||||
|
elseif type(children) == "table" then
|
||||||
|
for _, child: Children in next, children do
|
||||||
|
setChildren(instance, child)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
throw(`Cannot parent non-instance { typeof(children) }`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Children = {
|
||||||
|
priority = 1,
|
||||||
|
run = setChildren
|
||||||
|
} :: Types.Symbol<Children>
|
||||||
|
|
||||||
|
return Children :: unknown
|
||||||
16
src/Created.lua
Normal file
16
src/Created.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Created.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
local Created = {
|
||||||
|
priority = 3,
|
||||||
|
run = function(instance: Instance, callback: (Instance) -> ())
|
||||||
|
callback(instance)
|
||||||
|
end
|
||||||
|
} :: Types.Symbol<(Instance) -> ()>
|
||||||
|
|
||||||
|
return Created :: unknown
|
||||||
41
src/Event.lua
Normal file
41
src/Event.lua
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Event.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local memoize = require(script.Parent.memoize)
|
||||||
|
local bind = require(script.Parent.bind)
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
type Listener = (unknown) -> ()
|
||||||
|
|
||||||
|
local getEventSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
|
||||||
|
return {
|
||||||
|
priority = 2,
|
||||||
|
run = function(instance: Instance, listener: MaybeState<Listener>)
|
||||||
|
local event: RBXScriptSignal<...unknown> = (instance :: any)[name]
|
||||||
|
|
||||||
|
if type(listener) == "function" then
|
||||||
|
event:Connect(listener)
|
||||||
|
elseif wrapped(listener) then
|
||||||
|
bind.event(listener :: State<Listener>, instance, event)
|
||||||
|
else
|
||||||
|
error("Attempt to connect non-function to event", 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
local Event = table.freeze(setmetatable({}, {__index = function(_, index: string)
|
||||||
|
return getEventSymbol(index)
|
||||||
|
end})) :: any
|
||||||
|
|
||||||
|
return Event :: { [string]: unknown }
|
||||||
|
|
||||||
46
src/Layout.lua
Normal file
46
src/Layout.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Layout.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local bind = require(script.Parent.bind)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
local layoutProperties = {
|
||||||
|
Parent = true,
|
||||||
|
AnchorPoint = true,
|
||||||
|
LayoutOrder = true,
|
||||||
|
Position = true,
|
||||||
|
Rotation = true,
|
||||||
|
Size = true,
|
||||||
|
SizeConstraint = true,
|
||||||
|
Visible = true,
|
||||||
|
ZIndex = true
|
||||||
|
}
|
||||||
|
|
||||||
|
local function setLayout(instance: Instance, properties: { [string]: unknown })
|
||||||
|
for property, value in next, properties do
|
||||||
|
if flags.strict and not layoutProperties[property] then
|
||||||
|
throw(`{ property } is not a valid layout property`)
|
||||||
|
end
|
||||||
|
|
||||||
|
if wrapped(value) then
|
||||||
|
bind.property(value :: graph.State<unknown>, instance, property)
|
||||||
|
else
|
||||||
|
(instance :: any)[property] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Layout = {
|
||||||
|
priority = 1,
|
||||||
|
run = setLayout
|
||||||
|
} :: Types.Symbol<{ [string]: unknown }>
|
||||||
|
|
||||||
|
return Layout :: unknown
|
||||||
19
src/Types.lua
Normal file
19
src/Types.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/Types.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
|
||||||
|
export type Symbol<T> = {
|
||||||
|
priority: number,
|
||||||
|
run: (Instance, T) -> ()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Children = Instance | State<Children> | { Children }
|
||||||
|
|
||||||
|
return {}
|
||||||
15
src/apply.lua
Normal file
15
src/apply.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/apply.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local applyProperties = require(script.Parent.applyProperties)
|
||||||
|
|
||||||
|
local function apply<T>(instance: T & Instance)
|
||||||
|
return function(properties: { [any]: unknown }): T
|
||||||
|
return applyProperties(instance, properties)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return apply
|
||||||
66
src/applyProperties.lua
Normal file
66
src/applyProperties.lua
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/applyProperties.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local bind = require(script.Parent.bind)
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
|
|
||||||
|
local function applyProperty(instance: Instance, property: string, value: unknown)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyProperties<T>(instance: T & Instance, properties: { [string|Types.Symbol<unknown> ]: unknown }): T
|
||||||
|
local parent: unknown = properties.Parent
|
||||||
|
if parent then properties.Parent = nil end
|
||||||
|
|
||||||
|
local eventBuffer: { [(Instance, () -> ()) -> ()]: () -> () } = {} -- connect events after setting properties
|
||||||
|
local postCreation: (Instance) -> ()? = nil -- buffer for post-creation callback
|
||||||
|
|
||||||
|
for property, value in next, properties do
|
||||||
|
if type(property) == "string" then
|
||||||
|
if wrapped(value) then
|
||||||
|
bind.property(value :: State<unknown>, instance, property)
|
||||||
|
else
|
||||||
|
(instance :: any)[property] = value
|
||||||
|
end
|
||||||
|
elseif type(property) == "table" then
|
||||||
|
local priority = property.priority
|
||||||
|
if priority == 1 then
|
||||||
|
property.run(instance, value)
|
||||||
|
elseif priority == 2 then
|
||||||
|
eventBuffer[property.run] = value :: () -> ()
|
||||||
|
elseif priority == 3 then
|
||||||
|
assert(not postCreation)
|
||||||
|
postCreation = value :: () -> ()
|
||||||
|
else
|
||||||
|
error("invalid priority")
|
||||||
|
end
|
||||||
|
else throw(`Invalid property { tostring(property) }, expected string or symbol`) end
|
||||||
|
end
|
||||||
|
|
||||||
|
for fn, v in next, eventBuffer do
|
||||||
|
fn(instance, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
if parent then
|
||||||
|
applyProperty(instance, "Parent", parent)
|
||||||
|
if wrapped(parent) then
|
||||||
|
bind.parent(parent :: State<Instance?>, instance)
|
||||||
|
else
|
||||||
|
instance.Parent = parent :: Instance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if postCreation then postCreation(instance) end
|
||||||
|
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
|
||||||
|
return applyProperties
|
||||||
139
src/bind.lua
Normal file
139
src/bind.lua
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/bind.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local warn = warn -- todo
|
||||||
|
|
||||||
|
if not game then
|
||||||
|
script = (require :: any) "test/wrap-require"
|
||||||
|
warn = print
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
local get = graph.get
|
||||||
|
local setEffect = graph.setEffect
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
|
||||||
|
local hold: { Instance? } = {}
|
||||||
|
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
|
||||||
|
local bindcount = 0
|
||||||
|
|
||||||
|
local srcs do
|
||||||
|
local src1 = debug.info(1, "s")
|
||||||
|
local srctrunc = string.sub(src1, 1, #src1-4)
|
||||||
|
|
||||||
|
srcs = {
|
||||||
|
src1,
|
||||||
|
srctrunc .. "applyProperties",
|
||||||
|
srctrunc .. "create",
|
||||||
|
srctrunc .. "apply"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function traceback() -- ensures trace begins outside of any vide library file
|
||||||
|
local s = 1
|
||||||
|
repeat
|
||||||
|
s += 1
|
||||||
|
local src = debug.info(s, "s")
|
||||||
|
until not table.find(srcs, src)
|
||||||
|
return debug.traceback("", s)
|
||||||
|
end
|
||||||
|
|
||||||
|
function setup(state: State<any>, instance: Instance, updateInstance: (Instance) -> ())
|
||||||
|
if flags.strict then
|
||||||
|
local fn = updateInstance
|
||||||
|
local trace = traceback()
|
||||||
|
updateInstance = function(instance)
|
||||||
|
local ok, err: string? = pcall(fn, instance)
|
||||||
|
if not ok then warn(`error occured updating state binding:\n{err}\nset from:{trace}`) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
updateInstance(instance)
|
||||||
|
setEffect(state, updateInstance, instance)
|
||||||
|
|
||||||
|
bindcount += 1
|
||||||
|
local key = bindcount
|
||||||
|
|
||||||
|
weak[key] = instance
|
||||||
|
|
||||||
|
local function ref()
|
||||||
|
local _ = state -- prevent gc of state while instance exists
|
||||||
|
local instance = weak[key] :: Instance
|
||||||
|
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
|
||||||
|
end
|
||||||
|
|
||||||
|
ref()
|
||||||
|
instance:GetPropertyChangedSignal("Parent"):Connect(ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindProperty(state: State<unknown>, instance_STRONG: Instance, property: string)
|
||||||
|
setup(state, instance_STRONG, function(instance)
|
||||||
|
(instance :: any)[property] = get(state)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindParent(state: State<Instance?>, instance_STRONG)
|
||||||
|
instance_STRONG.Destroying:Connect(function()
|
||||||
|
instance_STRONG = nil :: any -- allow gc when destroyed
|
||||||
|
end)
|
||||||
|
setup(state, instance_STRONG, function(instance)
|
||||||
|
local _ = instance_STRONG -- state will strongly reference instance when parent is bound
|
||||||
|
instance.Parent = get(state)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance)
|
||||||
|
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update
|
||||||
|
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update
|
||||||
|
|
||||||
|
setup(state, parent_STRONG, function(parent)
|
||||||
|
local newChildren = get(state) -- all (and only) children that should be parented after this update
|
||||||
|
if newChildren and type(newChildren) ~= "table" then
|
||||||
|
throw(`Cannot parent instance of type { type(newChildren) } `)
|
||||||
|
end
|
||||||
|
|
||||||
|
if newChildren then
|
||||||
|
for _, child in next, newChildren do
|
||||||
|
newChildrenSet[child] = true -- record child set from this update
|
||||||
|
if not currentChildrenSet[child] then
|
||||||
|
child.Parent = parent -- if child wasn't already parented then parent it
|
||||||
|
else
|
||||||
|
currentChildrenSet[child] = nil -- remove child from cache if it was already in cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for child in next, currentChildrenSet do
|
||||||
|
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||||
|
end
|
||||||
|
|
||||||
|
table.clear(currentChildrenSet) -- clear cache, preserve capacity
|
||||||
|
currentChildrenSet, newChildrenSet = newChildrenSet, currentChildrenSet
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindEvent(state: State<() -> ()?>, instance_STRONG: Instance, event: RBXScriptSignal)
|
||||||
|
local current: RBXScriptConnection? = nil
|
||||||
|
setup(state, instance_STRONG, function(instance)
|
||||||
|
if current then
|
||||||
|
current:Disconnect()
|
||||||
|
current = nil
|
||||||
|
end
|
||||||
|
local listener = get(state)
|
||||||
|
if listener then
|
||||||
|
current = event:Connect(listener)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
property = bindProperty,
|
||||||
|
parent = bindParent,
|
||||||
|
children = bindChildren,
|
||||||
|
event = bindEvent
|
||||||
|
}
|
||||||
78
src/create.lua
Normal file
78
src/create.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/create.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then
|
||||||
|
script = (require :: any) "test/wrap-require"
|
||||||
|
Instance = require("test/mock").Instance
|
||||||
|
end
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local defaults = require(script.Parent.defaults)
|
||||||
|
local applyProperties = require(script.Parent.applyProperties)
|
||||||
|
local memoize = require(script.Parent.memoize)
|
||||||
|
|
||||||
|
local function createInstance(className: string)
|
||||||
|
local success, instance: Instance = pcall(Instance.new, className :: any)
|
||||||
|
if success == false then throw(`invalid class name, could not create instance of class { className }`) end
|
||||||
|
|
||||||
|
local default: { [string]: unknown }? = defaults[className]
|
||||||
|
if default then
|
||||||
|
for i, v in next, default do
|
||||||
|
(instance :: any)[i] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(properties: { [any]: unknown }): Instance
|
||||||
|
return applyProperties(instance:Clone(), properties)
|
||||||
|
end
|
||||||
|
end; createInstance = memoize(createInstance)
|
||||||
|
|
||||||
|
local function cloneInstance(instance: Instance)
|
||||||
|
return function(properties: { [any]: unknown }): Instance
|
||||||
|
local clone = instance:Clone()
|
||||||
|
if not clone then error("Attempt to clone a non-archivable instance", 3) end
|
||||||
|
return applyProperties(clone, properties)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create(classNameOrInstance: string|Instance)
|
||||||
|
if type(classNameOrInstance) == "string" then
|
||||||
|
return createInstance(classNameOrInstance)
|
||||||
|
elseif typeof(classNameOrInstance) == "Instance" then
|
||||||
|
return cloneInstance(classNameOrInstance)
|
||||||
|
else
|
||||||
|
error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
type Props = { [any]: any }
|
||||||
|
return (create :: any) ::
|
||||||
|
( <T>(T & Instance) -> (Props) -> T ) &
|
||||||
|
( ("Folder") -> (Props) -> Folder ) &
|
||||||
|
( ("BillboardGui") -> (Props) -> BillboardGui ) &
|
||||||
|
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
|
||||||
|
( ("Frame") -> (Props) -> Frame ) &
|
||||||
|
( ("ImageButton") -> (Props) -> ImageButton ) &
|
||||||
|
( ("ImageLabel") -> (Props) -> ImageLabel ) &
|
||||||
|
( ("ScreenGui") -> (Props) -> ScreenGui ) &
|
||||||
|
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
|
||||||
|
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
|
||||||
|
( ("TextBox") -> (Props) -> TextBox ) &
|
||||||
|
( ("TextButton") -> (Props) -> TextButton ) &
|
||||||
|
( ("TextLabel") -> (Props) -> TextLabel ) &
|
||||||
|
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
|
||||||
|
( ("UICorner") -> (Props) -> UICorner ) &
|
||||||
|
( ("UIGradient") -> (Props) -> UIGradient ) &
|
||||||
|
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
|
||||||
|
( ("UIListLayout") -> (Props) -> UIListLayout ) &
|
||||||
|
( ("UIPadding") -> (Props) -> UIPadding ) &
|
||||||
|
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
|
||||||
|
( ("UIScale") -> (Props) -> UIScale ) &
|
||||||
|
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
|
||||||
|
( ("UIStroke") -> (Props) -> UIStroke ) &
|
||||||
|
( ("UITableLayout") -> (Props) -> UITableLayout ) &
|
||||||
|
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
|
||||||
|
( ("VideoFrame") -> (Props) -> VideoFrame ) &
|
||||||
|
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
|
||||||
|
( (string) -> (Props) -> Instance )
|
||||||
112
src/defaults.lua
Normal file
112
src/defaults.lua
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/defaults.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- todo
|
||||||
|
local Enum = Enum
|
||||||
|
local Color3 = Color3
|
||||||
|
local Vector3 = Vector3
|
||||||
|
|
||||||
|
if not game then
|
||||||
|
local mock = require "test/mock"
|
||||||
|
Enum = mock.Enum :: any
|
||||||
|
Color3 = mock.Color3 :: any
|
||||||
|
Vector3 = mock.Vector3 :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Part = {
|
||||||
|
Material = Enum.Material.SmoothPlastic,
|
||||||
|
Size = Vector3.new(1, 1, 1),
|
||||||
|
Anchored = true
|
||||||
|
},
|
||||||
|
|
||||||
|
BillboardGui = {
|
||||||
|
ResetOnSpawn = false,
|
||||||
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
|
||||||
|
},
|
||||||
|
|
||||||
|
CanvasGroup = nil,
|
||||||
|
|
||||||
|
Frame = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0
|
||||||
|
},
|
||||||
|
|
||||||
|
ImageButton = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
AutoButtonColor = false
|
||||||
|
},
|
||||||
|
|
||||||
|
ImageLabel = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
ScreenGui = {
|
||||||
|
ResetOnSpawn = false,
|
||||||
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
|
||||||
|
},
|
||||||
|
|
||||||
|
ScrollingFrame = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
ScrollBarImageColor3 = Color3.new(0, 0, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
SurfaceGui = {
|
||||||
|
ResetOnSpawn = false,
|
||||||
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
PixelsPerStud = 50,
|
||||||
|
SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
|
||||||
|
},
|
||||||
|
|
||||||
|
TextBox = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
ClearTextOnFocus = false,
|
||||||
|
Font = Enum.Font.SourceSans,
|
||||||
|
Text = "",
|
||||||
|
TextColor3 = Color3.new(0, 0, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
TextButton = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
AutoButtonColor = false,
|
||||||
|
Font = Enum.Font.SourceSans,
|
||||||
|
Text = "",
|
||||||
|
TextColor3 = Color3.new(0, 0, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
TextLabel = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
Font = Enum.Font.SourceSans,
|
||||||
|
Text = "",
|
||||||
|
TextColor3 = Color3.new(0, 0, 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
-- UIComponent instances
|
||||||
|
|
||||||
|
VideoFrame = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0
|
||||||
|
},
|
||||||
|
|
||||||
|
ViewportFrame = {
|
||||||
|
BackgroundColor3 = Color3.new(1, 1, 1),
|
||||||
|
BorderColor3 = Color3.new(0, 0, 0),
|
||||||
|
BorderSizePixel = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/derive.lua
Normal file
32
src/derive.lua
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/derive.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type Unwrapper = graph.Unwrapper
|
||||||
|
local create = graph.create
|
||||||
|
local captureAndLink = graph.captureAndLink
|
||||||
|
|
||||||
|
local function derive<T>(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): State<T>
|
||||||
|
local node = create((nil :: any) :: T)
|
||||||
|
|
||||||
|
if cleanup then
|
||||||
|
local fn = deriveValue
|
||||||
|
local last: T? = nil
|
||||||
|
deriveValue = function(from)
|
||||||
|
if last ~= nil then cleanup(last) end
|
||||||
|
last = fn(from)
|
||||||
|
return last :: T
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local value: T = captureAndLink(node, deriveValue)
|
||||||
|
node.cache = value
|
||||||
|
|
||||||
|
return node :: State<T>
|
||||||
|
end
|
||||||
|
|
||||||
|
return derive
|
||||||
24
src/each.lua
Normal file
24
src/each.lua
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/each.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type Node<T> = graph.Node<T>
|
||||||
|
local create = graph.create
|
||||||
|
local get = graph.get
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
local link = graph.link
|
||||||
|
|
||||||
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
|
local function each<K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
return (each :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
|
||||||
|
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
|
||||||
|
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||||
|
-> State<Map<K, VO>> )
|
||||||
5
src/flags.lua
Normal file
5
src/flags.lua
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/flags.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return { strict = false }
|
||||||
231
src/graph.lua
Normal file
231
src/graph.lua
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/graph.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
|
||||||
|
export type State<T> = typeof(setmetatable(
|
||||||
|
{} :: {
|
||||||
|
cache: T,
|
||||||
|
updated: boolean,
|
||||||
|
derive: (any) -> T,
|
||||||
|
effects: { [(unknown) -> ()]: unknown } | false, -- weak values
|
||||||
|
children: { State<T> } | false -- weak values
|
||||||
|
}, {} :: {
|
||||||
|
__concat: (any, any) -> any,
|
||||||
|
__add: (any, any) -> any,
|
||||||
|
__sub: (any, any) -> any,
|
||||||
|
__mul: (any, any) -> any,
|
||||||
|
__div: (any, any) -> any,
|
||||||
|
--__pow
|
||||||
|
--__mod
|
||||||
|
--__unm
|
||||||
|
--__eq: (unknown, unknown) -> State<boolean>
|
||||||
|
--__lt
|
||||||
|
--__le
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
export type MaybeState<T> = State<T> | T
|
||||||
|
export type Unwrapper = <T>(T) -> T
|
||||||
|
|
||||||
|
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
|
||||||
|
local EVALUATION_ERR = "Error while evaluating state:\n\n"
|
||||||
|
|
||||||
|
local State = {}
|
||||||
|
|
||||||
|
local function wrapped(value: any): boolean
|
||||||
|
return getmetatable(value) == State
|
||||||
|
end
|
||||||
|
|
||||||
|
local unwrap: <T>(T) -> T;
|
||||||
|
|
||||||
|
local checkForYield do
|
||||||
|
local t = { __mode = "kv" }
|
||||||
|
setmetatable(t, t)
|
||||||
|
|
||||||
|
checkForYield = function(fn: (Unwrapper) -> ())
|
||||||
|
t.__unm = function()
|
||||||
|
fn(unwrap)
|
||||||
|
end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
return -t :: any
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
|
||||||
|
error(EVALUATION_ERR .. "Cannot yield when deriving state in watcher", 3)
|
||||||
|
else
|
||||||
|
error(EVALUATION_ERR..err, 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setEffect<T>(state: State<unknown>, fn: (T) -> (), key: T)
|
||||||
|
if not state.effects then
|
||||||
|
state.effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
|
||||||
|
else
|
||||||
|
state.effects[fn :: () -> ()] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runEffects(state: State<unknown>)
|
||||||
|
if state.effects then
|
||||||
|
for effect, key in next, state.effects do
|
||||||
|
if flags.strict then effect(key) end
|
||||||
|
effect(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- retrieves a state's cached value
|
||||||
|
-- recalculates value if an ancestor was updated
|
||||||
|
local function get<T>(state: State<T>): T
|
||||||
|
if state.updated then
|
||||||
|
state.updated = false
|
||||||
|
|
||||||
|
if flags.strict then checkForYield(state.derive) end
|
||||||
|
|
||||||
|
local ok, result: T|string? = pcall(state.derive, unwrap); if ok then
|
||||||
|
rawset(state :: any, "cache", result :: T)
|
||||||
|
else error(EVALUATION_ERR .. result :: string, 2) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return state.cache
|
||||||
|
end
|
||||||
|
|
||||||
|
-- utility function for retrieving a value from state and allowing passthrough of non-state
|
||||||
|
unwrap = function<T>(value: MaybeState<T>): T
|
||||||
|
if wrapped(value) then
|
||||||
|
return get(value :: State<T>)
|
||||||
|
else
|
||||||
|
return value :: T
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addChild(parent: State<unknown>, child: State<unknown>)
|
||||||
|
if parent.children then
|
||||||
|
table.insert(parent.children, child)
|
||||||
|
else
|
||||||
|
parent.children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- marks all state descendants for recalculation and runs effects
|
||||||
|
local function update(state: State<unknown>)
|
||||||
|
runEffects(state)
|
||||||
|
if state.children then
|
||||||
|
for _, child in state.children do
|
||||||
|
if not child.updated then
|
||||||
|
child.updated = true
|
||||||
|
update(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- sets a state's cached value and updates all descendants
|
||||||
|
local function set<T>(state: State<T>, value: T)
|
||||||
|
state.cache = value
|
||||||
|
update(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- links two states as parent-child
|
||||||
|
local function link(parent: State<unknown>, child: State<unknown>, derive: () -> unknown)
|
||||||
|
child.derive = derive
|
||||||
|
addChild(parent, child)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- detect what states were referenced in the given callback and returns them in an array
|
||||||
|
local function capture<T>(callback: (Unwrapper) -> T): ({ State<unknown> }, T)
|
||||||
|
if flags.strict then checkForYield(callback) end
|
||||||
|
|
||||||
|
local states = table.create(2)
|
||||||
|
|
||||||
|
local ok: boolean, result: T|string = pcall(callback, function<T>(value: MaybeState<T>): T
|
||||||
|
if wrapped(value) then
|
||||||
|
table.insert(states, value :: State<T>)
|
||||||
|
return get(value :: State<T>)
|
||||||
|
else
|
||||||
|
return value :: T
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not ok then error("Error while detecting watcher: " .. result :: string, 2) end
|
||||||
|
|
||||||
|
return states, result :: T
|
||||||
|
end
|
||||||
|
|
||||||
|
-- captures and links any detected states
|
||||||
|
local function captureAndLink<T>(child: State<T>, callback: (Unwrapper) -> T): T
|
||||||
|
local states, value = capture(callback)
|
||||||
|
|
||||||
|
child.derive = callback
|
||||||
|
for _, parent: State<unknown> in next, states do
|
||||||
|
addChild(parent, child)
|
||||||
|
end
|
||||||
|
|
||||||
|
return value :: T
|
||||||
|
end
|
||||||
|
|
||||||
|
local create: <T>(value: T) -> State<T>
|
||||||
|
|
||||||
|
-- factory function for creating operator overloads for shorthands to derive state
|
||||||
|
local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any
|
||||||
|
return function(a: MaybeState<unknown>, b: MaybeState<unknown>): State<unknown>
|
||||||
|
local derived: State<unknown> = create(nil :: any)
|
||||||
|
|
||||||
|
local aIsState = wrapped(a)
|
||||||
|
local bIsState = wrapped(b)
|
||||||
|
|
||||||
|
if aIsState and bIsState then
|
||||||
|
local function derive() return op(get(a :: State<unknown>), get(b :: State<unknown>)) end
|
||||||
|
link(a :: State<unknown>, derived, derive)
|
||||||
|
link(b :: State<unknown>, derived, derive)
|
||||||
|
elseif aIsState then
|
||||||
|
link(a :: State<unknown>, derived, function() return op(get(a :: State<unknown>), b) end)
|
||||||
|
else--if bIsState then
|
||||||
|
link(b :: State<unknown>, derived, function() return op(a, get(b :: State<unknown>)) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
derived.updated = true
|
||||||
|
return derived
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function State.__index(_, index)
|
||||||
|
if index == "cache" then return nil end -- todo: better solution
|
||||||
|
error("attempt to index state", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end)
|
||||||
|
State.__add = overload(function(a: any, b: any) return a + b end)
|
||||||
|
State.__sub = overload(function(a: any, b: any) return a - b end)
|
||||||
|
State.__mul = overload(function(a: any, b: any) return a * b end)
|
||||||
|
State.__div = overload(function(a: any, b: any) return a / b end)
|
||||||
|
--State.__eq = overload(function(a: any, b: any) return a == b end)
|
||||||
|
|
||||||
|
function create<T>(value: T): State<T>
|
||||||
|
return setmetatable({
|
||||||
|
cache = value,
|
||||||
|
updated = false,
|
||||||
|
derive = function() return nil end :: any,
|
||||||
|
effects = false :: false,
|
||||||
|
children = false :: false
|
||||||
|
}, State)
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.freeze {
|
||||||
|
setEffect = setEffect,
|
||||||
|
get = get,
|
||||||
|
set = set,
|
||||||
|
unwrap = unwrap,
|
||||||
|
link = link,
|
||||||
|
capture = capture,
|
||||||
|
captureAndLink = captureAndLink,
|
||||||
|
wrapped = wrapped,
|
||||||
|
create = create,
|
||||||
|
}
|
||||||
84
src/init.lua
Normal file
84
src/init.lua
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide.lua
|
||||||
|
-- v0.1.0
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local create = require(script.create)
|
||||||
|
local apply = require(script.apply)
|
||||||
|
local wrap = require(script.wrap)
|
||||||
|
local watch = require(script.watch)
|
||||||
|
local derive = require(script.derive)
|
||||||
|
local map = require(script.map)
|
||||||
|
|
||||||
|
local unwrap = require(script.unwrap)
|
||||||
|
local wrapped = require(script.wrapped)
|
||||||
|
|
||||||
|
local Layout = require(script.Layout)
|
||||||
|
local Children = require(script.Children)
|
||||||
|
local Event = require(script.Event)
|
||||||
|
local Changed = require(script.Change)
|
||||||
|
local Created = require(script.Created)
|
||||||
|
|
||||||
|
local spring, updateSprings = require(script.spring)()
|
||||||
|
|
||||||
|
local flags = require(script.flags)
|
||||||
|
|
||||||
|
type Map<K, V> = { [K]: V }
|
||||||
|
type Unwrapper = <T>(T) -> T
|
||||||
|
type Setter<T> = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, force: true?) -> T )
|
||||||
|
|
||||||
|
local vide = {
|
||||||
|
-- core
|
||||||
|
create = create,
|
||||||
|
apply = apply,
|
||||||
|
wrap = (wrap :: any) :: <T>(value: T?) -> (T, Setter<T>),
|
||||||
|
derive = (derive :: any) :: <T>(deriver: (from: <U>(U) -> U) -> T, cleanup: (T) -> ()?) -> T,
|
||||||
|
map = (map :: any) :: ( <V>(input: number, transform: (number) -> V, cleanup: (V) -> ()?) -> Map<number, V> ) & ( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?) -> Map<K, VO> ),
|
||||||
|
watch = watch :: ((Unwrapper) -> ()) -> () -> (),
|
||||||
|
|
||||||
|
-- util
|
||||||
|
unwrap = unwrap,
|
||||||
|
wrapped = wrapped,
|
||||||
|
|
||||||
|
-- animations
|
||||||
|
spring = (spring :: any) :: <T>(input: T, period: number, damping: number?) -> T,
|
||||||
|
|
||||||
|
-- symbols
|
||||||
|
Event = Event,
|
||||||
|
Changed = Changed,
|
||||||
|
Layout = Layout,
|
||||||
|
Children = Children,
|
||||||
|
Created = Created,
|
||||||
|
|
||||||
|
-- flags
|
||||||
|
strict = (nil :: any) :: boolean,
|
||||||
|
|
||||||
|
-- test
|
||||||
|
step = function(dt: number)
|
||||||
|
updateSprings(dt)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(vide :: any, {
|
||||||
|
__index = function(_, index: unknown)
|
||||||
|
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
|
||||||
|
end,
|
||||||
|
|
||||||
|
__newindex = function(_, index: unknown, value: unknown)
|
||||||
|
if index == "strict" then
|
||||||
|
flags.strict = if type(value) == "boolean" then value else error("strict must be a boolean", 2)
|
||||||
|
else
|
||||||
|
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
if game then
|
||||||
|
game:GetService("RunService").Heartbeat:Connect(function(dt: number)
|
||||||
|
task.defer(vide.step, dt)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return vide
|
||||||
76
src/map.lua
Normal file
76
src/map.lua
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/map.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
local create = graph.create
|
||||||
|
local get = graph.get
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
local link = graph.link
|
||||||
|
|
||||||
|
type Map<K, V> = { [K]: V }
|
||||||
|
|
||||||
|
local function map<K, VI, VO>(input: unknown, transform: (K, VI) -> VO, cleanup: (VO) -> ()?): (MaybeState<Map<K, VO>>)
|
||||||
|
if type(input) == "number" then
|
||||||
|
local output: Map<K, VO> = table.create(input) :: {}
|
||||||
|
|
||||||
|
for i = 1, input do
|
||||||
|
local v = transform(i :: any, nil :: any)
|
||||||
|
output[i :: any] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
elseif wrapped(input) then
|
||||||
|
local lastInput: Map<K, VI> = {}
|
||||||
|
local lastOutput: Map<K, VO> = {}
|
||||||
|
local output = create(lastOutput)
|
||||||
|
|
||||||
|
local function derive()
|
||||||
|
local newInput = get(input :: State<Map<K, VI>>)
|
||||||
|
if type(newInput) ~= "table" then error("Attempt to run map on a non-table state", 0) end
|
||||||
|
|
||||||
|
local lastInputClone = table.clone(lastInput)
|
||||||
|
|
||||||
|
for k, vi in next, newInput do
|
||||||
|
if vi ~= lastInputClone[k] then
|
||||||
|
local vo = transform(k, vi)
|
||||||
|
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
|
||||||
|
lastInput[k] = vi
|
||||||
|
lastOutput[k] = vo
|
||||||
|
end
|
||||||
|
lastInputClone[k] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in next, lastInputClone do
|
||||||
|
lastInput[k] = nil
|
||||||
|
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
|
||||||
|
lastOutput[k] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.clone(lastOutput)
|
||||||
|
end
|
||||||
|
|
||||||
|
link(input :: State<Map<K, VI>>, output, derive)
|
||||||
|
output.updated = true
|
||||||
|
|
||||||
|
return output
|
||||||
|
elseif type(input) == "table" then
|
||||||
|
local output: Map<K, VO> = table.create(#input :: any) :: {}
|
||||||
|
|
||||||
|
for k, vi in input :: Map<K, VI> do
|
||||||
|
local vo = transform(k, vi)
|
||||||
|
output[k] = vo
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
else error(string.format("Invalid type arg #1, expected number or table or state (got %s)", tostring(input)), 2) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return (map :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
|
||||||
|
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
|
||||||
|
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||||
|
-> State<Map<K, VO>> )
|
||||||
21
src/memoize.lua
Normal file
21
src/memoize.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/memoize.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function memoize<X, Y>(f: (X) -> Y): ((X) -> Y, { [X]: Y })
|
||||||
|
local cache: { [X]: Y } = {}
|
||||||
|
|
||||||
|
return function(x: X): Y
|
||||||
|
local y: Y? = cache[x]
|
||||||
|
|
||||||
|
if not y then
|
||||||
|
y = f(x)
|
||||||
|
cache[x] = y
|
||||||
|
end
|
||||||
|
|
||||||
|
return y :: Y
|
||||||
|
end, cache
|
||||||
|
end
|
||||||
|
|
||||||
|
return memoize
|
||||||
|
|
||||||
169
src/spring.lua
Normal file
169
src/spring.lua
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/spring.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Spring animation library adapted from RDL::spring v1.0
|
||||||
|
|
||||||
|
Supported datatypes:
|
||||||
|
*number
|
||||||
|
!bool
|
||||||
|
*CFrame
|
||||||
|
?Rect
|
||||||
|
*Color3
|
||||||
|
*UDim
|
||||||
|
*UDim2
|
||||||
|
*Vector2
|
||||||
|
!Vector2int16
|
||||||
|
*Vector3
|
||||||
|
!Vector3int16
|
||||||
|
!EnumItem
|
||||||
|
]]
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
local create = graph.create
|
||||||
|
local get = graph.get
|
||||||
|
local set = graph.set
|
||||||
|
local unwrap = graph.unwrap
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||||
|
|
||||||
|
type SpringData<T> = {
|
||||||
|
Alpha: number;
|
||||||
|
Duration: number;
|
||||||
|
Period: number;
|
||||||
|
Damping: number;
|
||||||
|
Velocity: number;
|
||||||
|
InitialVelocity: number;
|
||||||
|
Initial: T;
|
||||||
|
Target: T;
|
||||||
|
Input: State<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lerp<T> = (initial: T, target: T, alpha: number) -> T
|
||||||
|
|
||||||
|
local function solve(T: number, z: number, u: number, t: number): number -- alpha
|
||||||
|
local wn = 2*math.pi / T
|
||||||
|
local wd = wn * math.sqrt(1 - z^2)
|
||||||
|
local a = z * wn
|
||||||
|
|
||||||
|
local s = math.exp(-a*t) * math.cos(wd*t)
|
||||||
|
local v = (u/wn) * math.exp(-a*t) * math.sin(wn*t)
|
||||||
|
return (1-s) + v
|
||||||
|
end
|
||||||
|
|
||||||
|
local lerpable: { [string]: Lerp<any> } = {
|
||||||
|
number = function(v1, v2, a)
|
||||||
|
return v1 + (v2 - v1)*a
|
||||||
|
end :: Lerp<number>,
|
||||||
|
|
||||||
|
CFrame = function(v1, v2, a)
|
||||||
|
return v1:Lerp(v2, a)
|
||||||
|
end :: Lerp<CFrame>,
|
||||||
|
|
||||||
|
Color3 = function(v1, v2, a)
|
||||||
|
return v1:Lerp(v2, a)
|
||||||
|
end :: Lerp<Color3>,
|
||||||
|
|
||||||
|
UDim = function(v1, v2, a)
|
||||||
|
return UDim.new(
|
||||||
|
v1.Scale + (v2.Scale - v1.Scale)*a,
|
||||||
|
v1.Offset + (v2.Offset - v1.Offset)*a
|
||||||
|
)
|
||||||
|
end :: Lerp<UDim>,
|
||||||
|
|
||||||
|
UDim2 = function(v1, v2, a)
|
||||||
|
return v1:Lerp(v2, a)
|
||||||
|
end :: Lerp<UDim2>,
|
||||||
|
|
||||||
|
Vector2 = function(v1, v2, a)
|
||||||
|
return v1:Lerp(v2, a)
|
||||||
|
end :: Lerp<Vector2>,
|
||||||
|
|
||||||
|
Vector3 = function(v1, v2, a)
|
||||||
|
return v1:Lerp(v2, a)
|
||||||
|
end :: Lerp<Vector3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
local activeSprings: { [State<any>]: SpringData<any> } = {}
|
||||||
|
setmetatable(activeSprings, { __mode = "ks" })
|
||||||
|
|
||||||
|
local function spring<T>(input: MaybeState<T>, period: number, damping: number?): State<T>
|
||||||
|
local initial = unwrap(input) :: T
|
||||||
|
local output = create(initial)
|
||||||
|
|
||||||
|
local springData: SpringData<T> = {
|
||||||
|
Alpha = 0,
|
||||||
|
Duration = 0,
|
||||||
|
Period = period,
|
||||||
|
Damping = damping or 1,
|
||||||
|
Velocity = 0,
|
||||||
|
InitialVelocity = 0,
|
||||||
|
Initial = initial,
|
||||||
|
Target = initial,
|
||||||
|
Input = input :: any,
|
||||||
|
LastInput = initial,
|
||||||
|
LastOutput = initial
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSprings[output] = springData
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateSprings(dt: number)
|
||||||
|
for node, data in next, activeSprings do
|
||||||
|
local currentTarget = get(data.Input)
|
||||||
|
if currentTarget ~= data.Target then
|
||||||
|
data.Target = currentTarget
|
||||||
|
data.Initial = get(node)
|
||||||
|
data.Target = get(data.Input)
|
||||||
|
data.Alpha = 0
|
||||||
|
data.Duration = 0
|
||||||
|
data.InitialVelocity = data.Velocity
|
||||||
|
end
|
||||||
|
|
||||||
|
local initial: Animatable = data.Initial
|
||||||
|
local target: Animatable = data.Target
|
||||||
|
local targetType: string = typeof(target)
|
||||||
|
|
||||||
|
if targetType ~= typeof(initial) then
|
||||||
|
activeSprings[node] = nil
|
||||||
|
warn(string.format(
|
||||||
|
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
|
||||||
|
typeof(initial),
|
||||||
|
targetType
|
||||||
|
))
|
||||||
|
throw(`Cannot tween type { typeof(initial) } and { targetType }`)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local lerp: Lerp<Animatable> = lerpable[targetType]
|
||||||
|
|
||||||
|
if lerp == nil then
|
||||||
|
activeSprings[node] = nil
|
||||||
|
throw(`Cannot animate type { targetType }`)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local newTime = data.Duration + dt
|
||||||
|
local newAlpha = solve(data.Period, data.Damping, data.InitialVelocity, newTime)
|
||||||
|
|
||||||
|
data.Velocity = -(newAlpha - data.Alpha)/dt
|
||||||
|
data.Alpha = newAlpha
|
||||||
|
data.Duration = newTime
|
||||||
|
|
||||||
|
local value = lerp(initial, target, newAlpha)
|
||||||
|
|
||||||
|
set(node, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return function() return spring, updateSprings end
|
||||||
15
src/throw.lua
Normal file
15
src/throw.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/throw.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function throw(msg: string)
|
||||||
|
local stack = 1
|
||||||
|
|
||||||
|
while debug.info(stack, "s") == debug.info(1, "s") do
|
||||||
|
stack += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
error(msg, stack)
|
||||||
|
end
|
||||||
|
|
||||||
|
return throw
|
||||||
9
src/unwrap.lua
Normal file
9
src/unwrap.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/unwrap.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
|
||||||
|
return graph.unwrap
|
||||||
36
src/watch.lua
Normal file
36
src/watch.lua
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
-- vide/watch.lua
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type Unwrapper = graph.Unwrapper
|
||||||
|
local setEffect = graph.setEffect
|
||||||
|
local capture = graph.capture
|
||||||
|
local unwrap = graph.unwrap
|
||||||
|
|
||||||
|
local function watch(effect: (Unwrapper) -> ()): () -> ()
|
||||||
|
local states, cleanup = capture(effect :: () -> (() -> ()?))
|
||||||
|
|
||||||
|
local function fn()
|
||||||
|
if cleanup then cleanup(); cleanup = nil end
|
||||||
|
cleanup = effect(unwrap)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, state in next, states do
|
||||||
|
setEffect(state, fn, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unwatch()
|
||||||
|
for _, state in next, states do
|
||||||
|
setEffect(state, fn, nil)
|
||||||
|
end
|
||||||
|
if cleanup then cleanup(); cleanup = nil end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unwatch
|
||||||
|
end
|
||||||
|
|
||||||
|
return watch
|
||||||
42
src/wrap.lua
Normal file
42
src/wrap.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/wrap.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
type State<T> = graph.State<T>
|
||||||
|
type MaybeState<T> = graph.MaybeState<T>
|
||||||
|
local create = graph.create
|
||||||
|
local get = graph.get
|
||||||
|
local set = graph.set
|
||||||
|
local wrapped = graph.wrapped
|
||||||
|
|
||||||
|
local throw = require(script.Parent.throw)
|
||||||
|
local flags = require(script.Parent.flags)
|
||||||
|
|
||||||
|
type Setter<T> = (value: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?) -> T
|
||||||
|
|
||||||
|
local function wrap<T>(value: MaybeState<T>?): (State<T>, Setter<T>)
|
||||||
|
local state = create(if wrapped(value) then get(value :: State<T>) else value :: T)
|
||||||
|
|
||||||
|
local function setter(vi: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?): T
|
||||||
|
if type(vi) == "function" then
|
||||||
|
vi = vi(get(state))
|
||||||
|
end
|
||||||
|
|
||||||
|
local v = if wrapped(vi) then get(vi :: State<T>) else vi :: T
|
||||||
|
|
||||||
|
if v ~= state.cache or force then
|
||||||
|
set(state, v)
|
||||||
|
elseif flags.strict and type(v) == "table" then
|
||||||
|
throw("attempt to set same table object")
|
||||||
|
end
|
||||||
|
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
return state, setter
|
||||||
|
end
|
||||||
|
|
||||||
|
return wrap
|
||||||
9
src/wrapped.lua
Normal file
9
src/wrapped.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- vide/wrapped.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if not game then script = (require :: any) "test/wrap-require" end
|
||||||
|
|
||||||
|
local graph = require(script.Parent.graph)
|
||||||
|
|
||||||
|
return graph.wrapped
|
||||||
155
test/benchmark.luau
Normal file
155
test/benchmark.luau
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- benchmark.lua
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local BENCH, START = require("test/testkit").getBenchmarkTools()
|
||||||
|
|
||||||
|
local vide = require "src/init"
|
||||||
|
type State<T> = vide.State<T>
|
||||||
|
|
||||||
|
local function gc(n: number?)
|
||||||
|
for i = 1, n or 3 do
|
||||||
|
(collectgarbage :: any)("collect")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local N = 1e5
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Create state", function()
|
||||||
|
local cache = table.create(N)
|
||||||
|
local wrap = vide.wrap
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
cache[i] = wrap(1)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Get state value", function()
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
local unwrap = vide.unwrap
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
unwrap(state)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Set state value", function()
|
||||||
|
local _, set = vide.wrap(1)
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
set(i)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derive state", function()
|
||||||
|
local cache = table.create(N)
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
local derive = vide.derive
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
cache[i] = derive(function(from)
|
||||||
|
return from(state)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derive state (2)", function()
|
||||||
|
local cache = table.create(N)
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
local state2 = vide.wrap(2)
|
||||||
|
local derive = vide.derive
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
cache[i] = derive(function(from)
|
||||||
|
return from(state) + from(state2)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derive state (shorthand)", function()
|
||||||
|
local cache = table.create(N)
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
cache[i] = state + 1
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derive state (shorthand 2)", function()
|
||||||
|
local cache = table.create(N)
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
local state2 = vide.wrap(2)
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
cache[i] = state + state2
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derived state update", function()
|
||||||
|
local stateA, setA = vide.wrap(1)
|
||||||
|
local stateB = vide.derive(function(from) return from(stateA) end)
|
||||||
|
local unwrap = vide.unwrap
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
setA(i)
|
||||||
|
unwrap(stateB)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Derived state update (shorthand)", function()
|
||||||
|
local stateA, setA = vide.wrap(1)
|
||||||
|
local stateB = stateA + 1
|
||||||
|
local unwrap = vide.unwrap
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
setA(i)
|
||||||
|
unwrap(stateB)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Apply single property", function()
|
||||||
|
local instance = vide.create("Frame") {}
|
||||||
|
local apply = vide.apply
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
apply(instance) {
|
||||||
|
Name = i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
gc()
|
||||||
|
|
||||||
|
BENCH("Bind state", function()
|
||||||
|
local instance = vide.create("Frame") {}
|
||||||
|
local state = vide.wrap(1)
|
||||||
|
local apply = vide.apply
|
||||||
|
|
||||||
|
for i = 1, START(N) do
|
||||||
|
apply(instance) {
|
||||||
|
Name = state
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
return nil
|
||||||
187
test/goodsignal.lua
Normal file
187
test/goodsignal.lua
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
-- wrapped task library for use in pure luau
|
||||||
|
local task = { spawn = function(thread, ...)
|
||||||
|
local ok, err = coroutine.resume(thread, ...)
|
||||||
|
if not ok then error(err, 3) end
|
||||||
|
end }
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
-- Batched Yield-Safe Signal Implementation --
|
||||||
|
-- This is a Signal class which has effectively identical behavior to a --
|
||||||
|
-- normal RBXScriptSignal, with the only difference being a couple extra --
|
||||||
|
-- stack frames at the bottom of the stack trace when an error is thrown. --
|
||||||
|
-- This implementation caches runner coroutines, so the ability to yield in --
|
||||||
|
-- the signal handlers comes at minimal extra cost over a naive signal --
|
||||||
|
-- implementation that either always or never spawns a thread. --
|
||||||
|
-- --
|
||||||
|
-- API: --
|
||||||
|
-- local Signal = require(THIS MODULE) --
|
||||||
|
-- local sig = Signal.new() --
|
||||||
|
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
|
||||||
|
-- sig:Fire(arg1, arg2, ...) --
|
||||||
|
-- connection:Disconnect() --
|
||||||
|
-- sig:DisconnectAll() --
|
||||||
|
-- local arg1, arg2, ... = sig:Wait() --
|
||||||
|
-- --
|
||||||
|
-- Licence: --
|
||||||
|
-- Licenced under the MIT licence. --
|
||||||
|
-- --
|
||||||
|
-- Authors: --
|
||||||
|
-- stravant - July 31st, 2021 - Created the file. --
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- The currently idle thread to run the next handler on
|
||||||
|
local freeRunnerThread = nil
|
||||||
|
|
||||||
|
-- Function which acquires the currently idle handler runner thread, runs the
|
||||||
|
-- function fn on it, and then releases the thread, returning it to being the
|
||||||
|
-- currently idle one.
|
||||||
|
-- If there was a currently idle runner thread already, that's okay, that old
|
||||||
|
-- one will just get thrown and eventually GCed.
|
||||||
|
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
|
||||||
|
local acquiredRunnerThread = freeRunnerThread
|
||||||
|
freeRunnerThread = nil
|
||||||
|
fn(...)
|
||||||
|
-- The handler finished running, this runner thread is free again.
|
||||||
|
freeRunnerThread = acquiredRunnerThread
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Coroutine runner that we create coroutines of. The coroutine can be
|
||||||
|
-- repeatedly resumed with functions to run followed by the argument to run
|
||||||
|
-- them with.
|
||||||
|
local function runEventHandlerInFreeThread()
|
||||||
|
-- Note: We cannot use the initial set of arguments passed to
|
||||||
|
-- runEventHandlerInFreeThread for a call to the handler, because those
|
||||||
|
-- arguments would stay on the stack for the duration of the thread's
|
||||||
|
-- existence, temporarily leaking references. Without access to raw bytecode
|
||||||
|
-- there's no way for us to clear the "..." references from the stack.
|
||||||
|
while true do
|
||||||
|
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Connection class
|
||||||
|
local Connection = {}
|
||||||
|
Connection.__index = Connection
|
||||||
|
|
||||||
|
function Connection.new(signal, fn)
|
||||||
|
return setmetatable({
|
||||||
|
_connected = true,
|
||||||
|
_signal = signal,
|
||||||
|
_fn = fn,
|
||||||
|
_next = false,
|
||||||
|
}, Connection)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Connection:Disconnect()
|
||||||
|
self._connected = false
|
||||||
|
|
||||||
|
-- Unhook the node, but DON'T clear it. That way any fire calls that are
|
||||||
|
-- currently sitting on this node will be able to iterate forwards off of
|
||||||
|
-- it, but any subsequent fire calls will not hit it, and it will be GCed
|
||||||
|
-- when no more fire calls are sitting on it.
|
||||||
|
if self._signal._handlerListHead == self then
|
||||||
|
self._signal._handlerListHead = self._next
|
||||||
|
else
|
||||||
|
local prev = self._signal._handlerListHead
|
||||||
|
while prev and prev._next ~= self do
|
||||||
|
prev = prev._next
|
||||||
|
end
|
||||||
|
if prev then
|
||||||
|
prev._next = self._next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make Connection strict
|
||||||
|
setmetatable(Connection, {
|
||||||
|
__index = function(tb, key)
|
||||||
|
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
|
||||||
|
end,
|
||||||
|
__newindex = function(tb, key, value)
|
||||||
|
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Signal class
|
||||||
|
local Signal = {}
|
||||||
|
Signal.__index = Signal
|
||||||
|
|
||||||
|
function Signal.new()
|
||||||
|
return setmetatable({
|
||||||
|
_handlerListHead = false,
|
||||||
|
}, Signal)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Signal:Connect(fn)
|
||||||
|
if type(fn) ~= "function" then error(`attempt to connect non function (got { type(fn) })`, 2) end
|
||||||
|
local connection = Connection.new(self, fn)
|
||||||
|
if self._handlerListHead then
|
||||||
|
connection._next = self._handlerListHead
|
||||||
|
self._handlerListHead = connection
|
||||||
|
else
|
||||||
|
self._handlerListHead = connection
|
||||||
|
end
|
||||||
|
return connection
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Disconnect all handlers. Since we use a linked list it suffices to clear the
|
||||||
|
-- reference to the head handler.
|
||||||
|
function Signal:DisconnectAll()
|
||||||
|
self._handlerListHead = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Signal:Fire(...) implemented by running the handler functions on the
|
||||||
|
-- coRunnerThread, and any time the resulting thread yielded without returning
|
||||||
|
-- to us, that means that it yielded to the Roblox scheduler and has been taken
|
||||||
|
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
|
||||||
|
function Signal:Fire(...)
|
||||||
|
local item = self._handlerListHead
|
||||||
|
while item do
|
||||||
|
if item._connected then
|
||||||
|
if not freeRunnerThread then
|
||||||
|
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
|
||||||
|
-- Get the freeRunnerThread to the first yield
|
||||||
|
coroutine.resume(freeRunnerThread)
|
||||||
|
end
|
||||||
|
task.spawn(freeRunnerThread, item._fn, ...)
|
||||||
|
end
|
||||||
|
item = item._next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Implement Signal:Wait() in terms of a temporary connection using
|
||||||
|
-- a Signal:Connect() which disconnects itself.
|
||||||
|
function Signal:Wait()
|
||||||
|
local waitingCoroutine = coroutine.running()
|
||||||
|
local cn;
|
||||||
|
cn = self:Connect(function(...)
|
||||||
|
cn:Disconnect()
|
||||||
|
task.spawn(waitingCoroutine, ...)
|
||||||
|
end)
|
||||||
|
return coroutine.yield()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Implement Signal:Once() in terms of a connection which disconnects
|
||||||
|
-- itself before running the handler.
|
||||||
|
function Signal:Once(fn)
|
||||||
|
local cn;
|
||||||
|
cn = self:Connect(function(...)
|
||||||
|
if cn._connected then
|
||||||
|
cn:Disconnect()
|
||||||
|
end
|
||||||
|
fn(...)
|
||||||
|
end)
|
||||||
|
return cn
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make signal strict
|
||||||
|
setmetatable(Signal, {
|
||||||
|
__index = function(tb, key)
|
||||||
|
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
||||||
|
end,
|
||||||
|
__newindex = function(tb, key, value)
|
||||||
|
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
return Signal
|
||||||
230
test/mock.luau
Normal file
230
test/mock.luau
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
local Instance = {} do
|
||||||
|
local Signal = require "test/goodsignal"
|
||||||
|
|
||||||
|
type userdata = { __USERDATA__: true }
|
||||||
|
|
||||||
|
type Proxy = {
|
||||||
|
_Userdata: userdata,
|
||||||
|
_Data: Data,
|
||||||
|
__index: any,
|
||||||
|
__newindex: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
Name: string,
|
||||||
|
Parent: Data?,
|
||||||
|
Children: { Data },
|
||||||
|
Changed: { [string]: RBXScriptSignal & { Fire: any } },
|
||||||
|
Table: { [string]: unknown },
|
||||||
|
Destroying: RBXScriptSignal,
|
||||||
|
ClassName: string,
|
||||||
|
Type: "Instance"
|
||||||
|
}
|
||||||
|
|
||||||
|
--[[
|
||||||
|
attempt to mimic roblox engine's method of userdata proxy to actual instance data
|
||||||
|
proxy can gc independantly of actual instance data
|
||||||
|
proxy prevents gc of actual instance data
|
||||||
|
luau code never has direct access to actual instance data, only to proxy
|
||||||
|
]]
|
||||||
|
local proxies = {} :: { [Data]: userdata? }
|
||||||
|
setmetatable(proxies :: any, { __mode = "v" })
|
||||||
|
|
||||||
|
local function getdata(userdata: userdata): Data
|
||||||
|
local function getproxy(userdata: userdata): Proxy
|
||||||
|
return getmetatable(userdata :: any)
|
||||||
|
end
|
||||||
|
|
||||||
|
return getproxy(userdata)._Data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isInstance(value: unknown): boolean
|
||||||
|
local mt = getmetatable(value :: any)
|
||||||
|
return mt and mt._Data and mt._Data.Type == "Instance"
|
||||||
|
end
|
||||||
|
|
||||||
|
local methods = {}
|
||||||
|
|
||||||
|
local function __index(userdata: userdata, property: string): ()
|
||||||
|
local data = getdata(userdata)
|
||||||
|
return if methods[property] then methods[property]
|
||||||
|
elseif property == "Name" then data.Name
|
||||||
|
elseif property == "Parent" then data.Parent
|
||||||
|
elseif property == "Destroying" then data.Destroying
|
||||||
|
else data.Table[property]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function __newindex(userdata: userdata, property: string, value: unknown)
|
||||||
|
local data = getdata(userdata)
|
||||||
|
if property == "Name" then
|
||||||
|
data.Name = value :: string
|
||||||
|
elseif property == "Parent" then
|
||||||
|
assert(value == nil or isInstance(value), "attempt to set non-instance as parent")
|
||||||
|
local parent = data.Parent
|
||||||
|
if parent then
|
||||||
|
data.Parent = nil
|
||||||
|
table.remove(parent.Children, table.find(parent.Children, data))
|
||||||
|
end
|
||||||
|
if value then
|
||||||
|
data.Parent = getdata(value :: userdata)
|
||||||
|
table.insert(getdata(value :: userdata).Children, data)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
data.Table[property] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
if data.Changed[property] then
|
||||||
|
data.Changed[property]:Fire()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getuserdata(data: Data): userdata
|
||||||
|
return proxies[data] or (function()
|
||||||
|
local userdata = newproxy(true)
|
||||||
|
local proxy = getmetatable(userdata)
|
||||||
|
proxy._Userdata = userdata
|
||||||
|
proxy._Data = data
|
||||||
|
proxy.__index = __index
|
||||||
|
proxy.__newindex = __newindex
|
||||||
|
proxies[data] = userdata
|
||||||
|
return userdata
|
||||||
|
end)()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance.new(class: string): Instance
|
||||||
|
local data = {
|
||||||
|
Name = "UNNAMED",
|
||||||
|
Parent = nil,
|
||||||
|
Children = {},
|
||||||
|
Changed = {},
|
||||||
|
Table = {},
|
||||||
|
ClassName = class,
|
||||||
|
Destroying = Signal.new() :: any,
|
||||||
|
Type = "Instance" :: "Instance"
|
||||||
|
}
|
||||||
|
|
||||||
|
return getuserdata(data) :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function Instance.isInstance(value: unknown): boolean
|
||||||
|
return isInstance(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.Clone(userdata: userdata): userdata
|
||||||
|
local data = getdata(userdata)
|
||||||
|
local clone_userdata = (Instance.new("") :: any) :: userdata
|
||||||
|
local clone_data = getdata(clone_userdata)
|
||||||
|
|
||||||
|
table.clear(clone_data)
|
||||||
|
for i, v in next, data do
|
||||||
|
clone_data[i] = type(v) == "table" and table.clone(v) or v
|
||||||
|
end
|
||||||
|
return clone_userdata
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
||||||
|
local data = getdata(userdata)
|
||||||
|
for _, child in data.Children do
|
||||||
|
if child.Name == target then
|
||||||
|
return getuserdata(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.GetChildren(userdata: userdata): { userdata }
|
||||||
|
local children = getdata(userdata).Children
|
||||||
|
local userdatas = table.create(#children)
|
||||||
|
|
||||||
|
for i, child in next, children do
|
||||||
|
userdatas[i] = getuserdata(child)
|
||||||
|
end
|
||||||
|
|
||||||
|
return userdatas
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
||||||
|
local data = getdata(userdata)
|
||||||
|
if not data.Changed[property] then
|
||||||
|
data.Changed[property] = Signal.new() :: any
|
||||||
|
end
|
||||||
|
return data.Changed[property]
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.Destroy(userdata: userdata)
|
||||||
|
local data = getdata(userdata);
|
||||||
|
(data.Destroying :: any):Fire()
|
||||||
|
data.Parent = nil
|
||||||
|
if data.Changed["Parent"] then
|
||||||
|
data.Changed["Parent"]:Fire()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Color3 = {} do
|
||||||
|
function Color3.new(r, g, b): Color3
|
||||||
|
return setmetatable({ r = r, g = g, b = b}, Color3) :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function Color3.__eq(a, b)
|
||||||
|
return a.r == b.r and a.g == b.g and a.b == b.b
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Vector3 = {} do
|
||||||
|
function Vector3.new(x, y, z): Vector3
|
||||||
|
return setmetatable({ x = x, y = y, z = z}, Vector3) :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__eq(a, b)
|
||||||
|
return a.x == b.x and a.y == b.y and a.z == b.z
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Vector2 = {} do
|
||||||
|
function Vector2.new(x, y): Vector2
|
||||||
|
return setmetatable({ x = x, y = y }, Vector2) :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector2.__eq(a, b)
|
||||||
|
return a.x == b.x and a.y == b.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local UDim2 = {} do
|
||||||
|
function UDim2.fromScale(x, y): UDim2
|
||||||
|
return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2) :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function UDim2.__eq(a, b)
|
||||||
|
return a.x.scale == b.x.scale and
|
||||||
|
b.x.offset == b.x.offset and
|
||||||
|
a.y.scale == b.y.scale and
|
||||||
|
a.y.offset == b.y.offset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local Enum = {} :: any do
|
||||||
|
setmetatable(Enum, { __index = function(self, index)
|
||||||
|
local v = setmetatable({}, { __index = function(self, index)
|
||||||
|
self[index] = true
|
||||||
|
return true
|
||||||
|
end})
|
||||||
|
self[index] = v
|
||||||
|
return v
|
||||||
|
end})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function typeof(v)
|
||||||
|
return Instance.isInstance(v) and "Instance" or type(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Instance = Instance,
|
||||||
|
Color3 = Color3,
|
||||||
|
Vector3 = Vector3,
|
||||||
|
Vector2 = Vector2,
|
||||||
|
UDim2 = UDim2,
|
||||||
|
Enum = Enum,
|
||||||
|
typeof = typeof
|
||||||
|
}
|
||||||
72
test/syntax.luau
Normal file
72
test/syntax.luau
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
local vide = require "src/init"
|
||||||
|
local wrap = vide.wrap
|
||||||
|
local derive = vide.derive
|
||||||
|
local map = vide.map
|
||||||
|
local create = vide.create
|
||||||
|
local unwrap = vide.unwrap
|
||||||
|
local Event = vide.Event
|
||||||
|
local Layout = vide.Layout
|
||||||
|
|
||||||
|
do
|
||||||
|
local count = wrap(0)
|
||||||
|
local count2 = derive(function(from)
|
||||||
|
local c = from(count) * unwrap(count)
|
||||||
|
return c ^ 2
|
||||||
|
end)
|
||||||
|
local x = count2.value
|
||||||
|
|
||||||
|
local count2b = count + 1
|
||||||
|
local xb = unwrap(count2b)
|
||||||
|
end
|
||||||
|
|
||||||
|
local count = wrap(0)
|
||||||
|
local v = count.value
|
||||||
|
|
||||||
|
do
|
||||||
|
local t = map(1, function(i)
|
||||||
|
return ""
|
||||||
|
end)
|
||||||
|
|
||||||
|
local t = map({ true }, function(i, v)
|
||||||
|
return 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
local data = wrap { "" }
|
||||||
|
|
||||||
|
local t = map(data, function(i, v)
|
||||||
|
return 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
create("Frame") {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
local Value, Computed, peek, OnEvent = nil :: any, nil :: any, nil :: any, nil :: any
|
||||||
|
local New = nil :: any
|
||||||
|
|
||||||
|
local function Counter(props)
|
||||||
|
local count, set = wrap(0)
|
||||||
|
|
||||||
|
return create("TextLabel") {
|
||||||
|
Text = "Count: " .. count,
|
||||||
|
|
||||||
|
[Layout] = props[Layout],
|
||||||
|
|
||||||
|
[Event.Activated] = function()
|
||||||
|
set(count + 1)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Frame(props)
|
||||||
|
return create("Frame") {
|
||||||
|
Position = props.Position
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Frame { Position = UDim2.fromScale(0.5, 0.5) }
|
||||||
|
|
||||||
|
Frame { Position = positionState }
|
||||||
321
test/testkit.luau
Normal file
321
test/testkit.luau
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- testkit.luau
|
||||||
|
-- v0.2.0
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--[[
|
||||||
|
|
||||||
|
EXAMPLE USAGE:
|
||||||
|
|
||||||
|
local testkit = require "path-to-testkit"
|
||||||
|
|
||||||
|
local TEST, CASE, CHECK = testkit.getUnitTestTools()
|
||||||
|
|
||||||
|
TEST("test name", function()
|
||||||
|
do CASE "A"
|
||||||
|
CHECK(condition)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local BENCH, START = testkit.getBenchmarkTools()
|
||||||
|
|
||||||
|
BENCH("benchmark name", function()
|
||||||
|
local x = 0
|
||||||
|
|
||||||
|
for i = 1, START(1e6) do
|
||||||
|
x += 1
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
]]
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- Unit Testing
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Test = {
|
||||||
|
name: string,
|
||||||
|
activeCase: Case?,
|
||||||
|
cases: { Case },
|
||||||
|
duration: number,
|
||||||
|
error: string?
|
||||||
|
}
|
||||||
|
|
||||||
|
type Case = {
|
||||||
|
name: string,
|
||||||
|
result: number,
|
||||||
|
line: number?
|
||||||
|
}
|
||||||
|
|
||||||
|
local PASS = 1
|
||||||
|
local FAIL = 2
|
||||||
|
local NONE = 3
|
||||||
|
local ERROR = 4
|
||||||
|
|
||||||
|
local activeTest: Test?
|
||||||
|
local tests: { Test } = {}
|
||||||
|
|
||||||
|
local function outputTestResults(test: Test)
|
||||||
|
print("\27[1;4m"..test.name.."\27[0m")
|
||||||
|
for _, case in test.cases do
|
||||||
|
print(
|
||||||
|
"[" ..
|
||||||
|
(if case.result == PASS then
|
||||||
|
"\27[32;1mPASS\27[0m"
|
||||||
|
elseif case.result == FAIL then
|
||||||
|
"\27[31;1mFAIL:"..assert(case.line).."\27[0m"
|
||||||
|
elseif case.result == NONE then
|
||||||
|
"\27[33;1mNONE\27[0m"
|
||||||
|
else
|
||||||
|
"\27[41;1;30mERROR\27[0m")
|
||||||
|
.. "] " .. case.name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if test.error then
|
||||||
|
print("\27[31;1;30merror: " .. test.error .. "\27[0m")
|
||||||
|
end
|
||||||
|
|
||||||
|
print ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CASE(name: string)
|
||||||
|
assert(activeTest, "no active test")
|
||||||
|
|
||||||
|
local case: Case = {
|
||||||
|
name = name,
|
||||||
|
result = NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTest.activeCase = case
|
||||||
|
table.insert(activeTest.cases, case)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CHECK(value: any): boolean
|
||||||
|
assert(activeTest, "no active test")
|
||||||
|
local activeCase = activeTest.activeCase
|
||||||
|
|
||||||
|
if not activeCase then
|
||||||
|
CASE ""
|
||||||
|
activeCase = activeTest.activeCase
|
||||||
|
end; assert(activeCase, "no active case")
|
||||||
|
|
||||||
|
local result = value and PASS or FAIL
|
||||||
|
|
||||||
|
if activeCase.result == NONE or activeCase.result == PASS then
|
||||||
|
activeCase.result = result
|
||||||
|
activeCase.line = debug.info(2, "l")
|
||||||
|
end
|
||||||
|
|
||||||
|
return result == PASS
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TEST(name: string, fn: () -> ())
|
||||||
|
assert(not activeTest, "new test was started while a test was in progress")
|
||||||
|
local test: Test = {
|
||||||
|
name = name,
|
||||||
|
cases = {},
|
||||||
|
duration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTest = test
|
||||||
|
table.insert(tests, test)
|
||||||
|
|
||||||
|
local start = os.clock()
|
||||||
|
local msg: string?
|
||||||
|
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||||
|
test.duration = os.clock() - start
|
||||||
|
|
||||||
|
if not test.activeCase then CASE "" end
|
||||||
|
assert(test.activeCase, "no active case")
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
test.activeCase.result = ERROR
|
||||||
|
test.error = msg
|
||||||
|
end
|
||||||
|
|
||||||
|
activeTest = nil
|
||||||
|
outputTestResults(test)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FINISH(): boolean
|
||||||
|
local success = true
|
||||||
|
local totalCases = 0
|
||||||
|
local passedCases = 0
|
||||||
|
local duration = 0
|
||||||
|
|
||||||
|
for _, test in tests do
|
||||||
|
duration += test.duration
|
||||||
|
for _, case in test.cases do
|
||||||
|
totalCases += 1
|
||||||
|
if case.result == PASS or case.result == NONE then
|
||||||
|
passedCases += 1
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print(string.format("%d/%d test cases passed in %.3f ms.", passedCases, totalCases, duration*1e3))
|
||||||
|
|
||||||
|
local fails = totalCases - passedCases
|
||||||
|
|
||||||
|
print(string.format("\27[%d;1;30m%d fail%s\27[0m", fails > 0 and 41 or 42, fails, fails == 1 and "" or "s"))
|
||||||
|
|
||||||
|
return success, table.clear(tests)
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- Benchmarking
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Bench = {
|
||||||
|
timeStart: number?,
|
||||||
|
memStart: number?,
|
||||||
|
iterations: number?
|
||||||
|
}
|
||||||
|
|
||||||
|
local activeBench: Bench? = nil
|
||||||
|
|
||||||
|
function START(iter: number?): number
|
||||||
|
local n = iter or 1
|
||||||
|
if n < 1 then error("iteration count must be greater than 0", 2) end
|
||||||
|
assert(activeBench, "no active benchmark")
|
||||||
|
assert(not activeBench.timeStart, "clock was already started")
|
||||||
|
|
||||||
|
activeBench.iterations = n
|
||||||
|
activeBench.memStart = gcinfo()
|
||||||
|
activeBench.timeStart = os.clock()
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BENCH(name: string, fn: () -> ())
|
||||||
|
assert(not activeBench, "cannot run benchmark, a benchmark is already in progress")
|
||||||
|
|
||||||
|
local bench: Bench = {}
|
||||||
|
activeBench = bench
|
||||||
|
|
||||||
|
local memStart = gcinfo()
|
||||||
|
local timeStart = os.clock()
|
||||||
|
local msg: string?
|
||||||
|
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||||
|
local timeStop = os.clock()
|
||||||
|
local memStop = gcinfo()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
print("[\27[41;1mERROR\27[0m] " .. name)
|
||||||
|
print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m")
|
||||||
|
activeBench = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
timeStart = bench.timeStart or timeStart
|
||||||
|
memStart = bench.memStart or memStart
|
||||||
|
|
||||||
|
local n = bench.iterations or 1
|
||||||
|
local duration = timeStop - timeStart
|
||||||
|
local allocated = memStop - memStart
|
||||||
|
|
||||||
|
print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name))
|
||||||
|
|
||||||
|
activeBench = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- Printing
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function printa(v: unknown)
|
||||||
|
type Buffer = { n: number, [number]: string }
|
||||||
|
|
||||||
|
-- overkill concatenationless string buffer
|
||||||
|
local function tos(value: any, stack: number, str: Buffer)
|
||||||
|
local TAB = " "
|
||||||
|
local indent = table.concat(table.create(stack, TAB))
|
||||||
|
|
||||||
|
if type(value) == "string" then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = "\""
|
||||||
|
str[n + 2] = value
|
||||||
|
str[n + 3] = "\""
|
||||||
|
str.n = n + 3
|
||||||
|
elseif type(value) ~= "table" then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = value == nil and "nil" or tostring(value)
|
||||||
|
str.n = n + 1
|
||||||
|
elseif next(value) == nil then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = "{}"
|
||||||
|
str.n = n + 1
|
||||||
|
else
|
||||||
|
local tabbed_indent = indent .. TAB
|
||||||
|
|
||||||
|
str.n += 1
|
||||||
|
str[str.n] = "{\n"
|
||||||
|
|
||||||
|
local i, v = next(value, nil)
|
||||||
|
while v ~= nil do
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = tabbed_indent
|
||||||
|
|
||||||
|
if type(i) ~= "string" then
|
||||||
|
str[n + 2] = "["
|
||||||
|
str[n + 3] = tostring(i)
|
||||||
|
str[n + 4] = "]"
|
||||||
|
n += 4
|
||||||
|
else
|
||||||
|
str[n + 2] = tostring(i)
|
||||||
|
n += 2
|
||||||
|
end
|
||||||
|
|
||||||
|
str[n + 1] = " = "
|
||||||
|
str.n = n + 1
|
||||||
|
|
||||||
|
tos(v, stack + 1, str)
|
||||||
|
|
||||||
|
i, v = next(value, i)
|
||||||
|
|
||||||
|
n = str.n
|
||||||
|
str[n + 1] = v ~= nil and ",\n" or "\n"
|
||||||
|
str.n = n + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = indent
|
||||||
|
str[n + 2] = "}"
|
||||||
|
str.n = n + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local str = { n = 0 }
|
||||||
|
tos(v, 0, str)
|
||||||
|
print(table.concat(str))
|
||||||
|
end
|
||||||
|
|
||||||
|
printa "string"
|
||||||
|
|
||||||
|
printa(1)
|
||||||
|
|
||||||
|
printa {
|
||||||
|
hello = 1,
|
||||||
|
bye = "ok",
|
||||||
|
|
||||||
|
test = {
|
||||||
|
1, 2, 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
-- Return
|
||||||
|
------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUnitTestTools = function()
|
||||||
|
return TEST, CASE, CHECK, FINISH
|
||||||
|
end,
|
||||||
|
|
||||||
|
getBenchmarkTools = function()
|
||||||
|
return BENCH, START
|
||||||
|
end,
|
||||||
|
|
||||||
|
printa = printa
|
||||||
|
}
|
||||||
1293
test/tests.luau
Normal file
1293
test/tests.luau
Normal file
File diff suppressed because it is too large
Load diff
8
test/wrap-require.lua
Normal file
8
test/wrap-require.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
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
|
||||||
6
todo.md
Normal file
6
todo.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Todo
|
||||||
|
|
||||||
|
- investigate solid js `for` and batching
|
||||||
|
- equality checking of derived state updates to prevent redundant updates of derived state
|
||||||
|
- watcher callbacks when watcher watches a parent and child
|
||||||
|
- stores
|
||||||
26
vide.code-snippets
Normal file
26
vide.code-snippets
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"import vide": {
|
||||||
|
"scope": "lua",
|
||||||
|
"prefix": "getvide",
|
||||||
|
"body": [
|
||||||
|
"local vide = require($1.vide)",
|
||||||
|
"type State<T> = vide.State<T>",
|
||||||
|
"type Prop<T> = vide.Prop<T>",
|
||||||
|
"local create = vide.create",
|
||||||
|
"local apply = vide.apply",
|
||||||
|
"local wrap = vide.wrap",
|
||||||
|
"local derive = vide.derive",
|
||||||
|
"local foreach = vide.foreach",
|
||||||
|
"local match = vide.match",
|
||||||
|
"local watch = vide.watch",
|
||||||
|
"local spring = vide.spring",
|
||||||
|
"local Event = vide.Event",
|
||||||
|
"local Changed = vide.Changed",
|
||||||
|
"local Bind = vide.Bind",
|
||||||
|
"local Layout = vide.Layout",
|
||||||
|
"local Children = vide.Children",
|
||||||
|
"local Created = vide.Created",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wally.toml
Normal file
10
wally.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "centau/ecr"
|
||||||
|
description = "A fast, lightweight Luau ECS library."
|
||||||
|
version = "0.4.0"
|
||||||
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
|
realm = "shared"
|
||||||
|
include = ["src", "LICENSE.md", "default.project.json"]
|
||||||
|
exclude = ["CHANGELOG.md", ".gitignore", ".github", ".gitattributes", "test", "docs", "README.md", ".luaurc"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue