diff --git a/docs/api/animation.md b/docs/api/animation.md index 8370d14..fc64c68 100644 --- a/docs/api/animation.md +++ b/docs/api/animation.md @@ -1,56 +1,39 @@ # Animation API -
- ## spring() -Returns a new state with an animated value of the original. +Returns a new state with a dynamically animated value of the source. -### Type +- ### Type -```lua -function spring(state: State, period: number, dampingRatio: number = 1): State -``` + ```lua + function spring( + source: () -> T & Animatable, + period: number = 1, + damping_ratio: number = 1 + ): () -> T -### Details + type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 + ``` -The output state's value is updated every frame based on the current input state's value. +- ### Details -The change is physically simulated according to a [spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion). + The output state value is updated every frame based on the source state + value. -`period` is the amount of time in seconds it takes for the spring to complete one full cycle + The change is physically simulated according to a + [spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion). -`dampingRatio` is relared to the amount of resistant force applied to the spring. + `period` is the amount of time in seconds it takes for the spring to + complete one full oscillation. -- \>1 = Overdamped (Not currently supported) -- 1 = Critically damped -- <1 = Underdamped -- 0 = Undamped + `damping_ratio` is the amount of resistance applied to the spring. -Velocity is conserved between input state updates for smooth animation. + - \>1 = Overdamped (not currently supported). + - 1 = Critically damped - reaches target without any overshoot. + - <1 = Underdamped - reaches target with some overshoot. + - 0 = Undamped - never stabilizes, oscillates forever. -### Example + Velocity is conserved between source state updates for smooth animation. -```lua -local state = wrap(1) - -local springed = spring(state, 1, 1) -``` - -
Example of an animated counter - -```lua -local count = wrap(1000) - -local function Counter(props) - local tweenedCount = spring(count, 0.5, 1) - - return create("TextLabel") { - Text = "Count: " .. tweenedCount - } -end -``` - -
- -------------------------------------------------------------------- +-------------------------------------------------------------------------------- diff --git a/docs/api/creation.md b/docs/api/creation.md index 7ca416e..cfd7816 100644 --- a/docs/api/creation.md +++ b/docs/api/creation.md @@ -6,320 +6,76 @@ Creates a new UI element, applying any given properties. -### Type +- ### Type -```lua -function create(classNameOrInstance: string | Instance): (properties: Map) -> Instance -``` + ```lua + function create(class: string): (Properties) -> Instance + function create(instance: Instace): (Properties) -> Instance -### Details + type Properties = Map + ``` -The function can take either a `string` or an `Instance` as its first argument. +- ### Details -- If given a `string`, a new instance with the string class name will be created with default properties already applied. -- If given an `Instance`, a new instance that is a clone of the given instance will be created. + The function can take either a `string` or an `Instance` as its first argument. -This returns another function that is used to apply any properties to the new instance. + - If given a `string`, a new instance with the same class name will be created. + - If given an `Instance`, a new instance that is a clone of the given instance + will be created. -### Example + This returns another function that is used to apply any properties to the new + instance. -```lua -local frame = create("Frame") { - Name = "NewFrame", - Position = UDim2.fromScale(1, 0) -} +- ### Property setting rules --- creates a clone of `frame` with new properties applied. -local frame2 = create(frame) { - Size = UDim2.fromOffset(50, 100) -} -``` + - 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. -## apply() - -Applies any given properties to a given instance. - -### Type - -```lua -function apply(instance: Instance): (properties: Map) -> 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) -} -``` - -------------------------------------------------------------------- - -
- -## 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?; - LayoutOrder: Prop?; - Position: Prop?; - Rotation: Prop?; - Size: Prop?; - SizeConstraint: Prop?; - Visible: Prop?; - ZIndex: Prop?; + ```lua + local frame = create "Frame" { + Name = "NewFrame", + Position = UDim2.fromScale(1, 0) } -} + ``` -type Prop = T | State -``` + A component using property nesting/grouping. -### 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) - } -} -``` - -------------------------------------------------------------------- - -
- -## Children - -Symbol used to pass child instances to elements. - -### Type - -```lua -type Children = Symbol - -type ChildrenProps = { - [Symbol] = ChildrenProp -} - -type ChildrenProp = Prop | Array -``` - -### 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") {}, + ```lua + type Layout = { + Layout = { + Position: UDim2?, + Size: UDim2?, + AnchorPoint: Vector2? } } -} -``` -------------------------------------------------------------------- + type Children = { + Children = Array + } -
- -## Event - -Symbol used to connect callbacks to instance events. - -### Type - -```lua -type Event = Map - -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") + function Background(props: Layout & Children & { + Color: Color3 + }) + return create "Frame" { + BackgroundColor3 = Color, + props.Layout, + props.Children + } end -} -``` + ``` -------------------------------------------------------------------- - -
- -## Changed - -Symbol used to connect callbacks to instance property changed events. - -### Type - -```lua -type Changed = Map - -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 -} -``` - -------------------------------------------------------------------- - -
- -## Bind - -Symbol used to bind states to instance properties. - -### Type - -```lua -type Bind = Map - -type BindProps = { - [Symbol] = State -} -``` - -### 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" -``` - -------------------------------------------------------------------- - -
- -## 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" -``` - -------------------------------------------------------------------- +-------------------------------------------------------------------------------- diff --git a/docs/api/reactivity-core.md b/docs/api/reactivity-core.md index 4f91693..e534cde 100644 --- a/docs/api/reactivity-core.md +++ b/docs/api/reactivity-core.md @@ -2,248 +2,167 @@
-## wrap() +## source() -Wraps and returns any given values with reactive state objects. +Creates a new source state with the given value. -### Type +- ### Type -```lua -function wrap(value: T): State -function wrap(value: ...unknown): ...State + ```lua + function source(value: T): (T?) -> T + ``` -type State = { - Value: T, - value: T -} -``` +- ### Details -### Details + Calling the returned state with no arguments will return its stored value, + calling with arguments will set a new value. -The state object has a single mutable field `.Value`. + Reading from the state from within any reactive scope will cause changes + to that state to be tracked and anything depending on it to update. -Read operations to `.Value` are tracked and write operations can trigger -dependency updates and watchers. +- ### Example -### Example + ```lua + local count = source(0) -```lua -local count = wrap(0) + count() -- 0 -print(count.Value) -- 0 + count(count() + 1) -- 1 + ``` -count.Value += 1 - -print(count.Value) -- 1 -``` - -------------------------------------------------------------------- - -
+-------------------------------------------------------------------------------- ## derive() -Derives a new reactive state object from an existing state object. +Derives a new state from existing states. -### Type +- ### Type -```lua -function derive( - (from) -> T, - cleanup: (value: T) -> ()? -): State + ```lua + function derive(source: () -> T): () -> T + ``` -function from(T | State): T -``` +- ### Details -### Details + The derived state will have its value recalculated when any source state it + derives from is updated. -The derived state will have its value recalculated when any state it derives from is updated. + Anytime its value is recalculated it is also cached, subsequent calls will + retun this cached value until it recalculates again. -Takes a callback that is immediately run to determine what states are being referenced. Only states referenced in the immediate function scope can trigger updates. + Takes a callback that is immediately run to determine what states are being + referenced. -The state object returned by this function is readonly. +> ⚠️ Non-yielding. -Has an optional cleanup parameter which takes a function that is called with the old value any -time the derived state recalculates a value. +- ### Example -An optional utility function is passed as the first argument to the callback, if given a state, the value of the state will be returned (changes to this state still triggers updates unlike `unwrap`), if given a value the value is returned. + ```lua + local count = wrap(0) + local text = derive(function() return `count: {count()}` end) -> ⚠️ The callback cannot yield. + text() -- "count: 0" -### Example + count(1) -```lua -local count = wrap(0) -local text = derive(function() return "Count: "..count.Value end) + text() -- "count: 1" + ``` -print(text.Value) -- "Count: 0" +-------------------------------------------------------------------------------- -count.Value += 1 +## map() -print(text.Value) -- "Count: 1" -``` +Maps each value in a table state to a new table state. -```lua -local count = wrap(0) -local text = derive(function(from) - return "Count: "..from(count) -end) -``` +- ### Type -A shorthand method for deriving states also exists, following example is equivalent to the above: + ```lua + function map( + source: () -> Map, + transform: (value: () -> VI, index: KI) -> VO + ): Map -```lua -local count = wrap(0) -local text = "Count: "..count -- all binary operators are supported -``` +- ### 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. -## foreach() + Anytime an existing index changes, the transform function is not rerun, + instead the passed state for that index will update, causing anything + depending on it to update too. -Derives a new state object from an existing state object. -Designed to work specifically with table states. + Returns a state containing the mapped key-value pairs. -Also works with non state tables. + > ⚠️ Non-yielding. -### Type +- ### Example -```lua -function foreach( -- number as first arg - i: number, - transform: (key: number) -> (KO, VO), - cleanup: (KO, VO) -> ()? -): Map + ```lua + type Item = { + name: string, + icon: number + } -function foreach( -- table as first arg - table: Map, - transform: (key: KI, value: VI) -> (KO, VO), - cleanup: (KO, VO) -> ()? -): Map + local items = source {} :: () -> Array -function foreach( -- state as first arg - state: State>, - transform: (key: KI, value: VI) -> (KO, VO), - cleanup: (KO, VO) -> ()? -): State> -``` + local displays = map(numbers, function(item, i) + return ItemDisplay { + Name = function() + return item().name + end, -### Details + Image = function() + return "rbxassetid://" .. item().icon + end, -When the state being derived from is updated, the derived state will -recompute by applying its transform function to each key-value pair. + LayoutOrder = i + } + end) + ``` -Will only be recomputed if the corresponding key differs between updates. - -Has an optional cleanup function to cleanup the old key and value. - -> ⚠️ The transform function cannot yield. - -### Example - -```lua -local numbers = wrap { 1, 2, 3 } -local plusOne = foreach(numbers, function(i, v) - return i, v + 1 -end) - -print(plusOne.Value) -- { [1]: 2, [2]: 3, [3]: 4 } - --- note that assignment must take place to trigger reactive updates. --- modifying the value without assignment `numbers.Value[2] = 5` will not trigger updates. -numbers.Value = { 1, 5, 3 } - -print(plusOne.Value) -- { [1]: 2, [2]: 6, [3]: 4 } -``` - -------------------------------------------------------------------- - -
- -## match() - -Derives a new state object from an existing state object. -Similar to switch statements in other languages. - -### Type - -```lua -function match(value: K): (transform: Map) -> V -function Match(state: State): (transform: Map) -> State -``` - -### 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 -``` - -------------------------------------------------------------------- - -
+-------------------------------------------------------------------------------- ## watch() Runs a callback on state change. -### Type +- ### Type -```lua -function watch(callback: () -> Cleanup?): Unwatch + ```lua + function watch(callback: () -> ()): Unwatch -type Cleanup = () -> () -type Unwatch = () -> () -``` + type Unwatch = () -> () + ``` -### Details +- ### Details -The callback is ran immediately to determine what states to watch. + The callback is ran immediately to determine what states are referenced. -Any time a state read in the watch callback is changed, the watcher callback will be deferred -to the end of the resumption cycle and ran. + Any time a state referenced in the callback is changed, the callback will be + reran. -Only states in the immediate function scope can trigger the watch callback. + Also returns a function that when called, stops the watcher immediately. -Watchers are run *before* UI properties are updated. + > ⚠️ Non yielding. -The callback can return an optional cleanup function that is run each time the watcher is rerun. +- ### Example -Also returns a function that when called, stops the watcher immediately (also runs cleanup if any was given). + ```lua + local state = wrap(1) -> ⚠️ The callback cannot yield. + watch(function() + print(state.Value) + end) -### Example + -- prints 1 -```lua -local state = wrap(1) + state.Value += 1 -watch(function() - print(state.Value) -end) + -- prints 2 + ``` --- prints 1 - -state.Value += 1 - --- prints 2 -``` - -------------------------------------------------------------------- +-------------------------------------------------------------------------------- diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md index 885b4da..afe61b4 100644 --- a/docs/api/reactivity-utility.md +++ b/docs/api/reactivity-utility.md @@ -1,152 +1,27 @@ # Reactivity API: Utility -
+## cleanup() -## isState() +Runs a callback anytime a reactive scope is re-ran. -Determines if a given value is a state object or not. +- ### Type -### Type + ```lua + function cleanup(callback: () -> ()) + ``` -```lua -function isState(value: unknown): boolean -``` +- ### Example -### Example + ```lua + local data = source(1) -```lua -local value = wrap() + watch(function() + local label = create "TextLabel" { Text = data } -print(isState(value)) -- true + cleanup(function() + label:Destroy() + end) + end) + ``` -value = 0 - -print(isState(value)) -- false -``` - -------------------------------------------------------------------- - -
- -## unwrap() - -Unwraps a state and returns its stored value. - -### Type - -```lua -function unwrap(value: T | State): 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 -``` - -------------------------------------------------------------------- - -
- -## 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(state: State): State -``` - -### 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 -``` - -------------------------------------------------------------------- - -
- -## mutate() - -Mutates a given state's value and updated any derived states. - -### Type - -```lua -function mutate(value: T | State): 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 -``` - -
Motivation for this function - -```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 -``` - -
- -------------------------------------------------------------------- +-------------------------------------------------------------------------------- diff --git a/docs/api/strict.md b/docs/api/strict.md index 42d3dc0..15856d9 100644 --- a/docs/api/strict.md +++ b/docs/api/strict.md @@ -1,57 +1,27 @@ # Strict Mode -
+Vide has a special mode called "strict mode" which is used for debugging. -## strict +The purpose of strict mode is to help ensure stateful code is *pure* +(deterministic and free from side effects) or if there are side-effects, that +they are cleaned up correctly. -A flag that users can set to enable or disable strict mode (disabled by default). +Vide is set to strict by doing: -### Type - -```ts -boolean strict = false +```lua +local vide = require(path_to_vide) +vide.strict = true ``` -### 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. +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 set to `true` -and to set it back to false when pushing to production. +It is recommend to develop UI with strict mode and to disable it 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 -``` +-------------------------------------------------------------------------------- diff --git a/docs/api/types.md b/docs/api/types.md deleted file mode 100644 index 66e1e7b..0000000 --- a/docs/api/types.md +++ /dev/null @@ -1,55 +0,0 @@ -# Types API - -
- -## State\ - -A type representing a Vide state object. - -### Type - -```lua -type State = { - Value: T, - value: T -} -``` - -### Example - -```lua -local state: State = wrap(1) - -local derived: State = "Count: " .. state -``` - -------------------------------------------------------------------- - -
- -## Prop\ - -A utility type representing a union of a value and a state. - -### Type - -```lua -type Prop = T | State -``` - -### Example - -```lua -type BackgroundProps = { - Position: Prop, - Size: Prop -} -local function Background(props: { Position: Prop }) - return create("Frame") { - Position = props.Position - Size = props.Size - } -end -``` - -------------------------------------------------------------------- diff --git a/src/init.luau b/src/init.luau index 9802013..1e4e4d9 100644 --- a/src/init.luau +++ b/src/init.luau @@ -36,8 +36,14 @@ local vide = { -- runtime step = function(dt: number) + -- debug.profilebegin("VIDE STEP") + -- debug.profilebegin("VIDE SPRING") update_springs(dt) + -- debug.profileend() + -- debug.profilebegin("VIDE GARBAGE CLEANUP") clean_garbage() + -- debug.profileend() + -- debug.profileend() end } diff --git a/src/spring.luau b/src/spring.luau index d33ea5a..bcd7168 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -3,18 +3,20 @@ if not game then script = (require :: any) "test/wrap-require" end --[[ Supported datatypes: -*number -!bool -*CFrame -?Rect -*Color3 -*UDim -*UDim2 -*Vector2 -!Vector2int16 -*Vector3 -!Vector3int16 -!EnumItem +- number +- CFrame +- Color3 +- UDim +- UDim2 +- Vector2 +- Vector3 + +Unsupported datatypes: +- bool +- Rect +- Vector2int16 +- Vector3int16 +- EnumItem ]] diff --git a/todo.md b/todo.md index 203b7d5..d20ec85 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,8 @@ # todo +- strict mode + - warn when map returns primitive + - Implement from solid - onCleanup - Index