Initial commit

This commit is contained in:
aaron 2023-08-08 21:33:11 +01:00
commit cb002f4f27
50 changed files with 4666 additions and 0 deletions

57
docs/.vitepress/config.ts Normal file
View file

@ -0,0 +1,57 @@
import { defineConfig } from "vitepress"
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Vide",
description: "A declarative and reactive library for Luau.",
base: "/vide/",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "Tutorials", link: "/tut/crash-course/1-introduction" },
{ text: "API", link: "/api/reactivity-core"},
{ text: "GitHub", link: "https://github.com/centau/vide" }
],
sidebar: {
"/api/": [
{
text: "API",
items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" },
]
}
],
"/tut/": [
{
text: "Crash Course",
items: [
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
{ text: "Components", link: "/tut/crash-course/3-components" },
{ text: "State", link: "/tut/crash-course/4-state" },
{ text: "Derived State", link: "/tut/crash-course/5-derived-state" },
{ text: "Table State", link: "/tut/crash-course/6-table-state" },
{ text: "Property Groups", link: "/tut/crash-course/7-property-groups" },
]
},
{
text: "Tutorials",
items: [
{ text: "Crash Course", link: "/tut/crash-course/index" },
]
}
],
}
// socialLinks: [
// { icon: "github", link: "https://github.com/centau/vide" }
// ]
}
})

View file

@ -0,0 +1,4 @@
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './vars.css'
export default DefaultTheme

View file

@ -0,0 +1,7 @@
:root {
--vp-c-brand: #3086ff;
--vp-c-green-lighter: #02a5fd;
--vp-c-green-dark: #2b4efd;
--vp-c-green-darker: #5c2bfd;
}

37
docs/api/animation.md Normal file
View file

@ -0,0 +1,37 @@
# Animation API
## spring()
Returns a new state with a dynamically animated value of the source.
- **Type**
```lua
function spring<T>(
source: () -> T & Animatable,
period: number = 1,
damping_ratio: number = 1
): () -> T
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
```
- **Details**
The output state value is updated every frame based on the source state
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 oscillation.
`damping_ratio` is the amount of resistance applied to the spring.
- \>1 = Overdamped (not currently supported).
- 1 = Critically damped - reaches target without any overshoot.
- <1 = Underdamped - reaches target with some overshoot.
- 0 = Undamped - never stabilizes, oscillates forever.
Velocity is conserved between source state updates for smooth animation.

79
docs/api/creation.md Normal file
View file

@ -0,0 +1,79 @@
# Element Creation API
<br/>
## create()
Creates a new UI element, applying any given properties.
- ### Type
```lua
function create(class: string): (Properties) -> Instance
function create(instance: Instace): (Properties) -> Instance
type Properties = Map<string|number, any>
```
- ### Details
The function can take either a `string` or an `Instance` as its first argument.
- If given a `string`, a new instance with the same class name will be created.
- If given an `Instance`, a new instance that is a clone of the given instance
will be created.
This returns another function that is used to apply any properties to the new
instance.
- ### Property setting rules
- If a table value is another table, that nested table is processed so that
any properties inside that table are also applied to the instance just
like the outer table.
- If a table index is a string:
- If its value is a function then it will either bind that property to
a state or connect it if the property type is a `RBXScriptSignal`.
- If the value is not a function then the property will be set to that
value.
- If a table index is a number:
- If its value is a function then it will parent any instances returned by
that function as children.
- If its value is an instance then it will be parented to the instance.
- ### Example
Basic element creation.
```lua
local frame = create "Frame" {
Name = "NewFrame",
Position = UDim2.fromScale(1, 0)
}
```
A component using property nesting/grouping.
```lua
type Layout = {
Layout = {
Position: UDim2?,
Size: UDim2?,
AnchorPoint: Vector2?
}
}
type Children = {
Children = Array<Instance>
}
function Background(props: Layout & Children & {
Color: Color3
})
return create "Frame" {
BackgroundColor3 = Color,
props.Layout,
props.Children
}
end
```

250
docs/api/reactivity-core.md Normal file
View file

@ -0,0 +1,250 @@
# Reactivity API: Core
<br/>
## source()
Creates a new source state with the given value.
- **Type**
```lua
function source<T>(value: T): (T?) -> T
```
- **Details**
Calling the returned state with no arguments will return its stored value,
calling with arguments will set a new value.
Reading from the state from within any reactive scope will cause changes
to that state to be tracked and anything depending on it to update.
- **Example**
```lua
local count = source(0)
count() -- 0
count(count() + 1) -- 1
```
## watch()
Runs a callback on state change.
- **Type**
```lua
function watch(callback: () -> ()): Unwatch
type Unwatch = () -> ()
```
- **Details**
The callback is ran immediately to determine what states are referenced.
Any time a state referenced in the callback is changed, the callback will be
reran.
Also returns a function that when called, stops the watcher immediately.
::: warning
`callback()` cannot yield.
:::
- **Example**
```lua
local state = wrap(1)
watch(function()
print(state.Value)
end)
-- prints 1
state.Value += 1
-- prints 2
```
## derive()
Derives a new state from existing states.
- **Type**
```lua
function derive<T>(source: () -> T): () -> T
```
- **Details**
The derived state will have its value recalculated when any source state it
derives from is updated.
Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
Takes a callback that is immediately run to determine what states are being
referenced.
::: warning
`source()` cannot yield.
:::
- **Example**
```lua
local count = wrap(0)
local text = derive(function() return `count: {count()}` end)
text() -- "count: 0"
count(1)
text() -- "count: 1"
```
## indexes()
Maps each index in a table to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a state containing the index's value and
the second argument is just the index.
Anytime a new index is added, the transform function will be called again for
that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the passed state for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(numbers, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a state containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the passed state for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(numbers, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -0,0 +1,25 @@
# Reactivity API: Utility
## cleanup()
Runs a callback anytime a reactive scope is re-ran.
- **Type**
```lua
function cleanup(callback: () -> ())
```
- **Example**
```lua
local data = source(1)
watch(function()
local label = create "TextLabel" { Text = data }
cleanup(function()
label:Destroy()
end)
end)
```

25
docs/api/strict-mode.md Normal file
View file

@ -0,0 +1,25 @@
# Strict Mode
Vide has a special mode called "strict mode" which is used for debugging.
The purpose of strict mode is to help ensure stateful code is *pure*
(deterministic and free from side effects) or if there are side-effects, that
they are cleaned up correctly.
Vide is set to strict by doing:
```lua
local vide = require(path_to_vide)
vide.strict = true
```
What strict mode will do:
1. Run derived callbacks twice when re-evaluating.
2. Run watcher callbacks twice when a state changes.
3. Throw an error if yields occur where they are not allowed.
4. Checks for `map()` returning primitive values.
5. Better error reporting and stack traces.
It is recommend to develop UI with strict mode and to disable it when pushing to
production.

21
docs/index.md Normal file
View file

@ -0,0 +1,21 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: Vide
text: ""
tagline: A declarative and reactive library for Luau.
actions:
- theme: brand
text: Tutorials
link: /tut/crash-course/1-introduction
- theme: alt
text: API Reference
link: /api/reactivity-core
features:
- title: In Development
details: Do not use for production
---

13
docs/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"type": "module",
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"vitepress": "^1.0.0-beta.6"
}
}

View file

@ -0,0 +1,23 @@
# Introduction
This is a brief tutorial designed to give you a quick run through the usage of
Vide.
Vide is largely inspired by other UI libraries such as Solid and Fusion.
## Why Vide?
Creating UI is a slow and tedious process. The purpose of Vide is to make UI
declarative and concise, making it faster to create and more importantly easier
to maintain. Vide achieves this using a reactive style of programming which
allows you to focus on the flow of data through your application without
worrying about manually updating UI instances.
Some of the main focuses behind Vide's design choices:
- Concise syntax to reduce verbosity as much as possible.
- Reducing the amount of imports needed for usage by using Luau's syntax and
semantics.
- Being completely typecheckable.
- Flexibility, particularly with integrating other libraries and allowing users
to use their own patterns.

View file

@ -0,0 +1,60 @@
# Creating UI Elements
Instances are created using [`create()`](../../api/creation.md#create).
```lua
local vide = require(path_to_vide)
local create = vide.create
```
`create()` returns a constructor for a given class which then takes a table of
properties to assign when creating a new instance for that class.
```lua
local frame = create "Frame" {
Name = "Background",
Position = UDim2.fromScale(0.5, 0.5)
}
```
String keys are assigned as properties and integer keys are assigned as child
instances.
```lua
create "ScreenGui" {
Parent = game.StarterGui,
create "Frame" {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromScale(0.4, 0.7),
create "TextLabel" {
Text = "hi"
},
create"TextLabel" {
Text = "bye"
}
}
}
```
To connect to an event, just set the event property name to a function.
All event arguments are passed into the function.
```lua
create "TextButton" {
Activated = function()
print "clicked!"
end
}
```
In short:
- String keys = properties
- Function values = events
- Non-function values = property values
- Numeric keys = children

View file

@ -0,0 +1,47 @@
# Components
Components are custom-made reusable pieces of UI made from other pieces of UI.
Using components you make your application more modular and better organized.
Components leverage functions to create self-contained UI that can even have
its own state and behavior.
```lua
local function Button(props: {
Position: UDim2,
Text: string,
Callback: () -> ()
})
return create "TextButton" {
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
Size = UDim2.fromOffset(400, 250),
Position = props.Position,
Text = props.Text,
Callback = props.Callback
}
end
local button = Button {
Position = UDim2.new(),
Text = "Click me!",
Callback = function()
print "clicked"
end
}
```
Above is a simple example of a button component with its background color set to
a dark grey and with a fixed size.
A single parameter `props` is used to pass properties to the component.
Components allow you to *encapsulate* behavior. You can only modify the
component in ways that you allow in the component.
This also promotes code reusability. Anytime you want a new button all you do
is call `Button {}` instead of creating and setting every property each time.
This can be extended to much more complicated UI.

View file

@ -0,0 +1,56 @@
# State
State in Vide are the core of reactivity in Vide.
State contain values that can change, and when they do change, automatically
update anything that is using it.
A state object in Vide can be created using
[`source()`](../../api/reactivity-core.md#source).
```lua
local source = vide.source
```
```lua
local count = source(0)
```
The value of a state can be set by calling it with an argument, and can be read
by calling it with no arguments.
```lua
count(count() + 1) -- increment count state by 1
```
Below is an example of a counter component that has state.
```lua
local function Counter()
local count = source(0)
return create "TextButton" {
Text = count,
Activated = function()
count(count() + 1)
end
}
end
```
Any time the source value is set, anything depending on it will automatically be
updated using the new value.
Vide detects when you assign a function to a property. This is known
as *binding* and doing so will cause the property to *automatically* update
whenever a state in that function is updated, by rerunning the function and
assigning its return value. You can only bind non-event
properties, otherwise the function is connected as the event callback.
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 update when changes occur.
Each call of `Counter {}` will create a new counter element, each with their own
independent count state.

View file

@ -0,0 +1,58 @@
# Derived State
You can create new state from existing states. This is known as *deriving
state*.
A function that wraps a state effectively becomes a state. If a state used
inside a function is updated, the whole function can be re-ran to recompute
its value.
```lua
local count = source(0)
local function text()
return "count: " .. count()
end
create "TextLabel" {
Text = text
}
```
Sometimes when using expensive computations to derive state, you only want to
recalculate it once when a source state has changed
If you wrap a source state with a regular function, its value will be recomputed
every time you call that function.
[`derive()`](../../api/reactivity-core.md#derive) accepts a functions whose
return value will be cached, so that subsequent calls of this derived state
will return the same cached value until one of its source states have changed.
```lua
local derive = vide.derive
```
```lua
local count = source(0)
local factorial = derive(function()
local n = 1
for i = 2, count() do
n *= i
end
return n
end)
```
This can improve performance for expensive calculations.
```lua
create "TextLabel" {
Text = function()
return "factorial squared: " .. factorial() * factorial()
end
}
count(3) -- displays "factorial squared: 36"
count(4) -- displays "factorial squared: 576"
```

View file

@ -0,0 +1,35 @@
# Table State
Vide has functions for dealing with table states.
Below is an example using the above `List` class.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
List {
Children = indexes(items, function(item, i)
return create "ImageLabel" {
Image = function()
return "rbxassetid://" .. item().Icon
end,
LayoutOrder = i
}
end)
}
```
Here we map each element in `items` to a value returned by a callback.
The callback is called only *once* per key. The first argument given to the
callback is a state that has the value of the table key's value.
Anytime the value of the corresponding table key changes, the state value
changes too. This saves us from having to recreate a UI element any time a
table index changes.

View file

@ -0,0 +1,98 @@
# Property Groups
Often when creating components from existing components, you can find yourself
repetitively passing through properties such as size or position.
```lua
function Background(props: {
Color: Color3,
AnchorPoint: UDim2,
Position: UDim2,
Size: UDim2
})
return create "Frame" {
Color = props.Color
AnchorPoint = props.AnchorPoint,
Position = props.Position,
Size = props.Size
}
end
function Menu(props: {
Color = props.Color
AnchorPoint: UDim2,
Position: UDim2,
Size: UDim2
})
return Background {
Color = props.COlor,
AnchorPoint = props.AnchorPoint,
Position = props.Position,
Size = props.Size
}
end
```
One way this can be avoided is by using *property nesting*. In Vide, passign a
table value inside `props` has special semantics. Any key with a table value is
not assigned like a property, instead the table is iterated and processed just
like the outer table is. Any properties in the nested table will be assigned
to the instance just the same.
Below is an example of how you can use this to pass groups of similar properties
together such as position and size, while also using typechecking.
```lua
type Layout = {
Layout = {
Position: UDim2?,
Size: UDim2?,
AnchorPoint: Vector2?
}
}
function Background(props: Layout & { Color: Color3 })
return create "Frame" {
Color = props.Color,
props.Layout
}
end
function Menu(props: Layout & { Color: Color3 })
return Background {
Color = props.Color,
Layout = props.Layout
}
end
```
Here we created a nested group with the key `Layout` that can accept
layout-related properties. Any name could be chosen for the key.
This allows us to write much more concise syntax that is also typecheckable.
The same can be done for properties such as children to pass table of instances.
```lua
type Children = {
Children = Array<Instance>
}
local function List(props: Children & Layout)
return create "Frame" {
props.Layout,
props.Children,
create "UIListLayout" {}
}
end
List {
Layout = {
Position = UDim2.new()
},
Children = {
create "TextLabel" { Text = "1" },
create "TextLabel" { Text = "2" }
}
}
```